Gareth Wilson Gareth Wilson

Guide to Rebilly Webhooks: Features and Best Practices

Published


Rebilly webhooks keep your systems in step with your billing. A transaction is approved or declined, a subscription renews or is cancelled, an invoice is issued or paid, a customer opens a dispute, or a KYC document is reviewed, and Rebilly notifies your endpoint with the details. They drive fulfilment, access control, dunning, accounting sync and fraud workflows.

This guide covers how Rebilly webhooks are configured, what they send, how to authenticate them with basic credentials, where Rebilly's two signatures fit, how retries work, and the practices that keep a Rebilly receiver reliable.

What are Rebilly webhooks?

A Rebilly webhook sends a request to your URL when one of the events in its filter happens. You choose the HTTP method, the URL (which can include placeholders), extra headers, and either Rebilly's default payload for the event or a body you write yourself with Liquid templates. Leave the events filter empty and the webhook receives every event.

Rebilly gives you three ways to authenticate a webhook:

  • HTTP basic access authentication, which Rebilly highly recommends over HTTPS. You create webhook service credentials, and Rebilly sends Authorization: Basic ... on every request.
  • A Security-Signature header on every webhook: a JWT you verify against Rebilly's published JSON Web Keys, which includes a SHA-256 hash of the body.
  • Standard Webhooks headers on every webhook, with a v1a Ed25519 signature that you verify against a fixed public key.

Hookdeck's Rebilly source checks the first of these, basic authentication. It doesn't check either signature, but your own service can.

Rebilly webhook features

FeatureDetails
ConfigurationAutomations > Development > Webhooks > Create webhook, or the API
Limit25 active webhooks per organisation
Event selectionAn events filter (empty means all events), plus optional conditions
HTTP methodGET, POST, PUT, PATCH or DELETE, chosen per webhook
BodyThe default event payload, or a custom body with Liquid placeholders
Content typeapplication/json; charset=utf-8
User agentRebilly/<release>, a 7-character release ID that changes frequently
Basic authenticationOptional, sent with every request when configured
SignaturesSecurity-Signature (RS256 JWT) and webhook-signature (v1a Ed25519) on every webhook
Delivery IDwebhook-id, the same on every retry
SuccessHTTP 200 within 20 seconds
RetriesAfter about 5 minutes, then at about 1, 2, 4, 8 and 16 hours. Up to 6 retries
Delivery guaranteesWebhooks may be delayed or delivered more than once
Source IPs52.203.182.244, 52.203.227.248, 52.5.114.150, 54.208.113.57, 52.4.4.116, 52.2.91.201, 34.194.210.166, 34.237.3.102

Common events

Rebilly's published event list has 138 types. Some of the ones integrations use most:

AreaEvents
Transactionstransaction-processed, transaction-declined, transaction-refunded, transaction-voided, transaction-reconciled
Subscriptionssubscription-created, subscription-activated, subscription-renewed, subscription-canceled, subscription-trial-ended, subscription-paused
Invoicesinvoice-issued, invoice-paid, invoice-past-due, invoice-voided, renewal-invoice-payment-declined
Customerscustomer-created, customer-updated, customer-merged
Disputesdispute-created, dispute-modified
Payment cardspayment-card-created, payment-card-expiration-reminder, payment-card-expired
KYCkyc-document-accepted, kyc-document-rejected, kyc-request-fulfilled
Data exportsdata-export-completed, data-export-failed

Your organisation may also have additional event types, so the published list is a starting point.

Payload

The default payload carries the event in eventType, the IDs of the resources involved, and HAL-style _embedded and _links sections with the full resources. Trimmed from Rebilly's data-export-completed example:

{
  "dataExportId": "d9dd134f-0bee-46b1-bec0-992da3173336",
  "fileId": "d348968b-41ed-4d7d-b534-02e5265c8912",
  "eventType": "data-export-completed",
  "_embedded": {
    "dataExport": {
      "id": "d9dd134f-0bee-46b1-bec0-992da3173336",
      "name": "Preview",
      "resource": "customers",
      "format": "csv",
      "status": "pending"
    }
  }
}

A custom body replaces all of this. If you write one, you decide which fields exist, including whether eventType is there.

Rebilly also notes that the data reflects its records at the time of the event. If a webhook is delayed, your own records may already be newer than what it carries.

Inspect Rebilly webhooks as they arrive. Use a Hookdeck Inspect URL as the webhook URL and press Test webhook in Rebilly to see the method, headers, signatures and body it sends, with no account or setup required.

Setting up Rebilly webhooks

Choose a username and a long random password first, for example with openssl rand -hex 32 for the password.

  1. In Rebilly, press Automations in the left navigation bar.
  2. In the Development section, press Webhooks, then Create webhook.
  3. Enter an ID and turn the webhook on in Status.
  4. In Events, select Send specific event types and choose your events in Events filter.
  5. Optionally, clear No conditions and add filters.
  6. In Endpoint, choose the HTTP method and enter your HTTPS URL.
  7. Press Authorization and set HTTP basic access authentication with your username and password.
  8. Leave Use default event payload selected unless you need a custom body.
  9. Press Test webhook, then Save webhook.

Through the API, create webhook service credentials for your webhook host and attach them to the webhook.

With Hookdeck. Rebilly is a supported source in Hookdeck. Its setup note reads: "Set HTTP basic access authentication on the Rebilly webhook. Its JWT and Ed25519 signatures are not checked." Create a source with the Rebilly type, enter the same Username and Password, and use the source URL as the webhook URL. Rebilly sends the Authorization header with every request, which matters because Hookdeck never answers with a 401 challenge.

The source pre-selects POST but doesn't manage the method, because a Rebilly webhook can use GET, PUT, PATCH or DELETE too. If yours does, allow that method on the source as well, or requests with it won't be accepted. A request with missing or wrong credentials still gets a 200 from Hookdeck, but Hookdeck marks it as failed verification and doesn't deliver it to your service. A webhook without basic authentication has nothing for the source to check.

For local development, hookdeck listen 3000 rebilly --path /webhooks/rebilly gives you a public URL that forwards to your machine.

Securing Rebilly webhooks

Start with the basic credentials: decode the header, split on the first colon, and compare the username and password in constant time.

const crypto = require('crypto');
const express = require('express');

const USERNAME = process.env.REBILLY_WEBHOOK_USERNAME;
const PASSWORD = process.env.REBILLY_WEBHOOK_PASSWORD;

function safeEqual(a, b) {
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

function verifyBasicAuth(header) {
  if (!USERNAME || !PASSWORD || typeof header !== 'string') return false;
  const match = /^basic\s+([A-Za-z0-9+/]+={0,2})\s*$/i.exec(header);
  if (!match) return false;
  const decoded = Buffer.from(match[1], 'base64');
  const colon = decoded.indexOf(0x3a); // split on the first colon
  if (colon === -1) return false;
  const userOk = safeEqual(decoded.subarray(0, colon), Buffer.from(USERNAME, 'utf8'));
  const passOk = safeEqual(decoded.subarray(colon + 1), Buffer.from(PASSWORD, 'utf8'));
  return userOk && passOk;
}

const app = express();

app.post('/webhooks/rebilly', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyBasicAuth(req.get('authorization'))) {
    return res.sendStatus(401);
  }

  let event;
  try {
    event = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  // Deduplicate on req.get('webhook-id'), then route on event.eventType.
  res.sendStatus(200);
});

The same check in Python:

import base64
import binascii
import hmac
import os

USERNAME = os.environ.get("REBILLY_WEBHOOK_USERNAME", "")
PASSWORD = os.environ.get("REBILLY_WEBHOOK_PASSWORD", "")


def verify_basic_auth(header) -> bool:
    if not USERNAME or not PASSWORD or not header:
        return False
    scheme, _, encoded = header.partition(" ")
    if scheme.lower() != "basic":
        return False
    try:
        decoded = base64.b64decode(encoded.strip(), validate=True)
    except (binascii.Error, ValueError):
        return False
    user, sep, password = decoded.partition(b":")
    if not sep:
        return False
    user_ok = hmac.compare_digest(user, USERNAME.encode("utf-8"))
    pass_ok = hmac.compare_digest(password, PASSWORD.encode("utf-8"))
    return user_ok and pass_ok

If you build test fixtures from Rebilly's documentation, note that its example header Basic cmViaWxseTpzdXBlcm1hbgo= decodes to rebilly:superman followed by a newline. Encode your own credentials rather than copying it.

Basic credentials are static. They authenticate the sender but not the body, and with no timestamp a captured request can be replayed. Rebilly's signatures close both gaps, and because they're on every webhook, your service can check one even when Hookdeck sits in front: Hookdeck forwards the original headers and body.

Rebilly recommends the Standard Webhooks check for new integrations, because it uses a fixed public key. The signed content is webhook-id, a period, webhook-timestamp, a period and the raw body. Adapted from Rebilly's own example:

const { createPublicKey, verify } = require('crypto');

// Live: whpk_lr2xkGO1EEdyZxVx4DKQ5XfmSjxOmwv9UcwNPbn9fFI=
// Sandbox: whpk_qism49rHt4dOh6txzL3dfnMTmhQiITY1A86Y9BJGEZM=
const PUBLIC_KEY = process.env.REBILLY_WEBHOOK_PUBLIC_KEY;
const raw = Buffer.from(PUBLIC_KEY.replace(/^whpk_/, ''), 'base64');
const key = createPublicKey({
  key: { kty: 'OKP', crv: 'Ed25519', x: raw.toString('base64url') },
  format: 'jwk',
});

// headers: lower-case names. rawBody: the Buffer from express.raw().
function verifyRebillySignature(headers, rawBody, { maxAgeSeconds } = {}) {
  const id = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  const signatures = headers['webhook-signature'];
  if (!id || !/^\d+$/.test(timestamp || '') || !signatures) return false;

  // Optional: Rebilly suggests rejecting timestamps more than 5 minutes off.
  if (maxAgeSeconds && Math.abs(Date.now() / 1000 - Number(timestamp)) > maxAgeSeconds) {
    return false;
  }

  const signed = Buffer.concat([Buffer.from(`${id}.${timestamp}.`), rawBody]);
  return signatures
    .split(' ')
    .filter((value) => value.startsWith('v1a,'))
    .some((value) => verify(null, signed, key, Buffer.from(value.slice(4), 'base64')));
}

The Standard Webhooks reference libraries only verify symmetric v1 signatures, so they won't verify Rebilly's v1a. Pass { maxAgeSeconds: 300 } if you want the timestamp check; leave it out if your service receives requests replayed from a queue, because their timestamps will be old.

Make Rebilly webhooks production-ready. Hookdeck Event Gateway checks the basic credentials, answers Rebilly with the 200 it expects, and keeps every event so a failing service doesn't burn through Rebilly's six retries.

Rebilly webhook limitations and pain points

Only 200 counts

The Problem: A receiver that answers 201, 202 or 204 sees the same event again and again.

Why It Happens: Rebilly's docs say that if the response code isn't 200, the request is considered not sent and is marked for retry.

Workarounds:

  • Return exactly 200, with any body, once you've stored the event.

How Hookdeck Can Help: Hookdeck answers Rebilly with 200 as soon as it stores the request, whatever your service returns.

Six retries, then nothing

The Problem: An outage that outlasts Rebilly's retry schedule loses events.

Why It Happens: After the first failure, Rebilly retries after about 5 minutes and then at about 1, 2, 4, 8 and 16 hours, and then stops.

Workarounds:

  • Alert on failed deliveries so outages are noticed early.
  • Reconcile important resources, such as transactions and invoices, against the API after an outage.

How Hookdeck Can Help: Hookdeck stores the event on the first attempt and delivers to your service with retries you configure. You can replay anything after a fix.

The gateway doesn't check the signatures

The Problem: If you rely on Hookdeck's Rebilly source alone, the body isn't integrity-checked.

Why It Happens: The source compares basic credentials. Rebilly's JWT and Ed25519 signatures pass through unchecked.

Workarounds:

  • Verify the v1a signature in your own service, as shown above.
  • Keep basic authentication on regardless, so unauthenticated traffic is filtered before it reaches you.

How Hookdeck Can Help: Hookdeck forwards Rebilly's original headers and raw body, so your signature check works the same behind Hookdeck as it does directly.

Custom bodies change the contract

The Problem: A teammate edits a webhook's body template, and your parser fails.

Why It Happens: A custom body replaces the default payload entirely, and the template lives in Rebilly, not in your code.

Workarounds:

  • Prefer the default payload, and fetch extra data from the API.
  • If you use a template, version it and include eventType and the resource IDs.

How Hookdeck Can Help: Hookdeck stores each request's body, so you can see what a changed template sends and replay it against your fixed handler.

Best practices

Use webhook-id as the idempotency key

Rebilly sends the same webhook-id on every retry of the same webhook and recommends it as an idempotency key. Store it before acting and skip repeats. See our guide to webhook idempotency.

Filter events in Rebilly

An empty events filter sends everything. Choose the events you handle, and use conditions to narrow further, so you stay well inside the 25-webhook limit.

Re-read state for decisions

Rebilly warns that webhooks can be delayed or duplicated, and that their data reflects the moment of the event. Before an irreversible action, such as shipping an order, fetch the current resource from the API.

Acknowledge fast and process asynchronously

You have 20 seconds. Verify, store, return 200, and do the work afterwards. See why to process webhooks asynchronously.

Conclusion

Rebilly webhooks deliver billing, payment and customer events to a URL and method you choose, with optional basic authentication and two signatures on every request. Turn on basic authentication with a long random password, return exactly 200, deduplicate on webhook-id, and verify the v1a Ed25519 signature in your own service when you need the body checked.

Hookdeck Event Gateway checks the basic credentials, answers Rebilly immediately, and retries and replays delivery to your service, so Rebilly's retry schedule stops being your only safety net.

Get started with Hookdeck for free and handle Rebilly webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.