Webhooks
Subscribe to payment lifecycle events with Amboss webhooks. Per-environment endpoints, signed deliveries, and at-least-once retries.
Webhooks notify your backend of payment events. Register one or more endpoints per environment and we deliver an HTTP POST whenever a relevant transition occurs.
Event types
| Event | When it fires |
|---|---|
payment.pending | An invoice is created and awaiting payment |
payment.completed | The transaction settled successfully (terminal) |
payment.failed | The transaction failed — routing failure, recipient rejection, etc. (terminal) |
payment.expired | The invoice's expires_at passed without payment (terminal) |
All events cover both receive and send transactions — distinguish with data.direction ("receive" or "send").
Register an endpoint
The URL must be HTTPS and resolve to a public IP — the API rejects loopback, private ranges, and .local / .internal hostnames.
mutation CreateWebhookEndpoint {
payment {
webhook_endpoint {
create(input: {
environment_id: "81b73615-ddf3-46e3-943a-467c3e442e04"
url: "https://api.example.com/webhooks/amboss"
event_filters: ["payment.completed", "payment.failed", "payment.expired"]
}) {
secret
endpoint {
id
url
event_filters
active
}
}
}
}
}curl -X POST https://rails.amboss.tech/graphql \
-H "Content-Type: application/json" \
-H "x-api-key: $AMBOSS_API_KEY" \
-d '{
"query": "mutation($input: CreateWebhookEndpointInput!) { payment { webhook_endpoint { create(input: $input) { secret endpoint { id url event_filters active } } } } }",
"variables": {
"input": {
"environment_id": "81b73615-ddf3-46e3-943a-467c3e442e04",
"url": "https://api.example.com/webhooks/amboss",
"event_filters": ["payment.completed", "payment.failed", "payment.expired"]
}
}
}'Example response:
{
"data": {
"payment": {
"webhook_endpoint": {
"create": {
"secret": "whsec_PvDaoBz6TXhuMKxk0VkckiPUTX9BTSDyCT2Pqc_vgOw",
"endpoint": {
"id": "wep_01HX9YQK7P8MVZ3FN4G2RWS6CD",
"url": "https://api.example.com/webhooks/amboss",
"event_filters": ["payment.completed", "payment.failed", "payment.expired"],
"active": true
}
}
}
}
}
}secret is returned once. It's the HMAC key for verifying delivery signatures — store it in your secret manager. If lost, rotate it with rotate_secret.
event_filters is optional. Omit it (or pass []) to receive every event type. Otherwise the endpoint only receives the listed events. Discover the live list at runtime:
query AvailableEvents {
payment {
webhook_endpoint {
available_event_types
}
}
}Payload shape
Every delivery is JSON with this envelope:
type WebhookEnvelope = {
id: string; // unique per event — dedupe on this
event_type:
| "payment.pending"
| "payment.completed"
| "payment.failed"
| "payment.expired";
environment: "sandbox" | "live";
environment_id: string;
wallet_id: string;
node_id: string | null; // null in sandbox
data: {
id: string;
direction: "receive" | "send";
status: string;
payment_details: {
payment_type: "bolt11";
payment_request: string | null;
payment_hash: string | null;
};
amount: { amount: string; asset_id: string; asset_symbol: string; precision: number } | null;
fee: { amount: string; asset_id: string; asset_symbol: string; precision: number } | null;
settle_amount: { amount: string; asset_id: string; asset_symbol: string; precision: number } | null;
exchange_rate: string | null;
description: string | null;
settled_at: string | null; // ISO 8601
expires_at: string | null; // ISO 8601
metadata: object | null; // round-tripped from create_receive / create_send
};
};Example payment.completed for a USDT receive:
{
"id": "payment.completed:tx_01HX9YQK7P8MVZ3FN4G2RWS6CD",
"event_type": "payment.completed",
"environment": "sandbox",
"environment_id": "81b73615-ddf3-46e3-943a-467c3e442e04",
"wallet_id": "5e4b1e2a-9f3c-4a5b-8c7d-1234567890ab",
"node_id": null,
"data": {
"id": "tx_01HX9YQK7P8MVZ3FN4G2RWS6CD",
"direction": "receive",
"status": "completed",
"payment_details": {
"payment_type": "bolt11",
"payment_request": "lnbc1m1p0...",
"payment_hash": "3b6e7d..."
},
"amount": { "amount": "100000", "asset_id": "...", "asset_symbol": "USDT", "precision": 6 },
"fee": { "amount": "5", "asset_id": "...", "asset_symbol": "USDT", "precision": 6 },
"settle_amount": { "amount": "99995", "asset_id": "...", "asset_symbol": "USDT", "precision": 6 },
"exchange_rate": null,
"description": "Order #42",
"settled_at": "2026-06-02T12:31:42.000Z",
"expires_at": null,
"metadata": { "order_id": "42" }
}
}Signature verification
Each delivery is signed using the endpoint's secret (whsec_…). Three headers carry the signature, timestamp, and event type:
x-webhook-signature: <hex HMAC-SHA256 of `${timestamp}.${body}`>
x-webhook-timestamp: <unix seconds>
x-webhook-event: <event type, e.g. payment.completed>Always verify on your side before applying side effects, and always hash the raw request body rather than a parsed-and-re-stringified version. The full recipe with Node.js, Python, and Go examples — plus replay protection and rotation handling — is on Verify Webhooks.
Idempotency and retries
Webhooks are delivered at least once, with up to 8 attempts on retryable failures (status 408/429/5xx, timeouts, network errors). Backoff is 10s, then 60s, then 10 minutes per attempt — see Verify Webhooks → Retry policy for the full schedule. The envelope-level id (e.g. payment.completed:tx_01HX9YQK7P8MVZ3FN4G2RWS6CD) is stable across retries — store it and dedupe on it before applying side effects.
A minimal Node.js dedupe:
const seen = new Set();
app.post("/webhooks/amboss", express.json(), (req, res) => {
const { id, event_type, data } = req.body;
if (seen.has(id)) return res.sendStatus(200); // already processed
seen.add(id);
if (event_type === "payment.completed" && data.direction === "receive") {
fulfillOrder(data.metadata?.order_id, data.settle_amount);
}
res.sendStatus(200);
});In production, dedupe against a durable store (Redis, your DB) — not an in-memory Set.
Rotate the secret
mutation RotateSecret($id: String!) {
payment {
webhook_endpoint {
rotate_secret(id: $id) {
secret
endpoint { id }
}
}
}
}The new secret is returned once. Update your verifier to accept either old or new for a short overlap window, then switch over.
Update event filters or URL
mutation UpdateEndpoint($input: UpdateWebhookEndpointInput!) {
payment {
webhook_endpoint {
update(input: $input) {
id
url
event_filters
active
}
}
}
}Set active: false to pause delivery without deleting the endpoint.
Testing in sandbox
The dashboard has a "Generate test URL" button for sandbox webhooks that creates a one-off endpoint on webhook.site — useful for inspecting payload shape without standing up a real handler. Pair it with amb_sandbox_behavior metadata on create_receive / create_send to fire specific events on demand.