Zodiac Tech CRM Docs

Outbound webhooks & API keys

Push CRM events to your own systems with signed webhooks, and let external apps read data through scoped API keys.

admin developer super-admin Advanced ⏱ 15 min Updated 2026-10-05

Outbound webhooks & API keys

Two ways to connect the CRM to other systems:

  • Outbound webhooks — the CRM calls your URL whenever something happens (a lead is created, a stage changes…) with a signed JSON payload.
  • API keys — your systems call the CRM's REST API with a scoped key.

Both live in the Advanced group of Settings and are gated by your plan (features webhooks and api).

Who this is for

  • Admin who subscribes to CRM events.
  • Developer integrating an ERP, ad platform, call tool or data warehouse.
  • Super Admin auditing what a workspace has connected.

Difficulty: Advanced · Time: about 15 minutes · Prerequisites:

  • A public HTTPS endpoint on your side that can verify a signature.
  • The plan feature enabled for webhooks / API access.

Part 1 — Outbound webhooks

Step 1 — Create the webhook

Open Settings → Advanced → Outbound Webhooks → New, and fill:

Field Notes
Name Label for your own reference
URL Your HTTPS endpoint. Private/loopback/link-local and cloud-metadata addresses are blocked by the SSRF guard — use a public host
Events Any of: lead.created, lead.updated, lead.deleted, lead.stage_changed, form.submitted, automation.triggered
Secret Auto-filled with a random value — copy it into your receiver to verify signatures
Filters (optional) Narrow events: source, status, pipeline_id, pipeline_stage_id, assigned_user_id, tags. Filters combine with AND across fields and OR within a field; tags matches on intersection
Enabled Off means nothing is delivered

The secret is write-only later The secret is generated for you and stored encrypted; it is never included in API responses or listings. Copy it to your receiver when you create the webhook — you cannot read it back from the page afterwards.

Step 2 — Test and watch deliveries

  • Send test fires a sample delivery to your URL and reports the result.
  • View deliveries opens the delivery log: status (pending / success / failed), response code, response body (first ~4,000 characters), latency and attempt count.

Step 3 — Verify the signature in your receiver

Delivery attempts carry these headers:

Header Content
X-<Brand>-Event The event name, e.g. lead.created
X-<Brand>-Signature t=<unix>,v1=<hex hmac sha256>
X-<Brand>-Timestamp Unix timestamp
X-<Brand>-Webhook The delivery id
User-Agent <Brand>-Webhooks/1.0

<Brand> is the platform brand slug (on the default install: X-LeadHub-Signature). The v1 value is HMAC-SHA256(secret, "{t}.{body}") — Stripe-style. Verify like this (PHP):

$header = $_SERVER['HTTP_X_LEADHUB_SIGNATURE'] ?? '';
$parts = [];
foreach (explode(',', $header) as $kv) {
    [$k, $v] = explode('=', $kv, 2) + [null, null];
    $parts[$k] = $v;
}
$t = (int) ($parts['t'] ?? 0);
$expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
$fresh = abs(time() - $t) <= 300; // 5-minute tolerance
if (!$fresh || !hash_equals($expected, $parts['v1'] ?? '')) {
    http_response_code(403);
    exit;
}

Retries and queueing Deliveries run on a dedicated webhooks queue with up to 5 attempts (30-second timeout); a mid-flight failure reuses the same delivery row so the log stays accurate. A URL blocked by the safety guard is not retried — fix the URL and re-enable.

The payload

{
  "event": "lead.created",
  "tenant_id": 42,
  "timestamp": "2026-10-05T09:30:00Z",
  "data": { "...event-specific fields..." }
}

Respond with any 2xx quickly and process asynchronously. Treat deliveries as at-least-once: keep handlers idempotent, keyed on the payload's entity id plus event.

Part 2 — API keys

Open Settings → Advanced → API Keys → New:

Field Notes
Name What the key is for (e.g. "Warehouse sync")
Scopes Choose only what the integration needs
Rate limit Requests per hour — default 1,000, maximum 100,000
Expires at Optional expiry date; leave blank for a non-expiring key

The key value is shown once at creation; afterwards the table shows only a masked prefix.

Available scopes:

read:leads  write:leads  delete:leads
read:pipelines  write:pipelines
read:tags  write:tags
read:users  write:users  delete:users
read:forms  write:forms  delete:forms
read:automations  write:automations  delete:automations
read:integrations  write:integrations

Use the key as a bearer token against the REST API (/api/v1/...):

curl -H "Authorization: Bearer YOUR_API_KEY" \
  https://your-crm-domain/api/v1/leads

The interactive reference is at /api/docs (OpenAPI viewer), and a health probe is available at /api/v1/health. Full details in Authentication and REST API.

Rate limits are per key Exceeding the hourly limit returns 429 with retry information. Give each integration its own key so one noisy consumer cannot exhaust another's quota — and so you can revoke one without breaking the rest.

Expected result

A test delivery arrives at your endpoint and verifies against the secret; live events flow into your delivery log with success statuses. Your integration can read/write CRM data with its scoped key within its rate limit.

Common mistakes

  • Verifying the signature over the wrong string — it is "{t}.{body}", not just the body, and the timestamp comes from the header, not the payload.
  • Skipping the freshness check — always reject old timestamps to prevent replay.
  • Using the secret from the API — it is not exposed there by design; save it at creation.
  • A URL behind basic auth or on a private network — blocked by the SSRF guard; nothing is delivered and no retry happens.
  • Over-scoped API keys — grant read:leads when read is all you need.
  • Busy handler — acknowledging after slow work causes retries; acknowledge first.

Troubleshooting

Symptom Likely cause Fix
Test delivery fails instantly URL is private/blocked or not HTTPS Use a public HTTPS endpoint
Deliveries show failed with 4xx Your endpoint rejected the payload (or signature check failed) Inspect the stored response body in the delivery log
No deliveries at all Webhook disabled, wrong events chosen, or filters exclude everything Check the enabled toggle, event list and filters
Duplicate deliveries At-least-once delivery after a timeout Make handlers idempotent
API key returns 401 Key revoked, expired, or wrong header format Create a new key / fix the Bearer header
API key returns 429 Hourly limit reached Raise rate_limit_per_hour or slow the client

Technical details

  • Delivery job: DispatchOutboundWebhook on the webhooks queue ($tries = 5, 30s timeout); records live in the delivery log with statuses pending / success / failed (WebhookDelivery::STATUS_*).
  • URL safety: UrlSafetyGuard blocks private, loopback, link-local and metadata IP ranges (SSRF protection). Blocked URLs are not retried.
  • Signature headers are branded via WebhookBranding (tenant → global → config → LeadHub).
  • Secret storage: encrypted cast, $hidden on the model, never serialized into API listings.
  • API keys: ApiKeyResource; scopes listed above; quota ceiling governed by the plan's max_api_keys; keys are hashed at rest and matched by prefix + hash.
  • Queue note: production needs a worker covering the webhooks queue — see Cron & queue workers.

Was this page helpful?