Skip to main content
Enrik can tell your system when something happened, so it does not have to poll. Each event is a small signed POST to an HTTPS endpoint you control. It carries identifiers and an outcome, never record data: your system reads the details through the public API with its own key, so the key’s scopes still decide what it sees. Webhook events are in beta. The event types are companyDiscoveryJob.terminal and the record changes below: company.*, contact.* and companySignal.created. This page is about webhook events. An export delivered to the same destination is a separate system: it carries X-CRM-Signature instead, and a failed one is retried up to 3 attempts within about 30 seconds with the same t. See Security & data handling.
Webhook events are being rolled out and are not sent yet. A workspace owner or admin subscribes a receiver to event types in the Enrik app (Settings → Webhooks), or with an API key that holds webhooks:write; see Manage subscriptions with the API.

The event

  • id identifies the event. It is the same on every attempt and every redelivery: deduplicate by it.
  • type and version name the payload. A breaking change is a new version that a subscription opts in to; within a version, fields and values may be added, so ignore what you do not know.
  • occurredAt is when the job reached its outcome.
  • data.status is the job’s own outcome: completed, completed_with_errors, failed or canceled.
To read the job, call GET /v1/company-discovery-jobs/{jobId} with a key that holds discovery:read. The read shows the job as it is now, which may be later than the event (for example archived), or be refused if your key cannot see it.

Record changes

A subscription can also name the record changes. Each is sent once per change, whoever made it: a member in the app, an API key, a CSV import, an automation such as enrichment, or the in-app assistant.
  • changedFields names the fields that changed, never their values: the public field names, and customFields.<key> for a custom field. An update that changes nothing sends nothing.
  • changeSource is who made the change: app, api, import, automation, assistant or system; values may be added. When it is api, apiKeyId names the key, so your integration can ignore the echo of its own writes and avoid a sync loop.
  • Every read needs crm:read. A deleted record reads as not found; an updated that arrives after a deleted reads that way too, which is the current state.

The request

The destination’s own static headers and bearer token are sent too. They cannot replace any header above.

Verify the signature

The signature is HMAC-SHA256 under the destination’s signing secret, over the timestamp, a dot and the raw body exactly as received:
  1. Split X-Enrik-Signature on , and read t and v1.
  2. Compute v1 over the raw request body, before any JSON parsing.
  3. Compare in constant time, and reject a t more than 300 seconds from your clock.
t is fresh on every attempt, so a retry hours later still verifies; the body bytes never change. Exports to the same destination keep their own X-CRM-Signature header, with the same scheme but the same t on every retry.

Delivery

  • Answer 2xx within 10 seconds. Anything else is a failed attempt.
  • At least once. A crash after your endpoint answered can lead to the same event again.
  • Unordered. Events can arrive in any order; the API read is the source of truth.
  • Retries. 408, 429, 5xx, timeouts and network errors are retried after about 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours (±10 %), up to 8 attempts. A longer Retry-After is honoured up to the next step. Every other 4xx, a redirect, or an address the egress refuses ends the delivery at once.
  • Bounded. A delivery is attempted for at most 72 hours. It ends earlier when the subscription is disabled or archived, its destination is archived or deleted, or the workspace is deleted, each checked before every attempt. A request already sent cannot be recalled.
  • Auto-disable. After 10 consecutive failed deliveries the subscription is disabled. An owner or admin re-enables it in the app; deliveries canceled meanwhile are not sent again on their own.
  • Redelivery. An owner or admin can resend one delivery while its event is less than 27 days old. It keeps the same event id and body, with a fresh retry budget. Events are kept for 30 days.

Manage subscriptions with the API

Everything Settings → Webhooks does, a key can do too, in beta. Two scopes cover it:
  • webhooks:read lists subscriptions, the event types they can name and their delivery log.
  • webhooks:write creates, edits, tests, pauses, resumes and archives subscriptions, rotates their signing secret, and redelivers an event. Only a workspace owner or admin can grant it, because only they manage subscriptions in the app.
Subscribe a receiver URL to event types. Send an Idempotency-Key: the answer carries the signing secret once, and a retry with the same key and body answers the same response, secret included, for 24 hours.
Store signingSecret from the answer and verify every request with it, as above. The other operations: The lists are pages: pass pageInfo.endCursor as cursor for the next one. A subscription disabled after 10 failed deliveries is resumed with webhookSubscriptions.enable. A subscription made with a url can move to a new one with webhookSubscriptions.update. One that sends to an export destination with a bearer token keeps its URL there: a new url is 422 validation_error, because the stored token would follow it to the new receiver. Change that URL together with its token in the app, under Exports → Destinations.

Test a subscription

POST /v1/webhook-subscriptions/{id}/test (or Send test in Settings → Webhooks) sends one webhook.ping to the receiver right away, paused or not, and answers what happened:
The ping has the headers above and is signed like any event, so it also checks your signature code:
It is one attempt with no retry, and nothing is stored: it never appears in the delivery log, and a subscription never receives webhook.ping otherwise. Acknowledge it with any 2xx.

Rotate the signing secret

POST /v1/webhook-subscriptions/{id}/rotate-secret (or Rotate secret in the app) replaces the signing secret and answers the new one once; the old one stops verifying at once, so update your receiver straight away. Send an Idempotency-Key: a retry with the same key answers the same secret for 24 hours, until the secret is rotated again.