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

# Webhooks

How do I use Webhooks?

## Overview

Receive real-time notifications about events in your organization using Cybrid's webhook system.
Manage webhooks through the Organization API.

Configure your applications to receive events as they occur. Your back-end systems can then execute
actions based on these events.

To enable webhook events, register one or more webhook endpoints. Cybrid posts JSON payloads
containing a **SubscriptionEvent** object to your endpoints via HTTPS.

Webhooks notify you of asynchronous events: completed trades, settled transfers, and finished
identity verifications.

## Authenticate with the Organization API

### Use the Organization API

All webhook operations are in the **Organization API**, not the Bank API. Use Organization API
endpoints for webhook operations.

### Configure Authentication Scopes

Interact with webhook subscriptions using an OAuth2 access token with organization-level scopes.

**Required Scopes:**

| Scope                         | Description                            |
| ----------------------------- | -------------------------------------- |
| `subscriptions:read`          | List and retrieve subscription details |
| `subscriptions:write`         | Create subscriptions                   |
| `subscriptions:execute`       | Full subscription management           |
| `subscription_events:read`    | View webhook events                    |
| `subscription_events:execute` | Full event management                  |

### Get an Organization Token

Create an Organization Application in the IDP service, then request an OAuth2 token using client
credentials flow:

```http Request
POST https://id.{environment}.cybrid.app/oauth/token
Content-Type: application/json

{
  "grant_type": "client_credentials",
  "client_id": "client_id",
  "client_secret": "client_secret",
  "scope": "subscriptions:execute subscription_events:read"
}
```

```json Response
{
  "access_token": "access_token",
  "token_type": "Bearer",
  "expires_in": 3600
}
```

Use the access token in all Organization API requests:

```http
Authorization: Bearer {access_token}
```

> ⚠️ Common Authentication Errors
>
> * **401 Unauthorized**: Invalid or expired token
> * **403 Forbidden**: Token lacks required scopes
> * Using a bank token instead of organization token will not work

## Subscribe to Webhook Events

Cybrid sends event data when activity occurs in your account. Each event creates a new
**SubscriptionEvent** object. A single API request may create multiple events. For example,
creating a new transfer generates `transfer.storing`, `transfer.reviewing` and
`transfer.completed` events.

Register webhook endpoints in your organization to enable automatic delivery of
**SubscriptionEvent** objects via POST requests. Your app can then run back-end actions
(for example, informing the customer a trade has been settled).

<Tabs>
  <Tab title="Trade Events" icon="fa-coins" iconColor="black">
    Events related to cryptocurrency trading operations:

    | Event Type        | Description        | Use Case                  |
    | ----------------- | ------------------ | ------------------------- |
    | `trade.storing`   | Creating trade     | Initial state tracking    |
    | `trade.pending`   | Awaiting execution | Monitor pending trades    |
    | `trade.executed`  | Executed           | Track execution           |
    | `trade.settling`  | Settling           | Track settlement progress |
    | `trade.completed` | Completed          | Final confirmation        |
    | `trade.failed`    | Failed             | Handle failures           |
    | `trade.cancelled` | Cancelled          | Track cancellations       |
  </Tab>

  <Tab title="Transfer Events" icon="fa-right-left" iconColor="black">
    Events for money movement operations:

    | Event Type           | Description       | Use Case                  |
    | -------------------- | ----------------- | ------------------------- |
    | `transfer.storing`   | Creating transfer | Initial state tracking    |
    | `transfer.pending`   | Pending           | Monitor pending transfers |
    | `transfer.holding`   | On hold           | Track held transfers      |
    | `transfer.reviewing` | Under review      | Compliance workflows      |
    | `transfer.completed` | Completed         | Final confirmation        |
    | `transfer.failed`    | Failed            | Handle failures           |
  </Tab>

  <Tab title="Identity Verification Events" icon="fa-id-card" iconColor="black">
    Events for customer KYC/identity verification:

    | Event Type                        | Description              | Use Case               |
    | --------------------------------- | ------------------------ | ---------------------- |
    | `identity_verification.storing`   | Creating verification    | Initial tracking       |
    | `identity_verification.pending`   | In progress              | Monitor progress       |
    | `identity_verification.reviewing` | Requires manual review   | Compliance workflows   |
    | `identity_verification.waiting`   | Awaiting customer action | Customer notifications |
    | `identity_verification.expired`   | Expired                  | Re-verification needed |
    | `identity_verification.completed` | Completed                | Customer onboarding    |
  </Tab>

  <Tab title="Plan Events" icon="fa-clipboard-list" iconColor="black">
    Events for remittance plan operations:

    | Event Type       | Description                | Use Case                  |
    | ---------------- | -------------------------- | ------------------------- |
    | `plan.storing`   | Creating plan              | Initial state tracking    |
    | `plan.planning`  | Calculating rates and fees | Monitor planning progress |
    | `plan.completed` | Ready for execution        | Trigger execution         |
    | `plan.failed`    | Planning failed            | Handle failures           |
  </Tab>

  <Tab title="Execution Events" icon="fa-check-to-slot" iconColor="black">
    Events for remittance execution operations:

    | Event Type            | Description            | Use Case               |
    | --------------------- | ---------------------- | ---------------------- |
    | `execution.storing`   | Creating execution     | Initial state tracking |
    | `execution.executing` | Remittance in progress | Monitor progress       |
    | `execution.completed` | Completed              | Final confirmation     |
    | `execution.failed`    | Failed                 | Handle failures        |
  </Tab>

  <Tab title="Reserve Account Events" icon="fa-piggy-bank" iconColor="black">
    Events for reserve account balance monitoring:

    | Event Type                                   | Description         | Use Case               |
    | -------------------------------------------- | ------------------- | ---------------------- |
    | `account.balance_funding_pull.low`           | Approaching minimum | Early treasury warning |
    | `account.balance_funding_pull.below_minimum` | Below minimum       | Trigger top-up         |
  </Tab>
</Tabs>

To capture error events, listen for `trade.failed`, `transfer.failed`, `plan.failed`,
`execution.failed`, and/or `identity_verification.expired`.

## Create a Webhook Subscription

### Set up your webhook endpoint

Create a webhook endpoint handler to receive event data POST requests.

Set up an HTTPS endpoint function that accepts webhook requests with a POST method. For sandbox
setup you can use an HTTP endpoint.

> ⚠️ Production webhook endpoints must use HTTPS
>
> For production environments, your webhook URL MUST use HTTPS. HTTP URLs will be rejected with
> an `InvalidHttpsUrlException`. For sandbox/testing, HTTP URLs are allowed for local development.

Configure your endpoint function to handle POST requests with a JSON payload consisting of a
SubscriptionEvent object.

Return a successful status code (2xx) quickly, before any complex logic that could cause a timeout.
For example, you must return a 200 response before updating a transfer in your system.

> ⚠️ Endpoints must return a 2xx prior to any complex logic
>
> Your endpoint must respond with **2 seconds** to avoid timeouts.

> ℹ️ Registered webhook endpoints must be publicly accessible URLs
>
> You can register up to 5 webhook endpoints per organization.

### Register your webhook endpoint

After testing your webhook endpoint function, register the endpoint's accessible URL using the
[Subscription API](https://docs.cybrid.xyz/reference/createsubscription) so Cybrid knows where to
deliver events.

**Endpoint:** `POST /api/subscriptions`

**Request Parameters:**

| Field         | Type   | Required | Description                             |
| ------------- | ------ | -------- | --------------------------------------- |
| `type`        | string | Yes      | Must be `webhook` (only supported type) |
| `name`        | string | Yes      | Descriptive name (max 128 characters)   |
| `url`         | string | Yes      | Your HTTPS endpoint URL                 |
| `environment` | string | Yes      | `production` or `sandbox`               |

**Request Example:**

```http Request
POST /api/subscriptions
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN

{
  "name": "Production Trade Notifications",
  "type": "webhook",
  "url": "https://your-domain.com/webhooks/cybrid",
  "environment": "production"
}
```

```json Response
{
  "guid": "subscription_guid",
  "organization_guid": "organization_guid",
  "name": "Production Trade Notifications",
  "url": "https://your-domain.com/webhooks/cybrid",
  "environment": "production",
  "type": "webhook",
  "state": "storing",
  "signing_key": "c3ac436b171923eb688a6bf364f89effd90427f4d58018e25558af3ecd506bc5",
  "created_at": "2026-01-24T12:00:00Z"
}
```

> ⚠️ Save the signing\_key immediately
>
> Note the `signing_key` as it is only returned **once** and is needed to secure your webhook
> endpoint. The signing key is generated server-side (64-character hex string) and encrypted
> before storage. You cannot retrieve it later.

> ℹ️ Subscription activation
>
> A subscription becomes active only after the endpoint is available and responds to requests.

## Understand the Webhook Payload

When an event occurs, Cybrid sends an HTTP POST to your webhook URL.

### Headers

```http
Content-Type: application/json
X-Cybrid-Signature: {hmac_signature}
```

### SubscriptionEvent Object

The following is an example of a SubscriptionEvent:

```json
{
  "guid": "cf16b78e233464229c7eda5e979b25a8",
  "organization_guid": "ad9007e80f35ac6d343d43496cad2744",
  "bank_guid": "b3a9d531c26892954661735e1b7482b7",
  "event_type": "transfer.storing",
  "object_guid": "8f5ee194e5aa254feef3054bd9543939",
  "environment": "sandbox"
}
```

### Payload Fields

| Field               | Description                                                                                                                                                        |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `guid`              | Unique ID for this webhook event. Can also be used as an idempotency key.                                                                                          |
| `organization_guid` | Unique identifier for the organization that owns the object. This allows the same webhook endpoint to be registered with multiple organizations.                   |
| `bank_guid`         | Unique identifier for the bank that owns the object associated with the event.                                                                                     |
| `event_type`        | A tuple separated by `.` containing the type of the object (ie: trade, transfer or identity\_verification) and the type of event generated by the Cybrid platform. |
| `object_guid`       | Identifier for the object generating the event.                                                                                                                    |
| `environment`       | `production` or `sandbox`. You may receive both sandbox and production mode event delivery requests to your endpoints.                                             |

## Verify Webhook Signatures

Every webhook payload includes an `X-Cybrid-Signature` header containing an HMAC signature. You
MUST verify this signature to ensure the webhook came from Cybrid.

When Cybrid sends data to registered webhook endpoints, the payload is authenticated with a
hash-based message authentication code (HMAC). The key used to create the HMAC is the
`signing_key` provided on subscription registration (see section above). Verify it by running the
algorithm yourself with the payload and the key to re-create the HMAC. The HMAC is created with
the HMAC\_SHA256 algorithm, then encoded in hex. The HMAC is attached to the request in the
`X-Cybrid-Signature` header.

### Signature Verification Examples

<Tabs>
  <Tab title="JavaScript">
    ```javascript
    const express = require('express');
    const crypto = require('crypto');
    const bodyParser = require('body-parser');
    const PORT = 3333;
    // replace SIGNING_KEY with the key received when the webhook endpoint was created
    const SIGNING_KEY = "c3ac436b171923eb688a6bf364f89effd90427f4d58018e25558af3ecd506bc5"
    const ALGORITHM = 'sha256'
    const SIGNATURE_HEADER = 'X-Cybrid-Signature'
    const app = express();
    app.use(bodyParser.json({
        verify: (req, res, buf) => {
          req.rawBody = buf;
        }
      }));
    app.use(express.json());
    app.post('/webhook', async (req, res) => {
        try {
            const requestSignature = req.get(SIGNATURE_HEADER);
            const expectedSignature = crypto.createHmac(ALGORITHM, SIGNING_KEY)
              .update(req.rawBody)
              .digest('hex');
            const requestBuffer = Buffer.from(requestSignature || '', 'utf8');
            const expectedBuffer = Buffer.from(expectedSignature, 'utf8');
            const valid_request = requestBuffer.length === expectedBuffer.length
              && crypto.timingSafeEqual(requestBuffer, expectedBuffer);
            // respond with 200
            if (valid_request) {
                res.status(200).send("OK");
                console.log(`received payload ${req.body}`)
                // perform actions related to the event
            } else {
                res.status(403).send("INVALID SIGNATURE");
            }
        } catch (err) {
            res.status(500).send(err.toString());
        }
    })
    app.listen(PORT, () => {
        console.log(`Server running on ${PORT}`);
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    import hmac
    import hashlib
    import json

    def verify_webhook(payload_json, signature_header, signing_key):
        """Verify webhook signature"""

        # Compute expected signature
        message = json.dumps(payload_json)
        expected_signature = hmac.new(
            signing_key.encode('utf-8'),
            message.encode('utf-8'),
            hashlib.sha256
        ).hexdigest()

        # Compare signatures (use constant-time comparison)
        return hmac.compare_digest(signature_header, expected_signature)
    ```
  </Tab>
</Tabs>

> ⚠️ Security Best Practice
>
> Always use constant-time comparison when verifying signatures to prevent timing attacks.

## Webhook Delivery and Retry Logic

### Delivery Workflow

1. **Event Occurs** - A trade completes, transfer succeeds, etc.
2. **Event Created** - SubscriptionEvent record created
3. **Delivery Attempted** - HTTP POST sent to your webhook URL
4. **Retry Logic** - If delivery fails, exponential backoff retry

### Retry Strategy

Cybrid attempts to deliver a given event to your webhook endpoint for up to 7 days with an
exponential backoff.

| Attempt | Delay               | Total Time   |
| ------- | ------------------- | ------------ |
| 1       | Immediate           | 0s           |
| 2       | 2x base delay       | \~2s         |
| 3       | 4x base delay       | \~6s         |
| 4       | 8x base delay       | \~14s        |
| 5+      | Capped at max delay | Up to 7 days |

**Configuration:**

* **Base delay**: Exponential starting point (e.g., 2 seconds)
* **Max delay**: Cap on exponential growth (prevents extremely long delays)
* **Timeout**: \~7 days total retry period
* **Request timeout**: 10 seconds per HTTP request

Use the [SubscriptionDelivery API](https://docs.cybrid.xyz/reference/listsubscriptiondeliveries)
to view failing deliveries and when the next attempt will occur. If your endpoint subscription has
been disabled or deleted when Cybrid attempts a retry, future retries of that event are prevented.
If a delivery has failed, trigger a new retry cycle by creating a new delivery using
SubscriptionDelivery API.

### Delivery States

| State       | Description                                   |
| ----------- | --------------------------------------------- |
| `storing`   | Initial state, delivery queued                |
| `failing`   | Retries in progress (after \~10s of failures) |
| `completed` | Successfully delivered (2xx response)         |
| `failed`    | All retries exhausted or subscription deleted |

### Success Criteria

A webhook delivery is considered successful if your endpoint returns:

* HTTP 2xx status code (200, 201, 202, 204, etc.)

Any other response (4xx, 5xx, timeout, network error) triggers retry logic.

### Disable Behaviour

If your endpoint has not returned a successful response for multiple days (approximately 7 days),
Cybrid will automatically mark the endpoint as `failed` and skip future deliveries. To re-enable
the endpoint, create a new registration using the Subscription API.

**Subscription State Tracking:**

* `deliveries_failing_since`: Set on first delivery failure, cleared when delivery succeeds
* If failing for 7 days, subscription moves to `failed` state

## Common Errors and Troubleshooting

### Subscription Creation Errors

| Error                      | Cause                                         | Fix                                                     |
| -------------------------- | --------------------------------------------- | ------------------------------------------------------- |
| `InvalidHttpsUrlException` | URL uses HTTP instead of HTTPS in production  | Use HTTPS for production; HTTP allowed for sandbox only |
| `InvalidOrganization`      | Organization GUID doesn't exist or is invalid | Verify your organization GUID and authentication        |
| `ApiNotImplemented`        | Webhook API not enabled for your organization | Contact Cybrid to enable webhook access                 |

### Delivery Failures

#### Timeout Errors (`timeout`)

* **Cause**: Your endpoint didn't respond within 10 seconds
* **Fix**:
  * Optimize endpoint performance
  * Return 2xx immediately, process async
  * Check for network issues

#### Unreachable Errors (`unreachable`)

* **Cause**: DNS resolution failed, connection refused, network error
* **Fix**:
  * Verify URL is accessible from internet
  * Check firewall rules
  * Validate DNS configuration

#### Failed After Retries

* **Cause**: Repeated failures for \~7 days
* **Result**: Subscription marked as `failed`, deliveries stop
* **Fix**:
  * Resolve endpoint issues
  * Delete failed subscription
  * Create new subscription

### Authentication Errors

#### Wrong Token Type

* **Symptom**: 403 Forbidden
* **Cause**: Using bank token instead of organization token
* **Fix**: Get organization-scoped OAuth token

#### Missing Scopes

* **Symptom**: 403 Forbidden
* **Cause**: Token lacks `subscriptions:execute` scope
* **Fix**: Request token with proper scopes

## Follow Best Practices

### Endpoint Implementation

#### Return 2xx Quickly

```python
@app.route('/webhooks/cybrid', methods=['POST'])
def webhook_handler():
    # Verify signature first
    if not verify_signature(request):
        return '', 401

    # Queue for async processing
    queue.enqueue(process_webhook, request.json)

    # Return success immediately
    return '', 200
```

#### Implement Idempotency

* Store `guid` from webhook payload
* Skip processing duplicate events
* Use database unique constraints

#### Verify Signatures

* Always validate `X-Cybrid-Signature` header
* Reject webhooks with invalid signatures
* Use constant-time comparison

### Event Ordering

Cybrid may deliver events out of order from their generation sequence. For example, creating a
transfer generates the following events:

* `transfer.storing`
* `transfer.reviewing`
* `transfer.completed`

Your endpoint shouldn't expect delivery of these events in this order, and needs to handle
delivery accordingly. You can also use the API to fetch the updated object and react according
to the latest state of the object.

### Handle Duplicate Events

Webhook endpoints may occasionally receive the same event more than once. Log processed event
GUIDs and skip events you've already processed.

### Handle Events Asynchronously

Configure your handler to process incoming events with an asynchronous queue. Processing events
synchronously may cause scalability issues. Any large spike in webhook deliveries may overwhelm
your endpoint hosts. Asynchronous queues allow you to process the concurrent events at a rate
your system can support.

### Monitoring and Alerting

#### Track Delivery States

* Monitor subscription\_deliveries via API
* Alert on `failing` or `failed` states

#### Monitor Endpoint Health

* Log all webhook requests
* Track response times
* Alert on elevated error rates

#### Set Up Alerts

* Subscription enters `failed` state
* `deliveries_failing_since` is set for > 1 hour
* High delivery failure rates

### Roll Endpoint Signing Secrets Periodically

To keep your webhook handler safe, we recommend that you roll secrets periodically, or when you
suspect a compromised secret. The process of rolling a webhook signing secret consists of creating
a new registration (with a new signing key), updating your webhook handler and deleting the
previous registration. This process may lead to the same event being delivered multiple times but
your webhook handler should handle duplicate events.

### Testing

#### Use Sandbox Environment

* Test with `environment: sandbox`
* HTTP URLs allowed for local testing

#### Verify Retry Logic

* Temporarily break your endpoint
* Observe retry behavior
* Fix endpoint and confirm recovery

#### Test Event Types

* Create test trades, transfers, verifications
* Confirm correct event types received
* Validate payload structure

## API Reference Summary

### Subscription Endpoints

| Method | Endpoint                    | Description                                                                 |
| ------ | --------------------------- | --------------------------------------------------------------------------- |
| POST   | `/api/subscriptions`        | [Create subscription](https://docs.cybrid.xyz/reference/createsubscription) |
| GET    | `/api/subscriptions`        | [List subscriptions](https://docs.cybrid.xyz/reference/listsubscriptions)   |
| GET    | `/api/subscriptions/{guid}` | [Get subscription](https://docs.cybrid.xyz/reference/getsubscription)       |
| DELETE | `/api/subscriptions/{guid}` | [Delete subscription](https://docs.cybrid.xyz/reference/deletesubscription) |

### Event Endpoints

| Method | Endpoint                          | Description                                                             |
| ------ | --------------------------------- | ----------------------------------------------------------------------- |
| GET    | `/api/subscription_events`        | [List events](https://docs.cybrid.xyz/reference/listsubscriptionevents) |
| GET    | `/api/subscription_events/{guid}` | [Get event](https://docs.cybrid.xyz/reference/getsubscriptionevent)     |

### Delivery Endpoints

| Method | Endpoint                              | Description                                                                     |
| ------ | ------------------------------------- | ------------------------------------------------------------------------------- |
| GET    | `/api/subscription_deliveries`        | [List deliveries](https://docs.cybrid.xyz/reference/listsubscriptiondeliveries) |
| GET    | `/api/subscription_deliveries/{guid}` | [Get delivery](https://docs.cybrid.xyz/reference/getsubscriptiondelivery)       |

## Key Takeaways

* **Use Organization API** - Webhooks are in the Organization API, not the Bank API
* **Get Organization Token** - Use OAuth2 with `subscriptions:execute` scope
* **HTTPS Required** - Production webhooks must use HTTPS URLs
* **Verify Signatures** - Always validate `X-Cybrid-Signature` header with HMAC-SHA256
* **Handle Retries** - Return 2xx quickly; process async to avoid timeouts
* **Monitor Health** - Watch for `failing` states and `deliveries_failing_since` timestamps
* **Be Idempotent** - Use `guid` to handle duplicate deliveries
* **Save Signing Key** - The signing key is only returned once during subscription creation; store
  it securely