curl --request POST \
--url https://api.enrik.ai/v1/webhook-subscriptions \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: <idempotency-key>' \
--data '
{
"url": "https://hooks.example.test/enrik",
"description": "Order sync",
"eventTypes": [
"companyDiscoveryJob.terminal"
],
"eventVersion": 1
}
'import requests
url = "https://api.enrik.ai/v1/webhook-subscriptions"
payload = {
"url": "https://hooks.example.test/enrik",
"description": "Order sync",
"eventTypes": ["companyDiscoveryJob.terminal"],
"eventVersion": 1
}
headers = {
"Idempotency-Key": "<idempotency-key>",
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
'Idempotency-Key': '<idempotency-key>',
Authorization: 'Bearer <token>',
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: 'https://hooks.example.test/enrik',
description: 'Order sync',
eventTypes: ['companyDiscoveryJob.terminal'],
eventVersion: 1
})
};
fetch('https://api.enrik.ai/v1/webhook-subscriptions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.enrik.ai/v1/webhook-subscriptions",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'url' => 'https://hooks.example.test/enrik',
'description' => 'Order sync',
'eventTypes' => [
'companyDiscoveryJob.terminal'
],
'eventVersion' => 1
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json",
"Idempotency-Key: <idempotency-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.enrik.ai/v1/webhook-subscriptions"
payload := strings.NewReader("{\n \"url\": \"https://hooks.example.test/enrik\",\n \"description\": \"Order sync\",\n \"eventTypes\": [\n \"companyDiscoveryJob.terminal\"\n ],\n \"eventVersion\": 1\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Idempotency-Key", "<idempotency-key>")
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.enrik.ai/v1/webhook-subscriptions")
.header("Idempotency-Key", "<idempotency-key>")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"url\": \"https://hooks.example.test/enrik\",\n \"description\": \"Order sync\",\n \"eventTypes\": [\n \"companyDiscoveryJob.terminal\"\n ],\n \"eventVersion\": 1\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.enrik.ai/v1/webhook-subscriptions")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Idempotency-Key"] = '<idempotency-key>'
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"url\": \"https://hooks.example.test/enrik\",\n \"description\": \"Order sync\",\n \"eventTypes\": [\n \"companyDiscoveryJob.terminal\"\n ],\n \"eventVersion\": 1\n}"
response = http.request(request)
puts response.read_body{
"id": "0199a1b2-0000-7000-8000-000000000538",
"destinationId": "0199a1b2-0000-7000-8000-000000000001",
"url": "https://hooks.example.test/enrik",
"description": "Order sync",
"eventTypes": [
"companyDiscoveryJob.terminal"
],
"eventVersion": 1,
"status": "active",
"disabledReason": null,
"consecutiveFailures": 0,
"lastSuccessAt": null,
"lastFailureAt": null,
"createdBy": null,
"createdAt": "2026-10-07T08:00:00.000Z",
"updatedAt": "2026-10-07T08:00:00.000Z",
"signingSecret": "synthetic-secret"
}{
"error": {
"code": "malformed_json",
"message": "Bad Request",
"requestId": "5b1d3c1e-8d6f-4f2a-9a57-3a9b1f2c7d10"
}
}{
"error": {
"code": "unauthenticated",
"message": "Unauthorized",
"requestId": "5b1d3c1e-8d6f-4f2a-9a57-3a9b1f2c7d10"
}
}{
"error": {
"code": "insufficient_scope",
"message": "Forbidden",
"requestId": "5b1d3c1e-8d6f-4f2a-9a57-3a9b1f2c7d10"
}
}{
"error": {
"code": "conflict",
"message": "Conflict",
"requestId": "5b1d3c1e-8d6f-4f2a-9a57-3a9b1f2c7d10"
}
}{
"error": {
"code": "payload_too_large",
"message": "Payload Too Large",
"requestId": "5b1d3c1e-8d6f-4f2a-9a57-3a9b1f2c7d10"
}
}{
"error": {
"code": "unsupported_media_type",
"message": "Unsupported Media Type",
"requestId": "5b1d3c1e-8d6f-4f2a-9a57-3a9b1f2c7d10"
}
}{
"error": {
"code": "validation_error",
"message": "Unprocessable Entity",
"requestId": "5b1d3c1e-8d6f-4f2a-9a57-3a9b1f2c7d10"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests",
"requestId": "5b1d3c1e-8d6f-4f2a-9a57-3a9b1f2c7d10"
}
}{
"error": {
"code": "internal_error",
"message": "Internal Server Error",
"requestId": "5b1d3c1e-8d6f-4f2a-9a57-3a9b1f2c7d10"
}
}{
"error": {
"code": "service_unavailable",
"message": "Service Unavailable",
"requestId": "5b1d3c1e-8d6f-4f2a-9a57-3a9b1f2c7d10"
}
}{
"error": {
"code": "not_found",
"message": "Not found",
"requestId": "5b1d3c1e-8d6f-4f2a-9a57-3a9b1f2c7d10"
}
}Create an event subscription
Subscribes a receiver to event types. Owners and admins only (webhook_subscriptions:manage, which webhooks:write carries); the creator must also be allowed to read what each event type names. Name either a receiver url or an existing destination. A url creates a dedicated destination with its own signing secret in the same transaction (under the webhooks plan entitlement, as a destination create), and the answer carries that secret once. An existing destination must be active and have a signing secret, and has at most one live subscription (409 otherwise); its answer carries a null secret. Validated before commit. The required Idempotency-Key recovers the answer, with its secret, for 24 hours; recovery answers 409 idempotency_receipt_superseded once the secret was rotated since, and the same key with another body is 409 idempotency_key_reused.
curl --request POST \
--url https://api.enrik.ai/v1/webhook-subscriptions \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: <idempotency-key>' \
--data '
{
"url": "https://hooks.example.test/enrik",
"description": "Order sync",
"eventTypes": [
"companyDiscoveryJob.terminal"
],
"eventVersion": 1
}
'import requests
url = "https://api.enrik.ai/v1/webhook-subscriptions"
payload = {
"url": "https://hooks.example.test/enrik",
"description": "Order sync",
"eventTypes": ["companyDiscoveryJob.terminal"],
"eventVersion": 1
}
headers = {
"Idempotency-Key": "<idempotency-key>",
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
'Idempotency-Key': '<idempotency-key>',
Authorization: 'Bearer <token>',
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: 'https://hooks.example.test/enrik',
description: 'Order sync',
eventTypes: ['companyDiscoveryJob.terminal'],
eventVersion: 1
})
};
fetch('https://api.enrik.ai/v1/webhook-subscriptions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.enrik.ai/v1/webhook-subscriptions",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'url' => 'https://hooks.example.test/enrik',
'description' => 'Order sync',
'eventTypes' => [
'companyDiscoveryJob.terminal'
],
'eventVersion' => 1
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json",
"Idempotency-Key: <idempotency-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.enrik.ai/v1/webhook-subscriptions"
payload := strings.NewReader("{\n \"url\": \"https://hooks.example.test/enrik\",\n \"description\": \"Order sync\",\n \"eventTypes\": [\n \"companyDiscoveryJob.terminal\"\n ],\n \"eventVersion\": 1\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Idempotency-Key", "<idempotency-key>")
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.enrik.ai/v1/webhook-subscriptions")
.header("Idempotency-Key", "<idempotency-key>")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"url\": \"https://hooks.example.test/enrik\",\n \"description\": \"Order sync\",\n \"eventTypes\": [\n \"companyDiscoveryJob.terminal\"\n ],\n \"eventVersion\": 1\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.enrik.ai/v1/webhook-subscriptions")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Idempotency-Key"] = '<idempotency-key>'
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"url\": \"https://hooks.example.test/enrik\",\n \"description\": \"Order sync\",\n \"eventTypes\": [\n \"companyDiscoveryJob.terminal\"\n ],\n \"eventVersion\": 1\n}"
response = http.request(request)
puts response.read_body{
"id": "0199a1b2-0000-7000-8000-000000000538",
"destinationId": "0199a1b2-0000-7000-8000-000000000001",
"url": "https://hooks.example.test/enrik",
"description": "Order sync",
"eventTypes": [
"companyDiscoveryJob.terminal"
],
"eventVersion": 1,
"status": "active",
"disabledReason": null,
"consecutiveFailures": 0,
"lastSuccessAt": null,
"lastFailureAt": null,
"createdBy": null,
"createdAt": "2026-10-07T08:00:00.000Z",
"updatedAt": "2026-10-07T08:00:00.000Z",
"signingSecret": "synthetic-secret"
}{
"error": {
"code": "malformed_json",
"message": "Bad Request",
"requestId": "5b1d3c1e-8d6f-4f2a-9a57-3a9b1f2c7d10"
}
}{
"error": {
"code": "unauthenticated",
"message": "Unauthorized",
"requestId": "5b1d3c1e-8d6f-4f2a-9a57-3a9b1f2c7d10"
}
}{
"error": {
"code": "insufficient_scope",
"message": "Forbidden",
"requestId": "5b1d3c1e-8d6f-4f2a-9a57-3a9b1f2c7d10"
}
}{
"error": {
"code": "conflict",
"message": "Conflict",
"requestId": "5b1d3c1e-8d6f-4f2a-9a57-3a9b1f2c7d10"
}
}{
"error": {
"code": "payload_too_large",
"message": "Payload Too Large",
"requestId": "5b1d3c1e-8d6f-4f2a-9a57-3a9b1f2c7d10"
}
}{
"error": {
"code": "unsupported_media_type",
"message": "Unsupported Media Type",
"requestId": "5b1d3c1e-8d6f-4f2a-9a57-3a9b1f2c7d10"
}
}{
"error": {
"code": "validation_error",
"message": "Unprocessable Entity",
"requestId": "5b1d3c1e-8d6f-4f2a-9a57-3a9b1f2c7d10"
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests",
"requestId": "5b1d3c1e-8d6f-4f2a-9a57-3a9b1f2c7d10"
}
}{
"error": {
"code": "internal_error",
"message": "Internal Server Error",
"requestId": "5b1d3c1e-8d6f-4f2a-9a57-3a9b1f2c7d10"
}
}{
"error": {
"code": "service_unavailable",
"message": "Service Unavailable",
"requestId": "5b1d3c1e-8d6f-4f2a-9a57-3a9b1f2c7d10"
}
}{
"error": {
"code": "not_found",
"message": "Not found",
"requestId": "5b1d3c1e-8d6f-4f2a-9a57-3a9b1f2c7d10"
}
}Authorizations
An Enrik API key, sent as Authorization: Bearer <key>. A key reaches public operations only, and only those whose scopes it holds. A few parameters need a scope beyond their operation’s, which the parameter names. The scopes:
setup:read: Read the workspace setup and its company profile.setup:run: Change the setup: confirm the company profile, authorize sponsored drafting and background company research, choose what to set up and complete setup.workspace:read: Read the workspace’s own profile: its health summary and setup state.crm:read: Read companies with their signals, contacts and the saved views over them.crm:write: Create, update and restore companies and contacts, and create, update and delete saved views.crm:delete: Delete companies and contacts.twin:read: Read the Company Twin: structured company knowledge and its signals.discovery:read: Read ICP profiles, ICP assessments, discovery briefs, discovery jobs, candidates and the signal library.discovery:write: Create and change ICP profiles, discovery briefs and signal-definition drafts, and review discovery candidates.discovery:run: Create, start and cancel discovery jobs, and edit a draft one. Uses provider budget.enrichment:read: Read the enrichment configuration and its results, including a record’s enrichment, formula and custom-API cells.enrichment:run: Launch and cancel enrichment. Spends enrichment credits.automation:read: Read workflows and agents with their run history, including a record’s agent cells.automation:run: Run existing workflows and agents.exports:read: Read export jobs and the fields an export can carry.exports:run: Create, preview, start, cancel and archive CSV exports of companies, contacts and candidates, and download the file. Also needscrm:read. Delivering an export to a webhook destination stays in the app.integrations:read: Read connections, field mappings, CRM sync jobs and webhook destinations.webhooks:read: Read webhook subscriptions, the event types they can name and their delivery log.webhooks:write: Subscribe a receiver URL to webhook events; edit, test, pause, resume and archive a subscription and rotate its signing secret; redeliver an event. Only workspace owners and admins can grant it.reporting:read: Read analytics and dashboards.assistant:read: Read the Assistant: its threads, messages and pending approvals.assistant:run: Converse with the Assistant. Uses the workspace’s AI quota.assistant:write: Approve or reject the changes the Assistant proposes.
Headers
A key you choose for this request, such as a UUID. Retrying with the same key returns the first result instead of doing the work twice.
1 - 200Body
A new subscription: a receiver url (with an optional description) or an existing destinationId, exactly one, and the event types to send.
The event types to send, each once. The creator must be allowed to read what each one names.
1 - 10 elementscompanyDiscoveryJob.terminal, company.created, company.updated, company.deleted, company.restored, contact.created, contact.updated, contact.deleted, contact.restored, companySignal.created An existing webhook destination to subscribe, created in the app. Send it or url, not both.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$The https:// URL to send events to. Enrik creates its destination with a new signing secret, answered once. Send it or destinationId, not both.
A name for the receiver, at most 120 characters; only with url. Defaults to its host.
1 - 120The payload version to receive; 1, the only one so far.
Response
A subscription and, after a create with a url or a rotation, its signing secret, delivered once.
A subscription and, after a create with a url or a rotation, its signing secret, delivered once.
The subscription id.
The destination that receives the events and holds the signing secret.
The receiver URL, read from the subscription’s destination.
The destination’s name: the description given at create, or else the receiver’s host.
The event types the receiver gets; webhookEventTypes.list lists them all.
companyDiscoveryJob.terminal, company.created, company.updated, company.deleted, company.restored, contact.created, contact.updated, contact.deleted, contact.restored, companySignal.created The payload version the receiver gets.
active delivers; disabled is paused (see disabledReason); archived is final.
active, disabled, archived Why it is paused: manual, or consecutive_failures after 10 failed deliveries in a row; null unless disabled.
manual, consecutive_failures Failed deliveries since the last success; enabling resets it.
When a delivery last succeeded, if ever.
^((\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\d|3[01])|(0[469]|11)-(0[1-9]|[12]\d|30)|(02)-(0[1-9]|1\d|2[0-8])))T([01]\d|2[0-3]):[0-5]\d(:[0-5]\d(\.\d+)?)?Z$When a delivery last failed, if ever.
^((\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\d|3[01])|(0[469]|11)-(0[1-9]|[12]\d|30)|(02)-(0[1-9]|1\d|2[0-8])))T([01]\d|2[0-3]):[0-5]\d(:[0-5]\d(\.\d+)?)?Z$The user who created it; null when an API key or the system did.
When it was created.
^((\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\d|3[01])|(0[469]|11)-(0[1-9]|[12]\d|30)|(02)-(0[1-9]|1\d|2[0-8])))T([01]\d|2[0-3]):[0-5]\d(:[0-5]\d(\.\d+)?)?Z$When it last changed.
^((\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\d|3[01])|(0[469]|11)-(0[1-9]|[12]\d|30)|(02)-(0[1-9]|1\d|2[0-8])))T([01]\d|2[0-3]):[0-5]\d(:[0-5]\d(\.\d+)?)?Z$The signing secret, shown once: verify X-Enrik-Signature with it. A create with a url and a rotation answer it; null when the create named an existing destination.
Was this page helpful?