Webhooks
Olyron CRM both receives webhooks from the services you connect (Stripe, Resend, LinkedIn, WhatsApp, Twilio, Shopify) and sends outbound webhooks from automation workflows. Every inbound endpoint verifies a provider signature before it touches your data, and every event is scoped to the right tenant so multi-tenant isolation holds.
#Two directions of webhooks
Webhooks in Olyron CRM work in two directions. Inbound webhooks are HTTP endpoints Olyron exposes so third-party providers can push events to you in real time — a delivered email, a completed Stripe subscription, an incoming WhatsApp message. Outbound webhooks are HTTP calls Olyron makes to a URL you own, fired as a step inside an automation workflow.
- Inbound — providers POST to
/api/webhooks/*and other receiver routes; Olyron verifies a signature, resolves the tenant, and writes the event to the database. - Outbound — an automation workflow's
webhookaction node sends a JSON POST (or GET/PUT/PATCH) to any URL you configure.
https://core.olyron.com/api/webhooks/resend. Register that full URL in the provider's dashboard.#Inbound endpoints reference
Each integration has its own receiver route with its own signing scheme and secret. These are the endpoints currently implemented in the codebase:
| Endpoint | Provider | Verification header | Secret env var |
|---|---|---|---|
| /api/webhooks/stripe | Stripe billing | stripe-signature | STRIPE_WEBHOOK_SECRET |
| /api/webhooks/resend | Resend email events | svix-id / svix-timestamp / svix-signature | RESEND_WEBHOOK_SECRET |
| /api/webhooks/linkedin | LinkedIn partner events | x-li-signature | LINKEDIN_WEBHOOK_SECRET |
| /api/whatsapp/webhooks/cloud-api | WhatsApp Cloud API (Meta) | x-hub-signature-256 | WHATSAPP_APP_SECRET |
| /api/communications/webhooks | Twilio / RingCentral / GoTo | X-Twilio-Signature | TWILIO_AUTH_TOKEN |
| /api/commerce/webhook/shopify | Shopify | x-shopify-hmac-sha256 | per-connection webhookSecret |
| /api/commerce/webhook/woocommerce | WooCommerce | x-wc-webhook-signature | per-connection webhookSecret |
| /api/payments/webhook?provider= | Stripe / Authorize.Net / Square / Clover | provider-specific | provider-specific |
GET /api/webhooks/stripe with { "status": "ok" }, and the LinkedIn endpoint answers GET /api/webhooks/linkedin with { "ok": true, "status": "ready" }. Use these to confirm the route is live before wiring up the provider.#How signatures are verified
Every inbound endpoint reads the raw request body (not parsed JSON) so it can recompute the provider's signature exactly. Requests that fail verification are rejected with a 401 or 403 before any database write.
Per-provider schemes
- Stripe — the
stripe-signatureheader is validated againstSTRIPE_WEBHOOK_SECRET. A missing signature returns 400; a failed processing result returns 400. IfSTRIPE_WEBHOOK_SECRETis not set, the handler refuses withWebhook secret not configured. - Resend — validated with the Svix library using the
svix-id,svix-timestamp, andsvix-signatureheaders againstRESEND_WEBHOOK_SECRET. An invalid signature returns 401. - LinkedIn — the
x-li-signature: t=<unix>,v1=<hex>header is HMAC-SHA256 verified over<t>.<rawBody>usingLINKEDIN_WEBHOOK_SECRET, with a 300-second timestamp-skew tolerance and a constant-time compare. A missing secret returns 503; a bad signature returns 401. - WhatsApp — the
x-hub-signature-256header is HMAC-SHA256 verified againstWHATSAPP_APP_SECRET. If that secret is unset the check is skipped (a dev-only fallback that must not be relied on in production). - Twilio — the
X-Twilio-Signatureheader is recomputed as an HMAC-SHA1 base64 digest over the request URL plus sorted form parameters, keyed byTWILIO_AUTH_TOKEN.
// Header shape: x-li-signature: t=<unix>,v1=<hex hmac sha256>
const expected = createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
// constant-time compare against the v1 value, plus a
// timestamp-skew check (default tolerance: 300 seconds)
if (!timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) {
return { ok: false, reason: "signature mismatch" };
}WHATSAPP_APP_SECRET as a pass so local testing works, and the Shopify/WooCommerce handlers only verify when a per-connection webhookSecret is stored. In production, configure the secret for every integration so unsigned requests cannot inject events.#Verification handshakes
Some providers require a one-time challenge/echo handshake when you register the endpoint. Olyron handles these on the GET method of the receiver route.
- 1WhatsApp (Meta) challengeMeta calls
GET /api/whatsapp/webhooks/cloud-api?hub.mode=subscribe&hub.verify_token=...&hub.challenge=.... Olyron compareshub.verify_tokentoWHATSAPP_WEBHOOK_VERIFY_TOKENand echoeshub.challengeback on match, otherwise returns 403. Set the same token string in both places. - 2LinkedIn challengeA
GET /api/webhooks/linkedin?challenge=<value>request echoes thechallengevalue back astext/plain, satisfying the provider handshake. - 3Confirm deliveryAfter the handshake succeeds, send a test event from the provider dashboard and check that the corresponding record appears (an email event row, a WhatsApp message, a LinkedIn activity).
#What inbound events become
Verified events are normalized and written to tenant-scoped tables. For example, the Resend receiver looks up the original send by provider_message_id in email_messages, then routes by event type:
| Resend event type | Written to | Notes |
|---|---|---|
| email.opened | email_opens | Records an open with source metadata |
| email.clicked | email_clicks | Stores the clicked target_url |
| email.replied | email_replies | Stores the subject as a snippet |
| email.delivered / bounced / complained | (no-op) | Currently acknowledged but not stored |
LinkedIn events are mapped to strict activity kinds (for example INVITATION_SENT becomes linkedin_connection_sent), resolved to a tenant via the linkedin_accounts table, and inserted into linkedin_events in batches of up to 500 rows. Events whose account cannot be matched are counted as unmatched and dropped rather than stored — LinkedIn fans out events for shared resources you may not own.
{ ignored: true } (Resend) or an unmatched count (LinkedIn) when an event references a message or account Olyron does not have. This keeps providers from retrying events that will never apply to your tenant.#Sending outbound webhooks from workflows
To notify an external system when something happens in your CRM, add a webhook action node to an automation workflow. When the workflow runs, Olyron makes an HTTP request to the URL you configured with a JSON body describing the record.
- 1Add a webhook action nodeIn the workflow builder, add an action node and choose the webhook type. Configure the URL, the HTTP method (GET, POST, PUT, or PATCH), and optional custom headers as a JSON object (for example an Authorization bearer token).
- 2Understand the payloadFor non-GET methods, Olyron sends
Content-Type: application/jsonplus your custom headers, and a body containing the tenant id, record id, module, and the workflow's variable bag. - 3Read the resultThe node reports success based on the HTTP response being 2xx and records a message like
Webhook POST https://... → 200. A non-2xx status or a network error marks the step failed.
tenant_idstring- The tenant the record belongs to.
record_idstring- The id of the record that triggered the workflow.
modulestring- The CRM module the record lives in (for example leads or contacts).
variablesobject- The workflow variable bag assembled during the run.
{
"tenant_id": "b2c9...e41",
"record_id": "d18f...a03",
"module": "leads",
"variables": {
"lead_name": "Jane Advisor",
"stage": "qualified"
}
}fetch with no automatic retry or backoff. If your receiver is down, the step fails and the workflow moves on — build idempotency and any retry logic on your side, and return quickly with a 2xx to signal success.#Troubleshooting and limits
| Symptom | Likely cause | Fix |
|---|---|---|
| 401 invalid signature | Wrong or missing signing secret | Confirm the provider's secret matches the env var (STRIPE_WEBHOOK_SECRET, RESEND_WEBHOOK_SECRET, LINKEDIN_WEBHOOK_SECRET) |
| 503 not configured (LinkedIn) | LINKEDIN_WEBHOOK_SECRET is unset | Set the secret env var and redeploy |
| 403 on WhatsApp GET | hub.verify_token mismatch | Make WHATSAPP_WEBHOOK_VERIFY_TOKEN identical in Olyron and Meta |
| Event ignored / unmatched | No matching message or account row | Ensure the sending message or connected account exists in your tenant |
| 400 no signature (Stripe) | Missing stripe-signature header | Point Stripe at the exact endpoint URL so it signs the request |