> ## Documentation Index
> Fetch the complete documentation index at: https://docs-test.rye.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time notifications when checkout intent state changes occur.

When integrating with Rye's Universal Checkout API, you might need your backend to perform actions in response to changes in Rye's system. For example, notifying a customer when their order completes, or handling a failed offer retrieval.

While polling Rye's APIs is possible, it can be inefficient and increases system load. To support event-driven architectures, Rye provides webhook functionality. Webhooks enable you to receive real-time updates when checkout intent state transitions occur.

## Use Cases

* **Order Fulfillment:** Trigger downstream processes when a checkout intent completes.
* **Error Handling:** React immediately when an offer retrieval or order placement fails.
* **Customer Notifications:** Send real-time updates to buyers about their order status.

## Setting Up Webhooks

<Steps>
  <Step title="Navigate to Account Settings">
    Go to the [Rye Console](https://console.rye.com/account) and find the Webhooks section on the accounts tab.
  </Step>

  <Step title="Set Up an Endpoint">
    Enter your publicly accessible endpoint URL. It must be reachable over the internet and capable of handling HTTP POST requests.
  </Step>

  <Step title="URL Verification Handshake">
    After saving your endpoint, Rye sends a verification challenge. Your endpoint must respond with the ID of the webhook endpoint, found under `source.id`:

    **Verification Request:**

    ```json theme={null}
    {
      "id": "evt_e56b5b2b40d4491fbab2ecb96bc911a7",
      "object": "event",
      "type": "webhook_endpoint.verification_challenge",
      "createdAt": "2026-03-18T02:30:00.000Z",
      "source": {
        "type": "webhook_endpoint",
        "id": "we_dc275d547a374a73aceca0e4e1a045d2"
      }
    }
    ```

    **Expected Response:**

    ```json theme={null}
    {
      "challenge": "we_dc275d547a374a73aceca0e4e1a045d2"
    }
    ```
  </Step>

  <Step title="Implement Signature Verification">
    Each webhook includes a cryptographic signature to verify its authenticity. See the [verification section](#verifying-webhook-signatures) below.
  </Step>
</Steps>

## Payload Format

Most webhook payloads are **thin events** — they contain the resource ID and the event type, not the full resource. To get the complete state, call the API after receiving the event.

**Example Payload:**

```json theme={null}
{
  "id": "evt_ci_abc123def456_completed",
  "object": "event",
  "type": "checkout_intent.completed",
  "createdAt": "2026-03-18T02:30:00.000Z",
  "source": {
    "type": "checkout_intent",
    "id": "ci_abc123def456"
  }
}
```

After receiving this event, fetch the full checkout intent:

```bash theme={null}
curl -X GET https://api.rye.com/api/v1/checkout-intents/ci_abc123def456 \
  -H "Authorization: Bearer YOUR_API_KEY"
```

<Info>
  Thin events keep webhook payloads small and stable. The API always returns the most current state of the resource, so fetching after notification ensures you have the latest data.
</Info>

Some events (e.g. [`product.updated`](/api-v2/webhooks/types#products)) also include a `data` field carrying a snapshot of the resource at the time the event was emitted. This is a convenience for consumers that don't need strict consistency, but it does not replace fetching. The snapshot can be stale if events are delivered out of order or the underlying resource changes between emission and delivery. For authoritative state, still call the corresponding lookup endpoint using `source.id` — the same pattern as with thin events.

## Webhook Headers

Every webhook request includes the following headers:

| Header            | Description                                          |
| ----------------- | ---------------------------------------------------- |
| `x-rye-signature` | HMAC-SHA256 signature for verifying authenticity.    |
| `x-rye-event-id`  | Unique event ID.                                     |
| `x-rye-timestamp` | Unix timestamp (seconds) of the event delivery.      |
| `x-rye-topic`     | The event topic (e.g., `checkout_intent.completed`). |

<Warning>
  **CORS Configuration:** Ensure your backend allows Rye's custom headers (`x-rye-signature`, `x-rye-event-id`, `x-rye-timestamp`, `x-rye-topic`) in your CORS policy.
</Warning>

## Verifying Webhook Signatures

Each webhook is signed using HMAC-SHA256 with your HMAC secret key, available in the [Rye Console](https://console.rye.com/account) under the Webhooks section.

### Signature Format

The `x-rye-signature` header contains a hex-encoded HMAC-SHA256 signature of the raw request body:

```
v0=5bc05741c3dcbfe8187b980fe9bf1fe22c1fbd3aa56b9d78387667c04e8d27c3
```

### Verification Steps

SDKs contain an `event.unwrap(...)` helper function which can be used to verify a signature and parse a webhook payload. If you are using a language we do not have an SDK for, you can manually verify webhook signatures by following the instructions below:

1. Extract the signature value from the `x-rye-signature` header (strip the `v0=` prefix).
2. Compute HMAC-SHA256 of the raw request body using your secret key.
3. Hex-encode the result and compare with the extracted signature using a constant-time comparison.
4. Reject the request if they do not match.

### Code Examples

<CodeGroup>
  ```typescript webhook.ts theme={null}
  import { CheckoutIntents } from "checkout-intents";
  import express from "express";

  const app = express();
  const client = new CheckoutIntents();

  const WEBHOOK_SECRET = process.env.RYE_HMAC_SECRET_KEY!;

  app.use(express.raw({ type: "application/json" }));

  app.post("/webhook", (req, res) => {
    const signatureHeader = req.headers["x-rye-signature"] as string;
    if (!signatureHeader) {
      return res.status(401).send("Unauthorized");
    }

    const event = client.events.unwrap(
      req.body.toString(),
      signatureHeader,
      WEBHOOK_SECRET,
    );

    // Handle the challenge during setup
    if (event.type === "webhook_endpoint.verification_challenge") {
      return res.json({ challenge: event.source.id });
    }

    // Process the webhook event
    const topic = req.headers["x-rye-topic"];
    console.log(`Received event: ${topic}`);
    res.status(200).send("OK");
  });

  app.listen(3000, () => {
    console.log("Webhook server listening on port 3000");
  });
  ```

  ```python webhook.py theme={null}
  from flask import Flask, request, jsonify
  import os
  import hmac
  import hashlib
  from checkout_intents import CheckoutIntentsClient

  app = Flask(__name__)
  WEBHOOK_SECRET = os.environ["RYE_HMAC_SECRET_KEY"]
  client = CheckoutIntentsClient()


  @app.route("/webhook", methods=["POST"])
  def webhook():
      signature_header = request.headers.get("x-rye-signature")
      if not signature_header:
          return "Unauthorized", 401

      event = client.events.unwrap(
          request.get_data(),
          signature_header,
          WEBHOOK_SECRET,
      )

      # Handle the challenge during setup
      if event.type == "webhook_endpoint.verification_challenge":
          return jsonify({"challenge": event.source.id})

      # Process the webhook event
      topic = request.headers.get("x-rye-topic")
      print(f"Received event: {topic}")
      return "OK", 200


  if __name__ == "__main__":
      app.run(port=3000)
  ```

  ```java WebhookController.java theme={null}
  import com.rye.client.CheckoutIntentsClient;
  import com.rye.client.okhttp.CheckoutIntentsOkHttpClient;
  import com.rye.models.events.Event;
  import com.rye.models.events.WebhookSignatureVerificationException;
  import org.springframework.http.ResponseEntity;
  import org.springframework.web.bind.annotation.*;
  import com.fasterxml.jackson.databind.JsonNode;
  import com.fasterxml.jackson.databind.ObjectMapper;
  import java.util.Map;

  @RestController
  public class WebhookController {

      private static final String WEBHOOK_SECRET = System.getenv("RYE_HMAC_SECRET_KEY");
      private static final ObjectMapper mapper = new ObjectMapper();

      private final CheckoutIntentsClient client = CheckoutIntentsOkHttpClient.builder()
          .apiKey(System.getenv("RYE_API_KEY"))
          .build();

      @PostMapping("/webhook")
      public ResponseEntity<?> handleWebhook(
              @RequestBody String body,
              @RequestHeader(value = "x-rye-signature", required = false) String signatureHeader,
              @RequestHeader(value = "x-rye-topic", required = false) String topic) {
          Event event;
          try {
              event = client.events().unwrap(body, signatureHeader, WEBHOOK_SECRET);
          } catch (WebhookSignatureVerificationException e) {
              return ResponseEntity.status(401).body("Unauthorized");
          }

          // Handle the challenge during setup
          if (event.type().equals(Event.Type.WEBHOOK_ENDPOINT_VERIFICATION_CHALLENGE)) {
              return ResponseEntity.ok(Map.of("challenge", event.source().id()));
          }

          // Process the webhook event
          System.out.println("Received event: " + topic + " for " + event.source().id());
          return ResponseEntity.ok("OK");
      }
  }
  ```

  ```ruby webhook.rb theme={null}
  # gem install sinatra
  require "sinatra"
  require "openssl"
  require "json"

  WEBHOOK_SECRET = ENV["RYE_HMAC_SECRET_KEY"]

  def verify_signature(body, signature_header)
    return false unless signature_header&.start_with?("v0=")
    expected_signature = signature_header[3..]

    computed_signature = OpenSSL::HMAC.hexdigest("sha256", WEBHOOK_SECRET, body)

    Rack::Utils.secure_compare(computed_signature, expected_signature)
  end

  post "/webhook" do
    request.body.rewind
    body = request.body.read
    signature_header = request.env["HTTP_X_RYE_SIGNATURE"]

    unless signature_header && verify_signature(body, signature_header)
      halt 401, "Unauthorized"
    end

    data = JSON.parse(body)

    # Handle the challenge during setup
    if data["type"] == "WEBHOOK_URL_VERIFICATION" && data.dig("data", "challenge")
      content_type :json
      return { challenge: data["data"]["challenge"] }.to_json
    end

    # Process the webhook event
    topic = request.env["HTTP_X_RYE_TOPIC"]
    puts "Received event: #{topic}"
    status 200
    "OK"
  end
  ```
</CodeGroup>

## Best Practices

### Process Webhooks Asynchronously

Webhook requests must respond within **5 seconds**. Verify the signature, acknowledge receipt, and process the event in the background.

### Handle Out-of-Order Delivery

Webhooks may arrive out of order. Use the `createdAt` field to determine event ordering, or simply fetch the latest state from the API — since events are thin notifications, the API always returns the most current data.

### Deduplicate Events

Each event has a deterministic `id` (e.g., `evt_ci_abc123def456_completed`). Store processed event IDs to skip duplicates:

1. Extract the event `id` from the payload.
2. Check if you've already processed this `id`.
3. If yes, return `200` without reprocessing.
4. If no, process the event and store the `id`.

### Fetch Fresh State

Since webhook payloads are thin, always call the API to get the full checkout intent after receiving an event. This ensures you have the most up-to-date data, regardless of delivery order.
