subcent

Webhooks API

Webhooks deliver real-time event notifications to your application. When events occur (payments, escrow updates, vault alerts), Subcent sends HTTP POST requests to your registered URLs with signed payloads.

Create Webhook#

POST
/v1/webhooks

Register a new webhook endpoint for a vault.

Request Body#

| Field | Type | Required | Description | |---|---|---|---| | vault_id | string | Yes | The vault to receive events for | | url | string | Yes | The HTTPS URL to receive webhook deliveries | | events | array | Yes | Array of event types to subscribe to |

Example Request#

{
  "vault_id": "550e8400-e29b-41d4-a716-446655440000",
  "url": "https://your-app.com/webhooks/subcent",
  "events": [
    "payment.completed",
    "payment.pending_approval",
    "payment.rejected",
    "escrow.funded",
    "escrow.released",
    "vault.low_balance"
  ]
}

Response#

{
  "webhook_id": "whk_aa0e8400-e29b-41d4-a716-446655440000",
  "vault_id": "550e8400-e29b-41d4-a716-446655440000",
  "url": "https://your-app.com/webhooks/subcent",
  "secret": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8",
  "events": [
    "payment.completed",
    "payment.pending_approval",
    "payment.rejected",
    "escrow.funded",
    "escrow.released",
    "vault.low_balance"
  ],
  "status": "active",
  "created_at": "2025-01-15T10:30:00.000Z"
}
x

The secret field (prefixed with whsec_) is returned only once. Store it securely -- you will need it to verify webhook signatures. It cannot be retrieved again.


List Webhooks#

GET
/v1/webhooks

List all webhooks registered for a vault.

Query Parameters#

| Parameter | Type | Required | Description | |---|---|---|---| | vault_id | string | Yes | The vault to list webhooks for |

Response#

[
  {
    "webhook_id": "whk_aa0e8400-e29b-41d4-a716-446655440000",
    "url": "https://your-app.com/webhooks/subcent",
    "events": ["payment.completed", "payment.pending_approval"],
    "status": "active",
    "created_at": "2025-01-15T10:30:00.000Z"
  }
]

Delete Webhook#

DELETE
/v1/webhooks/:id

Delete a webhook endpoint.

Path Parameters#

| Parameter | Type | Description | |---|---|---| | id | string | The webhook ID |

Response#

{
  "deleted": true
}

Errors#

| Error Code | Description | |---|---| | webhook_not_found | No webhook exists with the given ID |


Event Types#

Subcent emits the following 11 event types:

Payment Events#

| Event | Description | |---|---| | payment.completed | A payment was auto-approved or manually approved and settled on-chain | | payment.pending_approval | A payment requires human approval before proceeding | | payment.rejected | A payment was rejected by policy or by human decision | | payment.failed | A payment failed during on-chain execution |

Escrow Events#

| Event | Description | |---|---| | escrow.funded | A new escrow was created and funded | | escrow.released | Escrow funds were released to the merchant | | escrow.refunded | Escrow funds were returned to the vault | | escrow.disputed | A dispute was raised on an escrow |

Vault Events#

| Event | Description | |---|---| | vault.low_balance | The vault balance has dropped below a threshold | | vault.frozen | The vault was frozen |

Policy Events#

| Event | Description | |---|---| | policy.updated | A policy was created or updated |


Webhook Payload#

Every webhook delivery is a JSON POST request with the following structure:

{
  "event": "payment.completed",
  "event_id": "evt_a1b2c3d4e5f6g7h8i9j0k1l2",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "data": {
    "payment_id": "pay_770e8400...",
    "status": "completed",
    "amount": "25.00",
    "currency": "USDC",
    "merchant_id": "mrc_660e8400...",
    "vault_id": "550e8400...",
    "agent_id": "agt_550e8400...",
    "transaction": {
      "chain": "solana",
      "tx_hash": "5UfDuX...",
      "finalized_at": "2025-01-15T10:30:02.000Z"
    }
  }
}

Signature Verification#

Every webhook request includes an X-Subcent-Signature header containing an HMAC-SHA256 signature of the raw request body, signed with your webhook secret.

Verification Steps#

  1. Read the raw request body as a string
  2. Compute HMAC-SHA256(webhook_secret, raw_body)
  3. Compare the result with the X-Subcent-Signature header value
  4. Use a constant-time comparison to prevent timing attacks

TypeScript

Retry Behavior#

Subcent retries failed webhook deliveries with exponential backoff:

| Attempt | Delay | Total Wait | |---|---|---| | 1st (initial) | Immediate | 0 seconds | | 2nd (retry 1) | 2 seconds | 2 seconds | | 3rd (retry 2) | 4 seconds | 6 seconds |

After 3 failed attempts, the delivery is marked as failed and no further retries are attempted. A delivery is considered failed if:

  • Your endpoint returns a non-2xx HTTP status code
  • Your endpoint does not respond within 10 seconds
  • The connection cannot be established

Always return a 200 status code quickly from your webhook handler. If you need to do heavy processing, queue the event and process it asynchronously.