Skip to main content

Overview

Webhooks let you receive real-time order eventsorder.pending, order.paid, order.failed — as HTTP POST requests to your own URLs. Every delivery is signed with Anyway’s platform Ed25519 key so you can verify it came from us.
There is no per-endpoint secret. Anyway signs all webhook payloads with a single platform key; you verify them with the public key published at GET /v1/webhooks/signing-key. The public key is safe to cache and is the same for every endpoint.
Webhook endpoints are org-scoped: authenticate with a bearer token and target an organization via the x-org-id header.

Events

EventFires whendata.order.status
order.pendingA payment was submitted and is being verifiedPENDING
order.paidThe order settled successfully — fulfill herePAID
order.failedThe order failedFAILED
An empty or omitted events array subscribes the endpoint to all events.

GET /v1/webhook

List the org’s webhook endpoints (paginated).

Request

curl "https://api.anyway.sh/v1/webhook?page=1&size=50" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "x-org-id: org_123"
page
integer
default:"1"
size
integer
default:"50"
Optional filter on name / URL.

Response

{
  "records": [
    {
      "webhookId": "wh_abc123",
      "name": "Production",
      "url": "https://your-app.com/webhooks/anyway",
      "events": ["order.paid", "order.failed"],
      "active": true,
      "createdAt": "2026-06-03T12:00:00Z"
    }
  ],
  "total": 1,
  "size": 50,
  "current": 1,
  "pages": 1
}

POST /v1/webhook

Create a webhook endpoint.

Request

curl -X POST https://api.anyway.sh/v1/webhook \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "x-org-id: org_123" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Production",
    "url": "https://your-app.com/webhooks/anyway",
    "events": ["order.paid", "order.failed"]
  }'

Request Body

name
string
required
A label for the endpoint (1–255 chars).
url
string
required
The HTTPS URL to deliver events to.
events
array
Events to subscribe to. Omit or pass [] to receive all events. One of order.pending, order.paid, order.failed.

Response

The response does not contain a secret — payloads are signed with the platform key (see Verifying signatures).
{
  "webhookId": "wh_def456",
  "name": "Production",
  "url": "https://your-app.com/webhooks/anyway",
  "active": true,
  "createdAt": "2026-06-03T12:00:00Z"
}

PATCH /v1/webhook/{webhookId}

Update an endpoint. All fields are optional; only those present are changed.
curl -X PATCH https://api.anyway.sh/v1/webhook/wh_abc123 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "x-org-id: org_123" \
  -H "Content-Type: application/json" \
  -d '{ "events": ["order.paid"], "active": false }'
name
string
url
string
events
array
active
boolean
Enable or disable delivery without deleting the endpoint.

DELETE /v1/webhook/{webhookId}

Delete an endpoint.
curl -X DELETE https://api.anyway.sh/v1/webhook/wh_abc123 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "x-org-id: org_123"
Returns 204 No Content.

GET /v1/webhooks/signing-key

Public endpoint (no auth) that publishes the Ed25519 verification key. Fetch it once and cache it — it is shared across all your endpoints.
curl https://api.anyway.sh/v1/webhooks/signing-key
{
  "keys": [
    { "kty": "OKP", "crv": "Ed25519", "x": "11qYAYK...", "kid": "key_1", "use": "sig", "alg": "EdDSA" }
  ],
  "keyId": "key_1",
  "algorithm": "ed25519",
  "publicKey": "whpk_MCowBQYDK2VwAyEA..."
}
The publicKey (whpk_-prefixed) is consumable directly by the Standard Webhooks verifier libraries. The keys array is the equivalent JWKS (RFC 7517/8037) for generic JOSE tooling.

Payload

Anyway uses the Standard Webhooks envelope. The event id and timestamp are authoritative in the headers (webhook-id, webhook-timestamp); the body carries the typed event.
{
  "type": "order.paid",
  "timestamp": "2026-06-03T12:03:00Z",
  "apiVersion": "2026-06-01",
  "data": {
    "order": {
      "orderId": "ORD_9f8e7d6c",
      "orgId": "org_123",
      "merchantReference": "a0b1c2d3-e4f5-6789-abcd-ef0123456789",
      "status": "PAID",
      "provider": "CRYPTO",
      "amountCents": 4999,
      "currency": "usdc",
      "crypto": {
        "chainId": "eip155:8453",
        "asset": "eip155:8453/erc20:0x833589...2913",
        "assetSymbol": "USDC",
        "txHash": "0x9a3f...c21d",
        "payer": "eip155:8453:0xPayer...",
        "recipient": "eip155:8453:0xRecipient..."
      },
      "product": { "id": "prod_42", "name": "Pro Plan" },
      "paymentLinkId": "plink_abc",
      "createdAt": "2026-06-03T12:00:00Z",
      "updatedAt": "2026-06-03T12:03:00Z"
    }
  }
}

Envelope fields

FieldTypeNotes
typestringorder.pending · order.paid · order.failed. Route on this.
timestampstringRFC 3339, when the event fired.
apiVersionstringPayload schema version (e.g. 2026-06-01).
data.orderobjectThe order resource (below).

order object

FieldTypeNullableNotes
orderIdstringnoAnyway order id.
orgIdstringnoYour org id.
merchantReferencestringyesYour own reference, echoed back unchanged. Use as your idempotency / correlation key.
statusenumnoPENDING · PAID · FAILED — mirrors the event type.
providerenumnoCRYPTO · X402 · CREDITS.
amountCentsintegernoAmount in minor units of currency.
currencystringnoLowercase ISO 4217 / token symbol (e.g. usdc).
cryptoobjectyesOn-chain provenance; null for off-chain (CREDITS) orders.
crypto.chainIdstringCAIP-2 chain id (e.g. eip155:8453).
crypto.assetstringCAIP-19 asset id.
crypto.assetSymbolstringHuman-readable symbol (e.g. USDC).
crypto.txHashstringOn-chain transaction hash.
crypto.payerstringyesBuyer account, CAIP-10.
crypto.recipientstringyesYour receiving account, CAIP-10.
productobjectyes{ id, name } or null.
paymentLinkIdstringyesOriginating payment link id.
createdAt / updatedAtstringnoRFC 3339 UTC.

Verifying signatures

Each delivery carries three Standard-Webhooks headers:
HeaderExampleMeaning
webhook-idmsg_2abc...Unique delivery id — use to de-duplicate.
webhook-timestamp1717400180Unix seconds; reject if too old.
webhook-signaturev1a,<base64(ed25519 signature)>Space-separated list; one or more schemes.
The signature is an Ed25519 signature over the exact string {webhook-id}.{webhook-timestamp}.{raw-body}. Verify it with the published public key — easiest via a Standard Webhooks library:
# pip install standardwebhooks
from standardwebhooks import Webhook

# Fetch once from GET /v1/webhooks/signing-key, then cache.
wh = Webhook("whpk_MCowBQYDK2VwAyEA...")

@app.post("/webhooks/anyway")
def handle(request):
    try:
        # Raises on bad signature or stale timestamp.
        event = wh.verify(request.get_data(), dict(request.headers))
    except Exception:
        return Response(status=401)
    # event["type"] == "order.paid", event["data"]["order"], ...
Prefer the whpk_ key with an off-the-shelf Standard Webhooks verifier. If you verify manually, sign-check the raw Ed25519 signature (base64 after the v1a, prefix) of {webhook-id}.{webhook-timestamp}.{body} against the 32-byte key (base64-decode the publicKey after stripping whpk_).

Delivery & retries

  • Transport: POST with Content-Type: application/json; the envelope is the body.
  • Acknowledge: return any 2xx. Non-2xx responses and timeouts count as failed.
  • Timeout: 10 seconds per attempt.
  • Retries: up to 12 attempts with exponential backoff (capped at 1 hour); a Retry-After response header is honored. After the final attempt the event is dropped.
  • De-duplication: the same webhook-id may be delivered more than once — make your handler idempotent.
  • Ordering is not guaranteed: events may arrive out of order (e.g. paid before pending). Drive your state off status/type, not arrival order.