Webhooks

Send a signed JSON request to your own URL when an invoice is paid, a service changes or a ticket is opened, and verify it on your side.

A webhook is a URL on your side that Tidemark Billing calls when something happens. Use it to update your own systems, post to chat or start a job. Each request is signed, so your code can check it really came from your billing installation.

Add an endpoint#

Only administrators can add, change or delete webhooks.

  1. Open API & webhooks in the admin menu and choose the Webhooks tab.
  2. Under Add a webhook, enter the Endpoint URL (it must start with http:// or https://, for example https://example.com/hooks/billing).
  3. Optionally add a Description.
  4. Tick the Events you want. With none ticked, the endpoint receives all events.
  5. Press Add webhook.
  6. Copy the signing secret from the top of the page. It is shown once and cannot be recovered.

To change the URL, the events or pause an endpoint, open it with Manage. Untick Send deliveries to pause it without deleting it. The secret cannot be changed: delete the webhook and add it again to get a new one.

You can also manage endpoints through the API (/admin/webhooks, ability webhooks.read or webhooks.write). See REST API.

What a delivery looks like#

Each delivery is a POST with a JSON body:

{
  "id": "6f1c2a52-7d5e-4f3e-9d0b-2a0f6c2b8e11",
  "event": "invoice.paid",
  "created_at": "2026-11-09T10:00:00+00:00",
  "data": { "id": 34, "number": "INV-00034", "status": "paid" }
}

id is unique per delivery. data has the same shape as the matching API resource, such as an invoice, and always shows the staff view. Ticket events never include the message thread, and internal notes never trigger a webhook.

These headers come with it:

Header Value
X-Billing-Event The event name, for example invoice.paid
X-Billing-Delivery The delivery id
X-Billing-Timestamp Unix time in seconds when it was sent
X-Billing-Signature sha256= followed by the hex signature
User-Agent Tidemark-Billing-Webhook/1.0

Verify the signature#

The signature is an HMAC-SHA256 over the timestamp, a dot and the raw request body, using the signing secret as the key. Compute it from the raw body, before any JSON parsing, and compare in constant time. Reject requests whose timestamp is old, for example more than five minutes, to stop replays.

$raw = file_get_contents('php://input');
$expected = 'sha256='.hash_hmac('sha256', $_SERVER['HTTP_X_BILLING_TIMESTAMP'].'.'.$raw, $secret);
if (! hash_equals($expected, $_SERVER['HTTP_X_BILLING_SIGNATURE'] ?? '')) {
    http_response_code(400);
    exit;
}

The steps in any language: read the raw body, build timestamp + "." + body, compute HMAC-SHA256 with your secret, hex-encode it, prefix sha256=, and compare with the header.

Retries and the delivery log#

Answer with any 2xx status within 10 seconds. If your endpoint answers with another status, or does not answer, the delivery is retried after 1 minute, then 10 minutes, then 1 hour. After the retries are used up the delivery stays marked as failed.

Open an endpoint with Manage to see Recent deliveries: the latest 50, with event, result, attempts, time and the first part of your response. Send test sends a ping event with data.message, delivered immediately, so you can check your URL and signature code. The endpoint list also shows the last delivery result per endpoint.

Deliveries are sent by the queue. On the standard setup the scheduler runs the queue every minute, so make sure the cron job is in place. Send test does not use the queue and shows the result straight away.

Events#

Event When
client.created A client account is created
invoice.created, invoice.paid, invoice.voided An invoice is issued, paid in full or voided
payment.received, payment.refunded A payment is recorded or refunded
quote.sent, quote.accepted, quote.declined Quote lifecycle
service.created, service.activated, service.suspended, service.unsuspended, service.cancelled, service.terminated Service lifecycle
service.provisioning_failed A module call failed
service.trial_started, service.trial_ending, service.trial_converted Free trials
service.plan_changed, service.plan_change_scheduled Plan changes
usage.recorded Metered usage was recorded for a service
ticket.created, ticket.replied, ticket.status_changed, ticket.assigned Support tickets

Plugins can add more events. GET /admin/events returns the list your installation has right now.

Was this article helpful?
Last updated 10 Nov 2026
Powered by Tidemark Billing