Send events out with webhooks.
Have Revvly post to your own address when a CRM lead changes or a Support request moves, with a signature you can check and a log you can retry from. This is the outbound direction. To send data into Revvly instead, see the invoice payments and event ingestion API pages.
01Outbound is the other direction
A webhook here means Revvly sends an HTTP POST to a web address you control, each time something happens. Both kinds below are signed, retried and logged, and both are set up by workspace admins and owners only.
02Add a CRM lead webhook
Choose Add webhook and fill in a Name, the URL, which events to Send when, and optionally which Pipelines. The URL must be https, must point at a public address, and must not contain a username or password. Revvly does not follow redirects, so give the final address. The events are Lead created (a lead was added to a pipeline), Lead updated (any field, a stage or pipeline move, a lost reason or a conversion; it carries the full current lead) and Lead deleted (carries a deletion snapshot with id, title, pipelineId, stageId, organizationId and deleted set to true). Leave Pipelines empty to receive leads from every pipeline, including ones you add later. When you save, the signing secret is shown once in Save your signing secret. Copy it before you close the dialog. Leads exist only in workspaces that have the CRM turned on.
- Org Settings
- Webhooks
- Add webhook
Admins and owners only. A workspace can have up to 25 webhooks.
03Add a billing webhook
The same Add webhook dialog offers billing events, in every workspace. Invoice issued (a subscription start, renewal, mid-cycle change or an invoice raised by hand), Invoice paid, Invoice overdue and Invoice payment reversed carry the invoice: id, invoiceNumber, subscriptionId, amount, subtotal and tax as amount and currency, taxBreakdown, paymentStatus, invoiceDate, dueDate and paidDate. Subscription created, Subscription trial will end (three days before a free trial ends), Subscription trial converted (with firstInvoiceId), Subscription cancelled and Subscription ended carry the subscription. Payment succeeded (a card charge went through and is booked on the invoice; Invoice paid follows when it settles it), Payment failed (with willRetry, nextAttemptAt, declineCode and message) and Payment action required (the customer must act, for example for 3-D Secure) are sent once per card charge, so each attempt on one invoice is its own event; they carry chargeId, invoiceId, paymentIntentId and amount in the currency’s major unit, and nothing about the card itself. Invoice issued is never sent for a draft, a refund or credit note, or imported history. All of them carry client with the client id and your externalId, and both use the same field names as the public GraphQL API. There is no PDF link in the payload because it would expire before a retried delivery arrived; read pdfUrl from the API when you need it. Billing events are not tied to a pipeline, so leave Pipelines empty on a webhook that takes them.
- Org Settings
- Webhooks
- Add webhook
04Send leads to a Google Sheet through Zapier
In Zapier, create a Zap that starts with Webhooks by Zapier and choose Catch Hook. Paste the address it gives you into the URL field in Revvly. Then add the Google Sheets action Create or Update Spreadsheet Row and match rows on the lead id, which is data.id in the body. Subscribe to Lead created, Lead updated and Lead deleted: updates then change the row, a delete lets you remove it, and a lead that moves out of a pipeline you scoped the webhook to sends one last update for the pipeline it left. Zapier does not check the signature, so you can ignore it there. Send test event from the row menu posts a sample and shows whether your address answered.
- Org Settings
- Webhooks
- row menu
- Send test event
05What a lead payload looks like
Every delivery is a JSON POST with an envelope: id is the event id and stays the same across retries, type is the event, occurredAt is the time, and data is the lead. The lead is flat and carries names as well as ids, so a spreadsheet column can use the stage, owner and company name directly. Fields with no value are null. A lead.deleted event carries only id, title, pipelineId, stageId, organizationId and deleted set to true. A test event has type webhook.test.
POST https://hooks.example.com/revvly
Content-Type: application/json
User-Agent: Revvly-Webhooks/1
X-Revvly-Event: lead.updated
X-Revvly-Delivery: 6ac49cd2866904b09b303383
X-Revvly-Timestamp: 1791270098
X-Revvly-Signature: v1=5f2c...e1
{
"id": "6ac49cd2866904b09b30aa01",
"type": "lead.updated",
"occurredAt": "2026-10-06T07:01:38.486Z",
"data": {
"id": "6ac49cd2866904b09b30337c",
"title": "Acme annual plan",
"status": "OPEN",
"pipelineId": "...", "pipelineName": "Sales",
"stageId": "...", "stageName": "Proposal", "stageKind": "...",
"stageEnteredAt": "2026-10-05T09:12:00.000Z",
"source": "...",
"ownerId": "...", "ownerName": "Dana Lee", "ownerEmail": "[email protected]",
"organizationId": "...", "organizationName": "Acme",
"estimatedValue": 12000, "expectedArr": 12000, "currency": "EUR",
"expectedCloseDate": null, "closeProbability": null, "billingPeriod": null,
"lostReason": null, "convertedAt": null, "externalRef": null,
"createdAt": "2026-09-30T10:00:00.000Z",
"updatedAt": "2026-10-06T07:01:38.000Z"
}
}06Bring a new destination up to date
A webhook only hears about changes from the moment it exists. To fill an empty sheet with the leads you already have, open the row menu and choose Send existing leads. Revvly queues the existing leads in your webhook’s pipelines, replayed as lead.updated when the webhook takes that event and as lead.created otherwise, and they arrive over the next few minutes at a steady pace rather than all at once. One run covers up to 2,000 leads; the dashboard repeats until done, and tells you if there are more so you can run it again. A lead that is already waiting to be sent to that webhook is not queued twice, so a double click is safe. The option is unavailable while the webhook is off.
- Org Settings
- Webhooks
- row menu
- Send existing leads
07Delivery, retries and limits for lead webhooks
A delivery succeeds on any 2xx answer. Delivery is at least once, so dedupe on the envelope id. A failure is retried up to seven attempts in all, after 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours and 12 hours, about 20 hours in total, and a Retry-After from your server is honoured up to six hours. A refusal that retrying cannot fix, such as most 4xx answers (not 408, 425 or 429) or a redirect, fails at once. If a newer update for the same lead has already been delivered, an older queued one is skipped so stale data does not overwrite fresh. After 25 failed deliveries in a row Revvly switches the webhook off and the Status column shows Switched off with the reason; deliveries queued meanwhile wait up to seven days for you to turn it back on, which also clears the count. The other statuses are Active, Failing and Off. Activity on a row lists recent deliveries with a Retry button for failed ones, and the log is kept for 30 days.
- A lead changesCreated, updated or deleted.
- Signed POST to your URLThe envelope id stays the same across retries.
- Any 2xx answerDelivered.orOther failureRetried, up to seven attempts in all.orCannot be fixed by retryingMost 4xx answers or a redirect fail at once.
- On the eventAttempt 1
- 30 seconds laterAttempt 2
- 2 minutes laterAttempt 3
- 10 minutes laterAttempt 4
- 1 hour laterAttempt 5
- 6 hours laterAttempt 6
- 12 hours laterAttempt 7
- After attempt 7Delivery fails25 failures in a row switch the webhook off.
- Org Settings
- Webhooks
- Activity
Turn off, Turn on, Edit and Delete are in the row menu. Delete also removes its log.
08Verify a lead webhook signature
Each request carries X-Revvly-Timestamp (unix seconds), X-Revvly-Delivery and X-Revvly-Signature, which is v1= followed by the hex HMAC-SHA256 of the timestamp, a full stop and the raw request body, keyed with your signing secret. Check it against the raw bytes, not re-serialised JSON, and reject timestamps older than a few minutes. Rotate signing secret in the row menu replaces the secret and the old one stops working at once, so update your receiver first or at the same moment.
import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody: the exact bytes received, before any JSON parsing.
// secret: the whole signing secret, including the whsec_ prefix.
function verify(rawBody, headers, secret) {
const t = Number(headers['x-revvly-timestamp']);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false; // older than 5 min
const expected = 'v1=' + createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const given = Buffer.from(headers['x-revvly-signature'] ?? '');
const want = Buffer.from(expected);
// timingSafeEqual throws on different lengths, so check that first.
return given.length === want.length && timingSafeEqual(want, given);
}09Support webhooks
Support webhooks send ticket events to your own backend, for instance to keep your product in step with a request. Choose Add endpoint, enter an https address and tick the events. The signing secret is shown once. Changing the URL later keeps the secret. Send test event posts a signed webhook.test. A workspace can have up to five endpoints, and the whole page needs Support.
- Org Settings
- Support
- Webhooks
- Add endpoint
Admins and owners only. Everyone else sees a notice that only admins can see webhooks.
10What a Support delivery contains
Bodies carry ids, not content. Only events the end user could see are sent, so internal notes never produce one, and only for requests that have an end-user requester. data holds ticketId, ticketNumber, messageId (set for ticket.message, otherwise null), by (end_user, agent, ai or system) and the requester’s endUser.externalUserId. ticket.progressed adds data.stage with a key and a label. Fetch the content through your own tools if you need it.
POST https://api.example.com/webhooks/revvly
Content-Type: application/json
Revvly-Event-Id: 6ac49cd2866904b09b303383
Revvly-Signature: t=1791270098,v1=5f2c...e1
{
"id": "6ac49cd2866904b09b303383",
"type": "ticket.message",
"createdAt": "2026-10-06T07:01:38.486Z",
"data": {
"ticketId": "6ac49cd2866904b09b30337c",
"ticketNumber": "AA-42",
"messageId": "6ac49cd2866904b09b30337d",
"by": "agent",
"endUser": { "externalUserId": "user_8412" }
}
}11Retries, replay and verification for Support webhooks
Revvly waits five seconds for an answer, does not follow redirects, and treats any non-2xx as a failure. It retries after 10 seconds, 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours, then marks the delivery Gave up; anything still owed 24 hours after it was queued is skipped as stale. After 10 deliveries in a row give up, the endpoint is turned off with a reason and nothing is queued for it until you turn it on. Deliveries lists each event with its status (Delivered, Waiting, Gave up or Skipped), kept for 30 days, and Replay sends a finished one again with the same event id and a fresh signature. Delivery is at least once and may arrive out of order, so ignore an event id you have already handled. The signature header is Revvly-Signature: t=unix seconds,v1=hex HMAC-SHA256 of t, a full stop and the raw body.
- On the eventFirst tryWaits five seconds for an answer.
- 10 seconds laterRetry 1
- 1 minute laterRetry 2
- 5 minutes laterRetry 3
- 30 minutes laterRetry 4
- 2 hours laterRetry 5
- 6 hours laterRetry 6
- After retry 6Gave upReplay sends it again.
- Org Settings
- Support
- Webhooks
- Deliveries
import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody: the exact bytes received, before any JSON parsing.
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(String(header ?? '').split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false; // older than 5 min
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
const given = Buffer.from(parts.v1 ?? '');
return given.length === expected.length && timingSafeEqual(Buffer.from(expected), given);
}