Webhooks
Verify Amboss Payments webhooks with the SDK. Instance and static APIs, raw-body handling for Express, NestJS, and Fetch, and typed error codes.
The SDK verifies webhook signatures for you. Give it the raw body, the signature, and the timestamp; it checks the HMAC-SHA256 signature in constant time and returns a typed PaymentEvent. No manual crypto, no timing-attack footguns.
Verify every webhook before acting on it. An unverified webhook is just an HTTP POST from anyone on the internet. For the protocol details, headers, retries, and replay protection, see Verify Webhooks.
Instance API
Construct a client with a webhookSecret, then verify each delivery:
const payments = new Payments({
webhookSecret: process.env.AMBOSS_WEBHOOK_SECRET,
});
const event = payments.webhooks.verify({
payload, // string | Buffer, the RAW request body
signature, // x-webhook-signature header value
timestamp, // x-webhook-timestamp header value
toleranceSeconds: 300, // optional; default 300
});
event.event_type; // 'payment.pending' | 'payment.completed' | 'payment.failed'
event.data.id;Static API
For stateless verification without constructing a client, pass the secret inline:
import { Payments } from "@ambosstech/payments";
const event = Payments.webhooks.verify({
secret: process.env.AMBOSS_WEBHOOK_SECRET,
payload,
signature,
timestamp,
});Raw body matters
The HMAC is computed over `${timestamp}.${rawBody}`. If your framework parses JSON before your handler runs, the re-serialized bytes will not match and verification fails. Capture the raw body before any parser runs.
app.post(
"/webhook",
express.raw({ type: "application/json" }),
(req, res) => {
const event = payments.webhooks.verify({
payload: req.body, // Buffer, because of express.raw
signature: req.header("x-webhook-signature")!,
timestamp: req.header("x-webhook-timestamp")!,
});
// dispatch on event.event_type, dedupe on event.id
res.sendStatus(200);
},
);// Enable raw body capture when creating the app:
// const app = await NestFactory.create(AppModule, { rawBody: true });
import { Payments, WebhookVerificationError } from "@ambosstech/payments";
@Post("webhook")
handle(
@Req() req: RawBodyRequest<Request>,
@Headers() headers: Record<string, string>,
) {
try {
const event = this.payments.webhooks.verify({
payload: req.rawBody!,
signature: headers["x-webhook-signature"],
timestamp: headers["x-webhook-timestamp"],
});
return { ok: true, event };
} catch (error) {
if (error instanceof WebhookVerificationError) {
throw new BadRequestException(error.code);
}
throw error;
}
}export async function POST(request: Request) {
const rawBody = await request.text();
const event = payments.webhooks.verify({
payload: rawBody,
signature: request.headers.get("x-webhook-signature")!,
timestamp: request.headers.get("x-webhook-timestamp")!,
});
// dispatch on event.event_type, dedupe on event.id
return new Response(null, { status: 200 });
}The event payload
verify returns a typed PaymentEvent. The envelope carries routing fields; data carries the payment:
event.id; // envelope id; dedupe on this
event.event_type; // 'payment.pending' | 'payment.completed' | 'payment.failed'
event.environment; // 'sandbox' | 'production'
event.wallet_id;
event.data.status; // 'pending' | 'completed' | 'failed'
event.data.direction; // 'send' | 'receive'
event.data.amount; // { amount, asset_id, asset_symbol, precision }
event.data.settled_at;
event.data.payment_details.payment_hash;Error codes
Every verification failure throws a WebhookVerificationError with a typed code:
| Code | Meaning |
|---|---|
missing_secret | Constructor webhookSecret was not provided (instance API). |
missing_signature | The signature parameter was empty. |
missing_timestamp | The timestamp parameter was empty. |
invalid_timestamp | timestamp is not numeric. |
timestamp_out_of_tolerance | Clock skew exceeded toleranceSeconds. |
invalid_signature_format | Signature is not valid hex or has the wrong length. |
signature_mismatch | HMAC did not match: wrong secret, tampered body, or wrong payload bytes. |
invalid_payload_json | Signature verified but the body is not valid JSON. |
import { WebhookVerificationError } from "@ambosstech/payments";
try {
const event = payments.webhooks.verify({ payload, signature, timestamp });
} catch (error) {
if (error instanceof WebhookVerificationError) {
// error.code is one of the values above
return res.status(401).send(error.code);
}
throw error;
}Signature algorithm
For reference, the SDK computes:
expected = HMAC_SHA256(secret, `${timestamp}.${rawBody}`)
header = expected.toString('hex')Comparison uses crypto.timingSafeEqual.
Next steps
Transactions
Create Lightning invoices and send payments to BOLT11 invoices or Lightning Addresses with the Amboss Payments SDK, including live progress and sandbox testing.
Errors
Typed error classes thrown by the Amboss Payments SDK: ApiError, ConfigError, NetworkError, WebhookVerificationError, DecryptionError, and PaymentSendError.