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.
Using TypeScript? The SDK verifies each delivery and hands you a typed event, so you can skip the manual HMAC checks.
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://app.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. payment.pending is a valid, subscribable value like any other event type — the examples above omit it because they focus on terminal states, not because it's unfilterable. 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.
Identifying our requests
Every delivery carries this User-Agent:
User-Agent: Amboss-Webhooks/1.0 (+https://docs.amboss.tech/payments/webhooks)This header helps identify Amboss webhook deliveries in logs. It does not authenticate requests; always verify the webhook signature before processing.
If your firewall blocks deliveries, inspect its security logs and configure an exception scoped to your webhook endpoint, the POST method, and the specific blocking rule.
HTTP 403 responses are not retried. HTTP 408, 429, 5xx, timeouts, and network errors are retried up to eight total attempts.
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.