# send21-io Webhooks API

**Canonical:** https://apis.io/apis/send21-io/send21-io-webhooks-api/  
**Provider:** send21-io — https://apis.io/providers/send21-io/  
**Base URL:** https://send21.io  
**Documentation:** https://send21.io/docs

send21-io Webhooks API is one of 13 APIs that [send21-io](https://apis.io/providers/send21-io/) publishes on the [APIs.io](https://apis.io/) network, described by a machine-readable OpenAPI specification. This API exposes 6 JSON Schema definitions. Tagged areas include Webhooks. The published artifact set on APIs.io includes an OpenAPI specification and 6 JSON Schemas.

**send21 calls your server when a draft changes.** Register an endpoint with `POST /api/v1/webhooks` (scope `webhooks:manage`) and pick the events you want. The response contains the signing secret once; store it. Send a signed test event any time with `POST /api/v1/webhooks/{id}/test`. ### Delivery A `POST` to your URL with `Content-Type: application/json` and this body: ```json { "event": "draft.confirmed", "data": { ... } } ``` | Header | Meaning | |---|---| | `X-Send21-Event` | The event type, same as `event` in the body. | | `X-Send21-Delivery` | Unique delivery id. Retries reuse it, so use it to ignore duplicates. | | `X-Send21-Signature-V2` | `t=<timestamp>,v1=<hex HMAC-SHA256 of "<timestamp>.<delivery id>.<raw body>">`, keyed with your secret. Verify this one: it also signs the time and the delivery id, so a copied delivery cannot be replayed later. Refuse deliveries older than 5 minutes (300 seconds). | | `X-Send21-Timestamp` | When the delivery was sent, in Unix seconds (the same value as `t=`). | | `X-Send21-Signature` | Original signature, kept for existing integrations: `sha256=` followed by the lowercase hex HMAC-SHA256 of the raw body only. It has no timestamp, so a copied delivery passes it again. | Any 2xx response counts as delivered. Anything else, or no answer within 15 seconds, is retried after 1, 2, 4, 8 minutes and so on, up to 8 attempts. Deliveries can arrive out of order. ### Verifying the signature Check `X-Send21-Signature-V2`: compute the HMAC over `<timestamp>.<delivery id>.<raw body>`, using the raw request bytes before parsing the JSON, compare in constant time, and refuse deliveries more than 300 seconds old. Answer 401 when the header is missing or wrong. The same check as on send21.io/docs/webhooks: ```js // X-Send21-Signature-V2: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<delivery id>.<raw body>"> const sig = String(req.headers['x-send21-signature-v2'] ?? ''); const t = /(?:^|,)t=(\d+)/.exec(sig)?.[1]; const v1 = /(?:^|,)v1=([0-9a-f]{64})/.exec(sig)?.[1]; const deliveryId = String(req.headers['x-send21-delivery'] ?? ''); const fresh = t !== undefined && Math.abs(Date.now() / 1000 - Number(t)) <= 300; const expected = crypto.createHmac('sha256', secret) .update(t + '.' + deliveryId + '.').update(rawBody).digest('hex'); const ok = fresh && v1 !== undefined && crypto.timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex')); if (!ok) return res.status(401).end(); ``` Before you ship goods, check that the confirmed amount, currency and `orderId` are the ones you expect, or fetch `GET /api/v1/drafts/{id}` with your API key and check its status there. ### Events | Event | When | |---|---| | `draft.created` | A draft was created: by you, an API key, a recurring template, or a payer picking a currency on a pay link. | | `draft.seen` | A payment for the draft was seen, but is not confirmed yet. Also sent when you accept a payment with a different amount (`acceptedByOwner: true`); `draft.confirmed` follows once that transfer has the required confirmations. | | `draft.confirmed` | The payment reached the required confirmations. For Lightning: the provider confirmed it with a valid preimage. | | `draft.amount_mismatch` | A transfer needs your decision (`reviewState: pending`). `reviewReason: "amount"`: it arrived within 10% of the billed amount, but not the exact amount. `reviewReason: "rate_lock_expired"`: a pay-link payment of the exact amount arrived more than 10 minutes after its rate lock ended, so it was priced at an older rate. It does not pay the draft until you accept it with `POST /api/v1/drafts/{id}/accept-received`; decline it with `POST /api/v1/drafts/{id}/reject-received`. Sent once. | | `draft.mismatch_accepted` | A payment with a different amount was accepted, by you (`acceptReason`) or by the draft's `shortfallPolicy` (`acceptReason: "auto: shortfallPolicy"`). Carries `expectedAmountSats`, `receivedAmountSats`, `acceptedBy` and `acceptedAt`. | | `draft.mismatch_rejected` | You declined it (`rejectReason`, `rejectedBy`, `rejectedAt`). `refund: "manual"`: the money is in your wallet and send21 cannot return it. | | `draft.mismatch_reminder` | Still undecided 2 hours before the draft expires. Sent once. | | `draft.mismatch_unresolved` | The draft expired or was cancelled while the transfer was undecided. The money arrived and is in your wallet; you can still accept it. | | `draft.expired` | The draft expired unpaid. If an undecided transfer with a different amount arrived, `receivedAmountSats` is set and `reviewState` is `unresolved`. Pay-link payments also carry `opened`, `quoteRequested`, `firstOpenedAt` and `quoteRequestedAt`. | | `draft.cancelled` | The draft was cancelled. | | `payment_request.expired` | A pay link expired unpaid. `opened`: the payer loaded the pay page (link previews and bots ignored). `quoteRequested`: they picked a currency. `reviewState` if a differing transfer arrived. No IP or payer data. | | `test` | Sent by the test endpoint. `data` has `message` and `occurredAt`. | ### Payload of draft.seen, draft.confirmed, draft.amount_mismatch and draft.expired Amounts ending in `Sats` are base units of the sent asset: 8 decimals for BTC, 6 for USDC, USDT and EURC, 9 for SOL and ETH (ETH amounts are in gwei). `txId` is the transaction id or signature; for Lightning it is the payment hash. For native ETH found by the balance fallback from a smart-wallet transfer, it is `block-<number>`. `receivedAmountSats` and `receivedAmount` are set only when the amount differs from the billed amount, including on the `draft.confirmed` of an accepted short or over payment, so you can record what arrived. `paymentRequestId` and `orderId` are set for pay-link payments. ```json { "event": "draft.amount_mismatch", "data": { "draftId": "3f6c2a9e-5d1b-4c8e-9a7f-2b4d6e8f0a1c", "status": "AwaitingPayment", "txId": "5KqZ9bQ...", "confirmations": 1, "receivedAmountSats": 49250000, "receivedAmount": "49.250000", "sentCurrency": "USDC", "network": "Solana", "amountSats": 50000000, "sentAmount": "50.000000", "fiatCurrency": "EUR", "fiatAmount": 46.00, "conversionRate": 0.92, "rateSource": "coinbase", "platformFee": { "accrued": true, "waived": false, "percent": 0.39, "feeUsd": 0.20, "feeBaseUnits": 195000, "feeAmount": "0.195000", "currency": "USDC" }, "paymentRequestId": "8d1e4b7a-0c2f-4e6d-b3a9-7f5c1e2d4a6b", "orderId": "ORDER-1042", "occurredAt": "2026-10-06T08:22:22Z" } } ``` ### Payload of draft.created and draft.cancelled `draft.created` has `draftId`, `amountSats`, `network` and `occurredAt`; pay-link drafts add `paymentRequestId`, `orderId` and `currency`. `draft.cancelled` has `draftId`, `status`, `network`, `amountSats`, `paymentRequestId` and `occurredAt`.

## Operations (4)

| Method | Path | Summary |
|---|---|---|
| GET | `/api/v1/webhooks` | Get api webhooks |
| POST | `/api/v1/webhooks` | Create api webhooks |
| POST | `/api/v1/webhooks/{id}/test` | Create api webhooks by id test |
| DELETE | `/api/v1/webhooks/{id}` | Delete api webhooks by id |

## Machine-readable artifacts (7)

- **OpenAPI** — https://artifacts.apis.io/send21-io/openapi/send21-io-webhooks-api-openapi.yml
- **JSONSchema** — https://artifacts.apis.io/send21-io/json-schema/send21-io-draft-response-schema.json
- **JSONSchema** — https://artifacts.apis.io/send21-io/json-schema/send21-io-payment-request-response-schema.json
- **JSONSchema** — https://artifacts.apis.io/send21-io/json-schema/send21-io-quote-info-schema.json
- **JSONSchema** — https://artifacts.apis.io/send21-io/json-schema/send21-io-create-draft-request-schema.json
- **JSONSchema** — https://artifacts.apis.io/send21-io/json-schema/send21-io-fee-breakdown-response-schema.json
- **JSONSchema** — https://artifacts.apis.io/send21-io/json-schema/send21-io-demo-draft-response-schema.json

## Other send21-io APIs (12)

- [send21-io Account API](https://apis.io/apis/send21-io/send21-io-account-api/)
- [send21-io Auth API](https://apis.io/apis/send21-io/send21-io-auth-api/)
- [send21-io Checkout API](https://apis.io/apis/send21-io/send21-io-checkout-api/)
- [send21-io Demo API](https://apis.io/apis/send21-io/send21-io-demo-api/)
- [send21-io Drafts API](https://apis.io/apis/send21-io/send21-io-drafts-api/)
- [send21-io Fees API](https://apis.io/apis/send21-io/send21-io-fees-api/)
- [send21-io Rates API](https://apis.io/apis/send21-io/send21-io-rates-api/)
- [send21-io Recurring Templates API](https://apis.io/apis/send21-io/send21-io-recurringtemplates-api/)
- [send21-io Suggestions API](https://apis.io/apis/send21-io/send21-io-suggestions-api/)
- [send21-io Address Book API](https://apis.io/apis/send21-io/send21-io-address-book-api/)
- [send21-io API Keys API](https://apis.io/apis/send21-io/send21-io-api-keys-api/)
- [send21-io Payment Requests API](https://apis.io/apis/send21-io/send21-io-payment-requests-api/)

## Tags

Webhooks

---

Profiled by [API Evangelist](https://apievangelist.com) and published on [APIs.io](https://apis.io/apis/send21-io/send21-io-webhooks-api/). The API's provider profile, Kin Score and agent-readiness rating are at https://apis.io/providers/send21-io/.
