Easy Labs
API

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

  1. Register an endpoint via the webhooks API or the dashboard. You'll get a one-time secret — store it immediately, it's never re-exposed.
  2. Receive deliveries at your URL — Easy Labs POSTs a signed JSON payload.
  3. Verify the signature with EasyWebhooks.constructEvent before trusting the payload.
  4. Respond 2xx within 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:

HeaderValue
content-typeapplication/json
x-easy-webhook-signaturesha256=<hex> — HMAC-SHA256 of the raw body using your endpoint's signing secret
x-easy-eventThe event type, e.g. payment.created
x-easy-delivery-idUUID identifying this specific delivery attempt
x-easy-webhook-attemptAttempt 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'
end

Native 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 initiated
  • payment.updated — payment status changed (succeeded, failed, refunded, etc.)
  • refund.created — a refund is initiated
  • refund.updated — refund status changed
  • authorization.created — an authorization is created (manual capture flow)
  • authorization.updated — authorization status changed
  • authorization.voided — authorization explicitly voided
  • dispute.created — a chargeback or pre-dispute opens
  • dispute.updated — dispute moves through its lifecycle (under-review, won, lost, etc.)
  • checkout.session.completed — a hosted/embedded Checkout session closes successfully
  • checkout.session.crypto_confirmed — a crypto checkout confirms on-chain

Billing

  • subscription.created — a new subscription is created
  • subscription.updated — subscription fields change (plan, quantity, billing cycle, etc.)
  • subscription.deleted — subscription canceled
  • subscription.paused — subscription paused (collection paused or fully halted)
  • subscription.resumed — subscription resumed
  • subscription.trial_will_end — fires 3 days before a trial ends
  • subscription.pending_update_applied — a scheduled update took effect
  • subscription.pending_update_expired — a scheduled update expired before applying
  • invoice.created — a draft invoice is created
  • invoice.finalized — invoice is finalized and ready to send/charge
  • invoice.paid — invoice fully paid
  • invoice.payment_failed — payment attempt failed (triggers dunning)
  • invoice.upcoming — fires before the next renewal so you can preview the invoice
  • invoice.voided — invoice voided
  • invoice.marked_uncollectible — invoice flagged unrecoverable
  • revenue_recovery.action_completed — a dunning recovery step fired (retry, email, etc.)
  • coupon.created / coupon.updated / coupon.deleted
  • promotion_code.created / promotion_code.updated / promotion_code.deleted

Treasury

  • settlement.created — a settlement batch lands

Account

  • identity.created — KYB / merchant identity record created
  • identity.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 signing secret once)
  • GET /webhooks — list endpoints
  • GET /webhooks/{id} — get a single endpoint (no secret in response)
  • PATCH /webhooks/{id} — update URL, events, or active status
  • DELETE /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.

On this page