Outbound webhooks
Send real-time event notifications to any external URL.
Note
Outbound webhooks are available on the Pro plan only.
Overview
Outbound webhooks let Cliento notify any external URL when a key event occurs in your account — an invoice is paid, a milestone is validated, or a quote is accepted. This makes it easy to plug Cliento into Zapier, Make, or your own backend without polling.
Supported events
| Event | Trigger |
|---|---|
invoice.paid | An invoice is marked as paid (Stripe, PayPal, or manual) |
invoice.payment_recorded | A payment is recorded on an invoice (a partial payment or the last one). The payload carries paymentId, amount, paidAt, the balance still due, the invoice total, the currency and settled (true when this payment settled the invoice). The internal note is never sent |
invoice.payment_deleted | A recorded payment is deleted. The payload carries paymentId, amount, paidAt (of the deleted payment), the balance now due, the invoice total, the currency and reopened (true when the invoice had been settled by its payments and is open again). It never sends invoice.paid |
invoice.cancelled | An invoice is cancelled. The payload carries cancelledAt and the amounts of the document |
credit_note.created | A credit note is issued on a paid invoice. The payload carries creditNoteId, creditNoteNumber, invoiceId, invoiceNumber, clientId, issueDate, the total (tax included, positive), the currency, the reason (or null) and creditableRemaining on the invoice |
milestone.validated | A milestone is validated — by the client from the portal, by you, or automatically when the client pays it |
quote.accepted | A quote is accepted — by the client from the public link, or when you convert it into a project |
project.status_changed | A project status changes (payload carries previousStatus and status; nothing is sent if the status is unchanged) |
subscription.created | A recurring invoice subscription is created (from the dashboard or via the external API; source tells which) |
subscription.paused | A recurring invoice subscription is paused (reversible — can be resumed). The payload carries a reason: manual when you pause it, plan_downgrade when Cliento pauses it because your plan no longer includes recurring invoices |
subscription.resumed | A paused recurring invoice subscription is resumed |
subscription.cancelled | A recurring invoice subscription is deleted (final) — including when its client is deleted |
subscription.payment_failed | A recurring invoice subscription's latest invoice is still unpaid past its due date, at the time the next one is generated |
Setting up a webhook
- Go to Profile → Outbound webhooks.
- Click Add.
- Enter the destination URL (must be
https://). - Select one or more events to subscribe to.
- Click Create webhook.
Warning
Copy your signing secret immediately after creation — it is shown only once and cannot be recovered. If you lose it, delete the webhook and create a new one.
Payload format
Every request is an HTTP POST with a JSON body:
{
"event": "invoice.paid",
"createdAt": "2026-06-12T10:00:00.000Z",
"data": {
"invoiceId": "clx...",
"invoiceNumber": "INV-0042",
"clientId": "clx...",
"paidVia": "stripe",
"paidAt": "2026-06-12T10:00:00.000Z",
"currency": "EUR",
"subtotal": 985,
"discountTotal": 115,
"taxAmount": 197,
"total": 1182
}
}
invoice.paid is sent once, when the invoice is fully settled. Each payment recorded on an invoice sends invoice.payment_recorded first (a payment that settles the invoice is followed by invoice.paid). Deleting a recorded payment sends invoice.payment_deleted, which says whether the invoice was reopened. paidVia can be stripe, paypal_manual, manual or payments (settled by the payments you recorded).
The data object varies by event type but always includes the relevant entity IDs.
invoice.paid, invoice.cancelled and quote.accepted also carry the amounts of the document: currency, subtotal (sum of the lines after line discounts, before tax), discountTotal (sum of the line discounts, 0 when there are none), taxAmount and total (subtotal plus tax). Amounts are in major units (for example 1182 for 1 182.00 EUR). These fields were added without changing the existing ones.
Verifying the signature
Every request includes an X-Cliento-Signature header:
X-Cliento-Signature: sha256=<hex-digest>
The digest is an HMAC-SHA256 of the raw request body signed with your webhook secret. Always verify it before processing:
import { createHmac } from "node:crypto";
function verifySignature(secret: string, rawBody: string, header: string): boolean {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
return expected === header;
}
import hmac, hashlib
def verify(secret: str, raw_body: bytes, header: str) -> bool:
digest = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(digest, header)
Tip
Always use a constant-time comparison (like hmac.compare_digest) to prevent timing attacks.
Retries
If your endpoint returns a non-2xx status or times out (10 s), Cliento retries the delivery automatically:
| Attempt | Delay after the previous attempt |
|---|---|
| 1st retry | at least 1 minute |
| 2nd retry | at least 5 minutes |
| 3rd retry | at least 15 minutes |
Retries are processed every 15 minutes, so a retry is sent at the first run after its delay has elapsed (at most 15 minutes later).
After 4 total attempts without success, the delivery is marked as failed and no further retries are made.
Tip
Make your endpoint idempotent — use the combination of event + data.invoiceId (or the relevant ID) as a deduplication key in case the same event is delivered more than once.
Managing webhooks
From Profile → Outbound webhooks you can:
- Toggle a webhook on/off without deleting it.
- Delete a webhook to permanently stop deliveries to that URL.
Up to 10 webhooks are allowed per account.
Zapier / Make integration
Point the webhook URL at a Zapier Catch Hook or a Make Custom webhook trigger. Use the event field as a filter to branch on different event types in your automation.
Compatible services
Outbound webhooks work with any service that accepts HTTP POST requests. Here are common integrations:
Notification & messaging
- Slack — via Slack Incoming Webhooks or a Zapier/Make step. Post to a channel on every payment or milestone.
- Discord — same pattern via Discord Incoming Webhooks. Useful if your team lives in Discord.
- Telegram — trigger a bot message via the Telegram Bot API.
Automation platforms
- Zapier — use a "Catch Hook" trigger, then connect to 6 000+ apps (Gmail, Notion, Airtable, HubSpot…).
- Make (ex-Integromat) — "Custom webhook" trigger, more flexible for multi-step data transformation.
- n8n — self-hosted alternative, "Webhook" node as trigger.
CRM & sales
- HubSpot — move a deal to "Won" when a quote is accepted, or log a payment activity.
- Pipedrive — update a deal stage or create an activity via the Pipedrive API.
- Notion — add a row to a database (via Zapier/Make or the Notion API directly).
- WPRadar — a Laravel-based prospecting CRM: react to
subscription.created,subscription.cancelled, andsubscription.payment_failedto keep a prospect's contract status in sync after it converts into a Cliento client (https://wpradar.io/api/webhooks/cliento).
Accounting & finance
- Google Sheets — append a row on each
invoice.paidevent for a lightweight payment ledger. - Airtable — same idea, with richer views and automations on top.
- Pennylane / Qonto — via a custom backend that calls their API after receiving the event.
Custom backends
- Any Express / FastAPI / Laravel endpoint. Verify the
X-Cliento-Signatureheader and process the event however you need. - Supabase Edge Functions or Vercel Functions — serverless handlers that react to events without maintaining a server.
Tip
Zapier and Make are the fastest way to get started — no code required. For more control over data transformation or if you need to fan out to multiple services at once, a lightweight serverless function is worth the extra step.