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

# Webhooks

> The two requests your endpoint receives for the widget's buy and sell flow, and how to verify their signature.

The widget notifies your backend of two things: a **buy** asks for your approval before it executes, and every operation reports its **result** once it's done. Both are `POST` requests to the webhook URL configured on your account.

<Note>
  This is a separate contract from the [Crypto as a Service webhooks](/crypto-as-a-service/webhooks/introduction) used by the core API — the widget's events use their own envelope and their own HMAC signature, documented below.
</Note>

### Envelope

Every message shares the same envelope:

```json theme={null}
{
  "event_id": "0f1c9a3e-5b7d-4c21-9f0a-6d2e8b41c7a5",
  "message_type": "transaction_approval_request",
  "issued_at": "2024-01-11T19:43:26.881527Z",
  "message": { "...": "..." }
}
```

| Key            | Type     | Description                                                                                                                  |
| -------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `event_id`     | *string* | Unique identifier of this event. A retry of the same logical event carries the same `event_id` — key your idempotency on it. |
| `message_type` | *string* | `transaction_approval_request` or `transaction_result`.                                                                      |
| `issued_at`    | *string* | ISO datetime the message was issued.                                                                                         |
| `message`      | *object* | The event's own payload, described below.                                                                                    |

### Verifying the signature

Every request carries an `Http-X-Bff-Signature-256` header: an HMAC-SHA256 signature of the raw request body, computed with the signing key issued alongside your `client_id`/`client_secret`.

```
Http-X-Bff-Signature-256: sha256=<hex-encoded HMAC>
```

**Python**

```python theme={null}
import hashlib
import hmac

def verify_signature(raw_body: bytes, signature_header: str, signing_key: str) -> bool:
    expected = "sha256=" + hmac.new(
        signing_key.encode("utf-8"), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)
```

**Node.js**

```javascript theme={null}
const crypto = require("crypto");

function verifySignature(rawBody, signatureHeader, signingKey) {
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", signingKey).update(rawBody).digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signatureHeader)
  );
}
```

<Warning>
  Always compare signatures with a constant-time comparison (`hmac.compare_digest`, `crypto.timingSafeEqual`) — a naive `==` leaks timing information an attacker can use to forge a signature byte by byte.
</Warning>

### `transaction_approval_request`

Sent before a **buy** executes. Your endpoint must respond within a few seconds; if it doesn't respond, or responds with anything other than `{"approved": true}`, the operation is **denied** — this request is fail-closed.

```json theme={null}
{
  "event_id": "df9d41d8-ac80-4125-9738-ce1fcb8dc523",
  "message_type": "transaction_approval_request",
  "issued_at": "2024-01-11T19:43:26.881527Z",
  "message": {
    "op_type": "BUY",
    "id": "df9d41d8-ac80-4125-9738-ce1fcb8dc523",
    "quote_id": "cf6c57ea-03a1-41dc-99e8-b91cc1b7333e",
    "amount": "1500",
    "external_ref": "00010203-0405-0607-0809-0a0b0c0d0e0f"
  }
}
```

| Key            | Type     | Description                                                                                                          |
| -------------- | -------- | -------------------------------------------------------------------------------------------------------------------- |
| `op_type`      | *string* | Always `BUY` — a sell doesn't need approval, since Ripio holds the custody of the crypto being sold.                 |
| `id`           | *string* | Correlates this request with the `transaction_result` that follows.                                                  |
| `quote_id`     | *string* | The quote the user accepted.                                                                                         |
| `amount`       | *string* | The fiat amount of the purchase.                                                                                     |
| `external_ref` | *string* | The user's identifier, as you provided it on [`POST /auth`](/crypto-as-a-service/widget/get-started/authentication). |

To approve, respond `HTTP 200` with:

```json theme={null}
{
  "approved": true
}
```

Any other response — a different status code, a body without `"approved": true`, or no response before the timeout — denies the operation.

### `transaction_result`

Sent once an operation — buy or sell — finishes, so you can update your own record of the user's balance. Delivered **at least once**, with retries on failure; use `event_id` to discard duplicates.

**On success:**

```json theme={null}
{
  "event_id": "57b526a5-092b-4d61-a394-b80e4bad5fcf",
  "message_type": "transaction_result",
  "issued_at": "2024-01-11T19:43:26.881527Z",
  "message": {
    "succeed": true,
    "quote": {
      "id": "df9d41d8-ac80-4125-9738-ce1fcb8dc523",
      "quote_id": "cf6c57ea-03a1-41dc-99e8-b91cc1b7333e",
      "txn_id": "57b526a5-092b-4d61-a394-b80e4bad5fcf",
      "rate": "100290.76161713",
      "charged_fee": "628.74685436",
      "base_amount": "0.00868727",
      "quote_amount": "1500.00000000",
      "base_asset": "BTC",
      "quote_asset": "ARS",
      "op_type": "BUY",
      "external_ref": "00010203-0405-0607-0809-0a0b0c0d0e0f",
      "created_at": "2024-01-11T19:43:22.765995Z"
    }
  }
}
```

**On failure:**

```json theme={null}
{
  "event_id": "57b526a5-092b-4d61-a394-b80e4bad5fcf",
  "message_type": "transaction_result",
  "issued_at": "2024-01-11T19:43:26.881527Z",
  "message": {
    "succeed": false,
    "failure_result": {
      "id": "df9d41d8-ac80-4125-9738-ce1fcb8dc523",
      "quote_id": "cf6c57ea-03a1-41dc-99e8-b91cc1b7333e",
      "code": "20001",
      "base_amount": "0.00868727",
      "quote_amount": "1500.00000000",
      "op_type": "BUY",
      "external_ref": "00010203-0405-0607-0809-0a0b0c0d0e0f",
      "created_at": "2024-01-11T19:43:22.765995Z"
    }
  }
}
```

| Key              | Type                  | Description                                                                                    |
| ---------------- | --------------------- | ---------------------------------------------------------------------------------------------- |
| `succeed`        | *boolean*             | Whether the operation completed.                                                               |
| `quote`          | *object* (on success) | The executed operation: rate, fee, both amounts and assets.                                    |
| `failure_result` | *object* (on failure) | The failed operation, with an error `code` instead of a rate.                                  |
| `id`             | *string*              | The same value sent in `transaction_approval_request` for a buy — use it to correlate the two. |
| `quote_id`       | *string*              | The quote that was executed.                                                                   |
| `txn_id`         | *string*              | The resulting transaction's identifier.                                                        |
| `created_at`     | *string*              | ISO datetime, UTC.                                                                             |

<Note>
  For a buy, persisting `id` is generally enough to reference the operation later if you need to reach out to Ripio about it.
</Note>

### Configuring your endpoint

Your webhook URL and signing key are set on your account during onboarding — reach out to your Ripio contact to configure or change them.

**Key considerations for your endpoint:**

* **HTTPS.** Your endpoint URL must use HTTPS.
* **Respond quickly.** For `transaction_approval_request`, respond within the timeout window — the operation is fail-closed, so a slow response is the same as a denial.
* **Idempotency.** Design your handler around `event_id`: processing the same event twice must not double-count anything on your side.
