ClientoDocs
Pro

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

EventTrigger
invoice.paidAn invoice is marked as paid (Stripe, PayPal, or manual)
invoice.payment_recordedA 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_deletedA 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.cancelledAn invoice is cancelled. The payload carries cancelledAt and the amounts of the document
credit_note.createdA 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.validatedA milestone is validated — by the client from the portal, by you, or automatically when the client pays it
quote.acceptedA quote is accepted — by the client from the public link, or when you convert it into a project
project.status_changedA project status changes (payload carries previousStatus and status; nothing is sent if the status is unchanged)
subscription.createdA recurring invoice subscription is created (from the dashboard or via the external API; source tells which)
subscription.pausedA 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.resumedA paused recurring invoice subscription is resumed
subscription.cancelledA recurring invoice subscription is deleted (final) — including when its client is deleted
subscription.payment_failedA 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

  1. Go to Profile → Outbound webhooks.
  2. Click Add.
  3. Enter the destination URL (must be https://).
  4. Select one or more events to subscribe to.
  5. 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:

AttemptDelay after the previous attempt
1st retryat least 1 minute
2nd retryat least 5 minutes
3rd retryat 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, and subscription.payment_failed to 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.paid event 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-Signature header 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.