Webhooks
Receive real-time notifications when events occur in your Rebased account.
How Webhooks Work
When something happens in Rebased (e.g., an invoice is created), we send an HTTP POST request to your specified URL with details about the event. Your server can then process this data automatically.
Creating a Webhook
- Go to Settings → Integrations → API Keys in your Rebased account
- Open the Webhooks tab (alongside API Keys and Usage & Limits)
- Click Create Webhook
- Enter a name and optional description
- Enter your destination URL (must be HTTPS in production)
- Select the events you want to receive
- Click Create Webhook
Your signing secret will be displayed once. Copy it immediately — you’ll need it to verify webhook signatures.
To change an existing endpoint’s URL, events, or name, click the pencil (Edit) icon on the webhook card. You can also add multiple webhook endpoints for the same business.
Only Business Admins (including the business owner) can create and edit webhooks. Team members without admin access cannot manage integrations.
Available Events
| Event | Description |
|---|---|
invoice.created | Triggered when an invoice is created |
invoice.sent | Triggered when an invoice is sent or marked as sent |
invoice.updated | Triggered when an invoice is updated (header, line items, or contacts — not payments) |
invoice.paid | Triggered when an invoice becomes fully paid |
bill.created | Triggered when a bill is created |
bill.approved | Triggered when a bill is approved for payment |
contact.created | Triggered when a client or supplier is created (via UI or API) |
payment.received | Triggered when a payment is allocated to an invoice (deposit, partial, or full) |
quote.created | Triggered when a quote is created |
quote.sent | Triggered when a quote is sent to a recipient |
quote.viewed | Triggered when a quote share link is viewed by a recipient |
quote.accepted | Triggered when a quote is accepted by a recipient or accepted on behalf by your team |
quote.unaccepted | Triggered when quote acceptance is withdrawn (e.g. undo acceptance or edit via new version) |
Payment events (Stripe, bank, and manual)
Payment webhooks fire when a payment is allocated against an invoice in Rebased. The payment method does not change which events you receive — Stripe Checkout, bank reconciliation, and manually recorded payments all use the same events.
| Situation | payment.received | invoice.paid |
|---|---|---|
| Deposit or partial payment | Yes | No |
| Final payment that clears the balance | Yes | Yes |
| Single payment that pays the invoice in full | Yes | Yes |
Recommended subscriptions
- Use
payment.receivedto react to any money applied to an invoice (including deposits and part payments). - Use
invoice.paidwhen you only care that the invoice is fully settled. - Do not use
invoice.updatedto detect payments — that event is for invoice content changes only.
payment.received includes payment_method (for example stripe or bank_transfer), plus amount, invoice_id, and optional invoice_number / reference for matching back to your system.
Webhook delivery format
Each delivery is an HTTP POST to your destination URL with a JSON body and Rebased-specific headers. Webhooks are not authenticated with an API key — use the per-endpoint signing secret (see Verifying signatures) to confirm requests came from Rebased.
Request headers
| Header | Description |
|---|---|
Content-Type | Always application/json |
X-Rebased-Event | Event type (e.g. invoice.created, quote.viewed) |
X-Rebased-Delivery | Unique delivery ID — use for deduplication |
X-Rebased-Timestamp | Unix timestamp (seconds) used when signing |
X-Rebased-Signature | HMAC-SHA256 signature, format sha256={hex} |
User-Agent | Rebased-Webhooks/1.0 |
The event type is sent in X-Rebased-Event, not inside the JSON body.
Request body
The POST body is a flat JSON object with event-specific fields. Rebased does not wrap payloads in an outer envelope (no top-level data, event_type, or event_id in the body).
Always verify signatures against the raw request body bytes exactly as received. Do not re-serialize parsed JSON — key order and spacing must match what Rebased sent.
Event payloads
| Event | Body fields |
|---|---|
invoice.created | invoice_id, invoice_number, client_id, reference, total, status, created_at |
invoice.sent | invoice_id, invoice_number, client_id, sent_at |
invoice.updated | invoice_id, invoice_number, client_id, reference, total, status, updated_at |
invoice.paid | invoice_id, invoice_number, client_id, reference, amount_paid, paid_at (fully paid only) |
bill.created | bill_id, bill_number, supplier_id, total, status, created_at |
bill.approved | bill_id, bill_number, supplier_id, approved_at |
contact.created | contact_id, contact_type (client or supplier), name, email, created_at |
payment.received | payment_id, invoice_id, amount, payment_method (stripe, bank_transfer, etc.), received_at, optional invoice_number, reference |
quote.created | Quote fields below + created_at |
quote.sent | Quote fields below + sent_at |
quote.viewed | Quote fields below + viewed_at, view_count, last_viewed_at, viewer |
quote.accepted | Quote fields below + recipients, optional accepting_contact, accepted_by, accepted_at, acceptance_comments, acceptance_order_number, interactive_selections, and when auto-conversion succeeds: invoice_id, invoice_number, invoice_status, invoice_admin_url, optional invoice_share_url. line_items and totals reflect accepted selections only (non-selected multiple-choice options and excluded optional items are omitted — same rules as quote→invoice conversion) |
quote.unaccepted | Quote fields below + previous_status, unaccepted_at, previous_acceptance |
All quote webhook events also include:
| Field | Description |
|---|---|
client_email | Primary client email from the client record |
client_address | { address_line1, address_line2, city, state, postcode, country } |
quote_public_url | Customer share link (/quotes/shared/{token}) — what the recipient sees |
quote_admin_url | Staff link (/quotes/{quote_id}) — opens the quote in Rebased when logged in |
quote_public_url is omitted if no share token exists yet. The public page requires a snapshot (created when the quote is sent). See docs/Quotes/PUBLIC_QUOTE_SHARING_SECURITY.md.
Monetary amounts are JSON strings (e.g. "1320.00").
Test deliveries
When you click Test in Settings → Integrations → Webhooks, Rebased sends:
- Header:
X-Rebased-Event: test - Body:
{
"event": "test",
"timestamp": "2026-06-22T12:00:00.000000",
"message": "This is a test webhook from Rebased",
"business_id": "your-business-uuid"
}Quote example
Quote webhooks include header details and line items at the top level of the JSON body:
{
"quote_id": "quo_abc123",
"quote_number": "QUO-1001",
"title": "Website Build Quote",
"client_id": "client_xyz789",
"client_name": "A3D Industries",
"client_email": "client@example.com",
"client_address": {
"address_line1": "123 Example St",
"address_line2": null,
"city": "Sydney",
"state": "NSW",
"postcode": "2000",
"country": "Australia"
},
"quote_public_url": "https://platform.rebased.app/quotes/shared/22222222-2222-2222-2222-222222222222",
"quote_admin_url": "https://platform.rebased.app/quotes/quo_abc123",
"status": "accepted",
"issue_date": "2026-06-21",
"valid_until": "2026-07-21",
"currency": "AUD",
"subtotal": "1200.00",
"tax_amount": "120.00",
"total": "1320.00",
"line_items": [
{
"id": "line_123",
"item_code": "CONSULT",
"title": "Consulting",
"description": "Discovery and implementation planning",
"quantity": 2,
"unit_price": "600.00",
"subtotal": "1200.00",
"tax_rate": 10,
"tax_amount": "120.00",
"total": "1320.00",
"optional": false
}
],
"accepted_by": "Aaron Smith",
"accepted_at": "2026-06-21T03:00:00Z",
"acceptance_comments": "Looks good",
"acceptance_order_number": "PO-1234",
"recipients": [
{
"quote_contact_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"contact_id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
"contact_name": "Aaron Smith",
"contact_email": "aaron@aussie3dfab.com.au",
"contact_type": "to"
}
],
"accepting_contact": {
"contact_id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
"contact_name": "Aaron Smith",
"contact_email": "aaron@aussie3dfab.com.au",
"contact_type": "to"
},
"invoice_id": "550e8400-e29b-41d4-a716-446655440000",
"invoice_number": "INV-1001",
"invoice_status": "awaiting_payment",
"invoice_admin_url": "https://platform.rebased.app/invoices/550e8400-e29b-41d4-a716-446655440000",
"invoice_share_url": "https://platform.rebased.app/invoices/shared/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
}When a quote is accepted, Rebased auto-creates an invoice based on your Quotes → Settings → On Acceptance Rules preference:
- Create draft invoice —
invoice_statusisdraft - Send approved invoice —
invoice_statusisawaiting_payment;invoice_share_urlis included when a public payment link exists
recipients lists every To/CC contact on the quote. accepting_contact identifies the recipient who accepted via a share link (same fields as quote.viewed’s viewer). accepted_by is the name recorded at acceptance and may differ if the signer typed a different name. accepting_contact is omitted when a staff member accepts on behalf of the client.
For interactive quotes, quote.accepted line_items only include what the client accepted:
- Multiple-choice groups — only the selected option (non-selected siblings are omitted)
- Optional items — omitted when the client excluded them
- Editable quantities — use the quantity chosen at acceptance
interactive_selections is still included for integrators that need the raw selection map. Pre-acceptance events (quote.created, quote.sent, quote.viewed) continue to include the full quote catalogue of options.
Invoice fields are omitted if auto-conversion fails. Download the PDF with GET /invoices/{id}/pdf using your API key (invoices:read scope).
For quote.viewed, viewer is an object such as:
{
"contact_id": "uuid-or-null",
"contact_name": "Aaron Smith",
"contact_email": "aaron@example.com",
"contact_type": "client"
}Verifying signatures
Every webhook request includes X-Rebased-Signature. Always verify signatures before processing webhooks.
Signing secret
When you create or rotate a webhook, Rebased shows a 64-character hexadecimal signing secret once. Store it securely and use it as a UTF-8 string (do not hex-decode it into binary).
After Rotate Secret, update your server immediately — the previous secret is invalidated.
Signature algorithm
Rebased computes:
HMAC-SHA256(signing_secret, "{timestamp}.{raw_body}")Where:
timestampis the value ofX-Rebased-Timestamp(Unix seconds, as a string)raw_bodyis the exact HTTP request body (bytes / string as received)- The header value is
sha256={hex_digest}— strip thesha256=prefix before comparing
Optionally reject requests whose timestamp is too old (e.g. more than 5 minutes) to limit replay attacks.
Verification examples
import hmac
import hashlib
import time
def verify_rebased_webhook(raw_body: bytes, headers: dict, secret: str, max_age_seconds: int = 300) -> bool:
signature_header = headers.get("X-Rebased-Signature", "")
timestamp = headers.get("X-Rebased-Timestamp", "")
if not signature_header.startswith("sha256=") or not timestamp:
return False
received_sig = signature_header.removeprefix("sha256=")
try:
ts = int(timestamp)
except ValueError:
return False
if abs(time.time() - ts) > max_age_seconds:
return False
signed_payload = f"{timestamp}.".encode() + raw_body
expected = hmac.new(secret.encode("utf-8"), signed_payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received_sig)const crypto = require('crypto');
function verifyRebasedWebhook(rawBody, headers, secret, maxAgeSeconds = 300) {
const signatureHeader = headers['x-rebased-signature'] || '';
const timestamp = headers['x-rebased-timestamp'] || '';
if (!signatureHeader.startsWith('sha256=') || !timestamp) {
return false;
}
const receivedSig = signatureHeader.slice('sha256='.length);
const ts = Number.parseInt(timestamp, 10);
if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > maxAgeSeconds) {
return false;
}
const signedPayload = Buffer.concat([
Buffer.from(`${timestamp}.`, 'utf8'),
Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody, 'utf8'),
]);
const expected = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(receivedSig));
}function verify_rebased_webhook(string $rawBody, array $headers, string $secret, int $maxAgeSeconds = 300): bool
{
$signatureHeader = $headers['X-Rebased-Signature'] ?? $headers['x-rebased-signature'] ?? '';
$timestamp = $headers['X-Rebased-Timestamp'] ?? $headers['x-rebased-timestamp'] ?? '';
if (!str_starts_with($signatureHeader, 'sha256=') || $timestamp === '') {
return false;
}
$receivedSig = substr($signatureHeader, strlen('sha256='));
$ts = (int) $timestamp;
if ($ts <= 0 || abs(time() - $ts) > $maxAgeSeconds) {
return false;
}
$signedPayload = $timestamp . '.' . $rawBody;
$expected = hash_hmac('sha256', $signedPayload, $secret);
return hash_equals($expected, $receivedSig);
}Managing Webhooks
Testing a Webhook
Click the Test button on any webhook to send a test event. This helps verify your endpoint is receiving and processing webhooks correctly.
Pausing a Webhook
If you need to temporarily stop receiving events:
- Find the webhook in Settings → Integrations → Webhooks
- Click the Pause button
Paused webhooks won’t receive events until resumed.
Rotating the Signing Secret
If your signing secret is compromised:
- Find the webhook in Settings → Integrations → Webhooks
- Click Rotate Secret
- Copy the new secret immediately
- Update your server with the new secret
The old secret is immediately invalidated.
Deleting a Webhook
- Find the webhook in Settings → Integrations → Webhooks
- Click the Delete button
- Confirm the action
Deleted webhooks cannot be restored.
Retry Policy
If your endpoint returns an error (non-2xx status code) or times out, we’ll retry the delivery:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry | 24 hours |
After 5 failed attempts, the delivery is marked as failed. You can manually retry failed deliveries from the webhook details page.
Best Practices
- Respond quickly — Return a 2xx response within 30 seconds
- Process asynchronously — Queue webhook data for background processing
- Verify signatures — Always verify the
X-Rebased-Signatureheader - Handle duplicates — Use the
X-Rebased-Deliveryheader to deduplicate - Use HTTPS — Production endpoints must use HTTPS
Who Can Manage Webhooks
Webhooks can be created and managed by:
- Business Admins — Full access to all integrations
- Advisors — Can manage integrations for client businesses they have access to
Team members with other roles cannot create or view webhooks.