send21-io Webhooks API

**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=,v1=..">`, 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 `..`, 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=,v1=.."> 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)) `. `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

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 #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/send21-io:send21-io-webhooks-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

send21-io-webhooks-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Send21 Io Webhooks API
  version: v1
  contact:
    name: send21 integration support
    email: integration@send21.io
  description: 'Operations tagged Webhooks across 2 of this provider''s published API definitions: send21-io-openapi.json, swagger.json. Each path carries the servers of the definition it was published in.'
servers:
- url: https://send21.io
security:
- ApiKey: []
tags:
- name: Webhooks
  description: '**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=,v1=..">`, 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 `..`, 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=,v1=..">

    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)) `.

    `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`.'
paths:
  /api/v1/webhooks:
    get:
      tags:
      - Webhooks
      responses:
        '200':
          description: OK
          content:
            text/plain:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/WebhookResponse'
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/WebhookResponse'
            text/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/WebhookResponse'
      summary: Get api webhooks
      x-summary-source: derived
      operationId: getApiV1Webhooks
      x-operation-id-source: derived
    post:
      tags:
      - Webhooks
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWebhookRequest'
          text/json:
            schema:
              $ref: '#/components/schemas/CreateWebhookRequest'
          application/*+json:
            schema:
              $ref: '#/components/schemas/CreateWebhookRequest'
      responses:
        '200':
          description: OK
          content:
            text/plain:
              schema:
                $ref: '#/components/schemas/WebhookCreatedResponse'
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookCreatedResponse'
            text/json:
              schema:
                $ref: '#/components/schemas/WebhookCreatedResponse'
      summary: Create api webhooks
      x-summary-source: derived
      operationId: postApiV1Webhooks
      x-operation-id-source: derived
    servers:
    - url: https://send21.io
  /api/v1/webhooks/{id}/test:
    post:
      tags:
      - Webhooks
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '202':
          description: Accepted
          content:
            text/plain:
              schema:
                $ref: '#/components/schemas/QueuedResponse'
            application/json:
              schema:
                $ref: '#/components/schemas/QueuedResponse'
            text/json:
              schema:
                $ref: '#/components/schemas/QueuedResponse'
        '404':
          description: Not Found
          content:
            text/plain:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
            application/json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
            text/json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
      summary: Create api webhooks by id test
      x-summary-source: derived
      operationId: postApiV1WebhooksByIdTest
      x-operation-id-source: derived
    servers:
    - url: https://send21.io
  /api/v1/webhooks/{id}:
    delete:
      tags:
      - Webhooks
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '204':
          description: No Content
        '404':
          description: Not Found
          content:
            text/plain:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
            application/json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
            text/json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
      summary: Delete api webhooks by id
      x-summary-source: derived
      operationId: deleteApiV1WebhooksById
      x-operation-id-source: derived
    servers:
    - url: https://send21.io
components:
  schemas:
    WebhookResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        url:
          type:
          - string
          - 'null'
        events:
          type:
          - array
          - 'null'
          items:
            type: string
        isActive:
          type: boolean
        createdAt:
          type: string
          format: date-time
      additionalProperties: false
    CreateWebhookRequest:
      type: object
      properties:
        url:
          type:
          - string
          - 'null'
        events:
          type:
          - array
          - 'null'
          items:
            type: string
      additionalProperties: false
    ProblemDetails:
      type: object
      properties:
        type:
          type:
          - string
          - 'null'
        title:
          type:
          - string
          - 'null'
        status:
          type:
          - integer
          - 'null'
          format: int32
        detail:
          type:
          - string
          - 'null'
        instance:
          type:
          - string
          - 'null'
      additionalProperties: {}
    WebhookCreatedResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        secret:
          type:
          - string
          - 'null'
        warning:
          type:
          - string
          - 'null'
      additionalProperties: false
    QueuedResponse:
      type: object
      properties:
        queued:
          type: boolean
      additionalProperties: false
  securitySchemes:
    ApiKey:
      type: http
      description: 'AI-agent API key (Authorization: Bearer s21_...). Humans use passkey session cookies instead.'
      scheme: bearer
      bearerFormat: s21_...
x-refined-from:
- send21-io-openapi.json
- swagger.json