Docs

Webhooks

Listen for events from your Zoneless instance so your application can automatically react to payments, subscriptions, account changes, and payouts.

Zoneless sends webhook events to your application as HTTP POST requests containing a JSON Event object. This lets you respond to asynchronous events such as completed checkout, subscription renewals, account status changes, and payouts without continuously polling the API.

How it works

  • Register a webhook endpoint URL via the Platform Dashboard or the Webhook Endpoints API.
  • Zoneless sends a POST request to your URL whenever a subscribed event occurs.
  • Your handler verifies the signature, processes the event, and returns a 2xx response.

Register an endpoint

Register the public URL that should receive events in the Zoneless dashboard, or create it with the Webhook Endpoints API.

  • Open Developers in the test dashboard or Developers in the live dashboard.
  • Choose Developers in the side menu, find Webhook Endpoints, and choose Add endpoint.
  • Enter the public endpoint URL and select the events it should receive.
  • Choose Create. Zoneless displays the signing secret once; copy it directly into the server secret manager or local environment.
Create with the API
import { Zoneless } from '@zoneless/node';
const zoneless = new Zoneless('sk_live_z_YOUR_API_KEY', 'https://api.zoneless.com');

const webhookEndpoint = await zoneless.webhookEndpoints.create({
  url: 'https://yoursite.com/webhook',
  enabled_events: [
    'checkout.session.completed',
    'invoice.paid',
    'customer.subscription.updated',
  ],
});

// Store webhookEndpoint.secret securely; it is returned only on creation.

Create a handler

Set up an endpoint that accepts POST requests with a JSON payload. Your handler should:

  • Verify the webhook signature using the Zoneless-Signature header.
  • Handle the event based on its type field.
  • Record event.id, apply the current resource state idempotently, and return a 2xx response.
Webhook handler
import express from 'express';
import { Zoneless } from '@zoneless/node';

const app = express();
const zoneless = new Zoneless(
  process.env.ZONELESS_API_KEY,
  process.env.ZONELESS_API_URL
);
const endpointSecret = process.env.ZONELESS_WEBHOOK_SECRET;

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['zoneless-signature'];
  let event;

  try {
    event = zoneless.webhooks.constructEvent(req.body, sig, endpointSecret);
  } catch (err) {
    console.log('Webhook signature verification failed.', err.message);
    return res.sendStatus(400);
  }

  switch (event.type) {
    case 'checkout.session.completed':
      const session = event.data.object;
      // Resolve your billing subject from session.client_reference_id.
      // Store session.subscription and grant the paid entitlement.
      break;
    case 'invoice.paid':
      const invoice = event.data.object;
      const subscriptionId =
        invoice.parent?.subscription_details?.subscription;
      // Extend the entitlement using invoice.period_start and period_end.
      break;
    case 'customer.subscription.updated':
      const subscription = event.data.object;
      const itemPeriods = subscription.items.data.map((item) => ({
        item: item.id,
        start: item.current_period_start,
        end: item.current_period_end,
      }));
      // Apply status, cancellation, and item period changes.
      break;
    case 'account.updated':
      const account = event.data.object;
      if (account.payouts_enabled) {
        // The connected account has completed onboarding
      }
      break;
    case 'payout.paid':
      const payout = event.data.object;
      // The payout was sent to the wallet
      break;
    case 'transfer.created':
      const transfer = event.data.object;
      // A transfer was created to a connected account
      break;
    default:
      console.log('Unhandled event type', event.type);
  }

  res.json({ received: true });
});

app.listen(4242, () => console.log('Running on port 4242'));

Verify signatures

Zoneless signs every webhook event by including a signature in the Zoneless-Signature header. Always verify this signature to confirm the event was sent by your Zoneless instance and not a third party.

The header contains a timestamp (t) and a signature (v1). The signature is an HMAC-SHA256 hash of {timestamp}.{payload}, using the endpoint's signing secret as the key.

Verify webhook signature
import express from 'express';
import { Zoneless } from '@zoneless/node';

const app = express();
const zoneless = new Zoneless(
  process.env.ZONELESS_API_KEY,
  process.env.ZONELESS_API_URL
);

const endpointSecret = process.env.ZONELESS_WEBHOOK_SECRET;

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['zoneless-signature'];
  let event;

  try {
    event = zoneless.webhooks.constructEvent(req.body, sig, endpointSecret);
  } catch (err) {
    res.status(400).send('Webhook Error: ' + err.message);
    return;
  }

  // Handle the event
  switch (event.type) {
    case 'account.updated':
      const account = event.data.object;
      console.log('Account updated:', account.id);
      break;
    default:
      console.log('Unhandled event type', event.type);
  }

  res.json({ received: true });
});

Event types

When registering a webhook endpoint, specify which event types to subscribe to. Use ["*"] to receive all events. See Types of events for the full list of available event types.

Delivery and recovery

Return a 2xx response after the event is safely recorded. Use event.id as the idempotency key so the same event can be processed safely whenever it is delivered or replayed.

Use the Events API to inspect event history and retrieve an event when recovering or reconciling application state.

Best practices

  • Handle duplicate events. Record event.id and make applying the event's current resource state idempotent.
  • Only subscribe to events you need. Listening for all events puts unnecessary load on your server.
  • Process events asynchronously. Push events onto a queue and process them in the background to handle traffic spikes.
  • Use HTTPS in production. Your webhook endpoint must be publicly accessible over HTTPS in live mode.
  • Verify every request. Always verify the Zoneless-Signature header to confirm events originate from your Zoneless instance.