Webhooks
How Easy Labs notifies your server about asynchronous events — endpoint setup, signature verification, retry behavior, and the full event catalog.
Easy Labs delivers asynchronous notifications about account activity by POST-ing JSON to an HTTPS endpoint you register. Use webhooks to react to payments completing, subscriptions renewing, disputes opening, or settlements landing — without polling.
Quickstart
- Register an endpoint via the
webhooksAPI or the dashboard. You'll get a one-timesecret— store it immediately, it's never re-exposed. - Receive deliveries at your URL — Easy Labs
POSTs a signed JSON payload. - Verify the signature with
EasyWebhooks.constructEventbefore trusting the payload. - Respond
2xxwithin 30 seconds. Anything else triggers a retry.
import express from 'express';
import { EasyWebhooks } from '@easylabs/node';
const app = express();
// IMPORTANT: use raw body — JSON re-stringifying breaks the signature.
app.post(
'/webhooks/easy',
express.raw({ type: 'application/json' }),
(req, res) => {
const event = EasyWebhooks.constructEvent(
req.body.toString('utf8'),
req.header('x-easy-webhook-signature') ?? '',
process.env.EASY_WEBHOOK_SECRET!,
);
switch (event.type) {
case 'payment.created':
// ...
break;
case 'subscription.updated':
// ...
break;
}
res.status(204).end();
},
);Delivery format
Each delivery is an HTTPS POST to your registered endpoint URL with these headers:
| Header | Value |
|---|---|
content-type | application/json |
x-easy-webhook-signature | sha256=<hex> — HMAC-SHA256 of the raw body using your endpoint's signing secret |
x-easy-event | The event type, e.g. payment.created |
x-easy-delivery-id | UUID identifying this specific delivery attempt |
x-easy-webhook-attempt | Attempt number, "1" through "3" |
The body is a JSON WebhookEvent:
{
"id": "evt_01HABCDEFGHIJK",
"type": "payment.created",
"created_at": "2026-05-03T12:34:56.789Z",
"created": "2026-05-03T12:34:56.789Z",
"api_version": "2026-05-01",
"data": { /* event-specific payload */ },
"previous_attributes": { /* present on `*.updated` events */ },
"requested": {
"id": "req_01HXYZ...",
"idempotency_key": "your-key"
}
}Verifying deliveries
The signing secret returned at registration is your shared key. Verify every delivery before acting on it — anyone who knows your endpoint URL can POST to it.
Node.js
import { EasyWebhooks } from '@easylabs/node';
const event = EasyWebhooks.constructEvent(
rawBody, // exact request body string
req.header('x-easy-webhook-signature')!, // sha256=<hex>
process.env.EASY_WEBHOOK_SECRET!,
);constructEvent throws an EasyApiError (status 400) on any of:
- Missing or malformed signature header
- Signature is not valid hex
- Signature does not match the computed HMAC (timing-safe comparison)
- Body is not valid JSON
On success it returns a typed WebhookEvent.
Other languages
The verification recipe is identical: HMAC-SHA256(body, secret), hex-encoded, prefixed with sha256=, compared in constant time.
# Python — manual verification (SDK helper coming soon)
import hashlib, hmac
expected = 'sha256=' + hmac.new(
secret.encode('utf-8'),
raw_body.encode('utf-8'),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, signature_header):
raise ValueError('Invalid webhook signature')# Ruby — manual verification (SDK helper coming soon)
require 'openssl'
expected = 'sha256=' + OpenSSL::HMAC.hexdigest('SHA256', secret, raw_body)
unless Rack::Utils.secure_compare(expected, signature_header)
raise 'Invalid webhook signature'
endNative EasyWebhooks helpers for Ruby and Python ship with a future SDK round — until then, use the manual verification above.
Replay protection
The current signature covers the body only — there is no signed timestamp. If you receive the same x-easy-delivery-id twice (because Easy Labs retried after a network blip and your server actually succeeded), idempotency is your responsibility: dedupe on event.id or x-easy-delivery-id.
A signed timestamp + tolerance: parameter is on the SDK gap-fix tracker (item #2). When it ships, EasyWebhooks.constructEvent will accept a tolerance argument and reject deliveries older than the window.
Retry behavior
Easy Labs retries any delivery that doesn't return a 2xx within 30 seconds. The dispatcher attempts each delivery up to 3 times before giving up, with exponential backoff between attempts. The attempt count is exposed on every delivery via x-easy-webhook-attempt.
If your endpoint fails enough consecutive deliveries, the endpoint's consecutive_failures counter increments and Easy Labs eventually marks it disabled. Re-enable it via the dashboard or the update endpoint API.
To replay a delivery on demand, find it in the dashboard's webhook log and click "Resend" — useful for testing handlers without waiting for real activity.
Event types
Subscribe to specific events when registering an endpoint, or use ["*"] to catch everything. The full catalog (37 event types as of api_version 2026-05-01):
Payments
payment.created— a new payment is initiatedpayment.updated— payment status changed (succeeded, failed, refunded, etc.)refund.created— a refund is initiatedrefund.updated— refund status changedauthorization.created— an authorization is created (manual capture flow)authorization.updated— authorization status changedauthorization.voided— authorization explicitly voideddispute.created— a chargeback or pre-dispute opensdispute.updated— dispute moves through its lifecycle (under-review, won, lost, etc.)checkout.session.completed— a hosted/embedded Checkout session closes successfullycheckout.session.crypto_confirmed— a crypto checkout confirms on-chain
Billing
subscription.created— a new subscription is createdsubscription.updated— subscription fields change (plan, quantity, billing cycle, etc.)subscription.deleted— subscription canceledsubscription.paused— subscription paused (collection paused or fully halted)subscription.resumed— subscription resumedsubscription.trial_will_end— fires 3 days before a trial endssubscription.pending_update_applied— a scheduled update took effectsubscription.pending_update_expired— a scheduled update expired before applyinginvoice.created— a draft invoice is createdinvoice.finalized— invoice is finalized and ready to send/chargeinvoice.paid— invoice fully paidinvoice.payment_failed— payment attempt failed (triggers dunning)invoice.upcoming— fires before the next renewal so you can preview the invoiceinvoice.voided— invoice voidedinvoice.marked_uncollectible— invoice flagged unrecoverablerevenue_recovery.action_completed— a dunning recovery step fired (retry, email, etc.)coupon.created/coupon.updated/coupon.deletedpromotion_code.created/promotion_code.updated/promotion_code.deleted
Treasury
settlement.created— a settlement batch lands
Account
identity.created— KYB / merchant identity record createdidentity.updated— KYB / merchant identity record changed (incl. status transitions)
Test
test.webhook— fires when you click "Send test event" in the dashboard
Endpoint management
Webhook endpoints are managed under the account-and-operations API:
POST /webhooks— register a new endpoint (returns the signingsecretonce)GET /webhooks— list endpointsGET /webhooks/{id}— get a single endpoint (no secret in response)PATCH /webhooks/{id}— update URL, events, or active statusDELETE /webhooks/{id}— remove an endpoint
Each endpoint exposes:
interface WebhookEndpoint {
id: string;
url: string;
events: string[]; // e.g. ["payment.created", "*"]
active: boolean;
status: 'enabled' | 'disabled';
consecutive_failures: number;
last_triggered_at: string | null;
created_at: string;
updated_at: string;
}Inspecting deliveries
Every delivery attempt is logged. Query the delivery log to debug failures or replay events:
const deliveries = await client.listWebhookDeliveries({
endpoint_id: 'whe_01HABCD...',
success: false,
created_after: '2026-05-01T00:00:00Z',
limit: 50,
});The dashboard surfaces the same data with one-click "Resend" — useful for replaying without writing code.
Related
- Webhooks API — endpoint management operations
- Node SDK reference —
EasyWebhooks.constructEventand theWebhookEventtype - SDK gap-fix tracker #2 — signed timestamp + replay protection roadmap