curl --request POST \
--url https://app.hookie.ai/admin/api/projects/{project_id}/webhooks \
--header 'Content-Type: application/json' \
--cookie hookie_session= \
--data '
{
"name": "<string>",
"slug": "<string>",
"dataset": "<string>",
"enabled": true,
"criteria": [
{
"path": "<string>",
"value": "<unknown>",
"equals": "<unknown>"
}
],
"mappings": [
{
"path": "<string>",
"key": "<string>"
}
],
"ip_allowlist": [
"203.0.113.0/24",
"2001:db8::/32"
],
"verification": {
"scheme": "stripe",
"secret": "whsec_..."
},
"redirect_url": "https://example.com/thanks",
"cors_origins": [
"https://example.com"
],
"handshake_secret": "<string>"
}
'const options = {
method: 'POST',
headers: {cookie: 'hookie_session=', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: '<string>',
slug: '<string>',
dataset: '<string>',
enabled: true,
criteria: [{path: '<string>', value: '<unknown>', equals: '<unknown>'}],
mappings: [{path: '<string>', key: '<string>'}],
ip_allowlist: ['203.0.113.0/24', '2001:db8::/32'],
verification: {scheme: 'stripe', secret: 'whsec_...'},
redirect_url: 'https://example.com/thanks',
cors_origins: ['https://example.com'],
handshake_secret: '<string>'
})
};
fetch('https://app.hookie.ai/admin/api/projects/{project_id}/webhooks', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://app.hookie.ai/admin/api/projects/{project_id}/webhooks"
payload = {
"name": "<string>",
"slug": "<string>",
"dataset": "<string>",
"enabled": True,
"criteria": [
{
"path": "<string>",
"value": "<unknown>",
"equals": "<unknown>"
}
],
"mappings": [
{
"path": "<string>",
"key": "<string>"
}
],
"ip_allowlist": ["203.0.113.0/24", "2001:db8::/32"],
"verification": {
"scheme": "stripe",
"secret": "whsec_..."
},
"redirect_url": "https://example.com/thanks",
"cors_origins": ["https://example.com"],
"handshake_secret": "<string>"
}
headers = {
"cookie": "hookie_session=",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"id": "<string>",
"slug": "orders-v1",
"webhook_slug": "aBcXyz0123456789abcdefgh",
"base_slug": "orders",
"version": "1",
"public_path": "acme-3f9a/orders/aBcXyz0123456789abcdefgh",
"public_url": "https://app.hookie.ai/acme-3f9a/orders/aBcXyz0123456789abcdefgh",
"verification": {
"scheme": "none",
"header": "<string>",
"algorithm": "sha256",
"encoding": "hex",
"prefix": "<string>",
"tolerance_seconds": 123,
"has_secret": true,
"previous_secret_expires_at": "2023-11-07T05:31:56Z"
},
"webhook": {
"id": "<string>",
"base_slug": "orders",
"version": "<string>",
"name": "<string>",
"enabled": 0,
"criteria": "<string>",
"dataset": "<string>",
"mappings": "<string>",
"slug": "aBcXyz0123456789abcdefgh",
"require_signature": 0,
"ip_allowlist": "<string>",
"created_at": "2023-11-07T05:31:56Z",
"webhook_slug": "aBcXyz0123456789abcdefgh",
"public_path": "acme-3f9a/orders/aBcXyz0123456789abcdefgh",
"public_url": "https://app.hookie.ai/acme-3f9a/orders/aBcXyz0123456789abcdefgh",
"previous_public_url": "<string>",
"previous_url_expires_at": "2023-11-07T05:31:56Z"
}
}{
"error": "<string>",
"retry_after": 123,
"upgrade_url": "<string>",
"reason": "<string>",
"code": "agent_scope_insufficient",
"required_scope": "hookie:read",
"granted_scopes": [
"<string>"
],
"manage_url": "<string>",
"scheme": "<string>"
}{
"error": "<string>",
"retry_after": 123,
"upgrade_url": "<string>",
"reason": "<string>",
"code": "agent_scope_insufficient",
"required_scope": "hookie:read",
"granted_scopes": [
"<string>"
],
"manage_url": "<string>",
"scheme": "<string>"
}{
"error": "<string>",
"retry_after": 123,
"upgrade_url": "<string>",
"reason": "<string>",
"code": "agent_scope_insufficient",
"required_scope": "hookie:read",
"granted_scopes": [
"<string>"
],
"manage_url": "<string>",
"scheme": "<string>"
}{
"error": "<string>",
"retry_after": 123,
"upgrade_url": "<string>",
"reason": "<string>",
"code": "agent_scope_insufficient",
"required_scope": "hookie:read",
"granted_scopes": [
"<string>"
],
"manage_url": "<string>",
"scheme": "<string>"
}{
"error": "<string>",
"retry_after": 123,
"upgrade_url": "<string>",
"reason": "<string>",
"code": "agent_scope_insufficient",
"required_scope": "hookie:read",
"granted_scopes": [
"<string>"
],
"manage_url": "<string>",
"scheme": "<string>"
}{
"error": "<string>",
"retry_after": 123,
"upgrade_url": "<string>",
"reason": "<string>",
"code": "agent_scope_insufficient",
"required_scope": "hookie:read",
"granted_scopes": [
"<string>"
],
"manage_url": "<string>",
"scheme": "<string>"
}{
"error": "<string>",
"retry_after": 123,
"upgrade_url": "<string>",
"reason": "<string>",
"code": "agent_scope_insufficient",
"required_scope": "hookie:read",
"granted_scopes": [
"<string>"
],
"manage_url": "<string>",
"scheme": "<string>"
}Create an endpoint
Top-level alias: POST /admin/api/webhooks. Write role required. The cap is checked before validation, so a full project returns 402 even for an invalid body. verification optionally makes the endpoint check its provider’s signature (#210); the create response echoes the scheme, never the secret.
curl --request POST \
--url https://app.hookie.ai/admin/api/projects/{project_id}/webhooks \
--header 'Content-Type: application/json' \
--cookie hookie_session= \
--data '
{
"name": "<string>",
"slug": "<string>",
"dataset": "<string>",
"enabled": true,
"criteria": [
{
"path": "<string>",
"value": "<unknown>",
"equals": "<unknown>"
}
],
"mappings": [
{
"path": "<string>",
"key": "<string>"
}
],
"ip_allowlist": [
"203.0.113.0/24",
"2001:db8::/32"
],
"verification": {
"scheme": "stripe",
"secret": "whsec_..."
},
"redirect_url": "https://example.com/thanks",
"cors_origins": [
"https://example.com"
],
"handshake_secret": "<string>"
}
'const options = {
method: 'POST',
headers: {cookie: 'hookie_session=', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: '<string>',
slug: '<string>',
dataset: '<string>',
enabled: true,
criteria: [{path: '<string>', value: '<unknown>', equals: '<unknown>'}],
mappings: [{path: '<string>', key: '<string>'}],
ip_allowlist: ['203.0.113.0/24', '2001:db8::/32'],
verification: {scheme: 'stripe', secret: 'whsec_...'},
redirect_url: 'https://example.com/thanks',
cors_origins: ['https://example.com'],
handshake_secret: '<string>'
})
};
fetch('https://app.hookie.ai/admin/api/projects/{project_id}/webhooks', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://app.hookie.ai/admin/api/projects/{project_id}/webhooks"
payload = {
"name": "<string>",
"slug": "<string>",
"dataset": "<string>",
"enabled": True,
"criteria": [
{
"path": "<string>",
"value": "<unknown>",
"equals": "<unknown>"
}
],
"mappings": [
{
"path": "<string>",
"key": "<string>"
}
],
"ip_allowlist": ["203.0.113.0/24", "2001:db8::/32"],
"verification": {
"scheme": "stripe",
"secret": "whsec_..."
},
"redirect_url": "https://example.com/thanks",
"cors_origins": ["https://example.com"],
"handshake_secret": "<string>"
}
headers = {
"cookie": "hookie_session=",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"id": "<string>",
"slug": "orders-v1",
"webhook_slug": "aBcXyz0123456789abcdefgh",
"base_slug": "orders",
"version": "1",
"public_path": "acme-3f9a/orders/aBcXyz0123456789abcdefgh",
"public_url": "https://app.hookie.ai/acme-3f9a/orders/aBcXyz0123456789abcdefgh",
"verification": {
"scheme": "none",
"header": "<string>",
"algorithm": "sha256",
"encoding": "hex",
"prefix": "<string>",
"tolerance_seconds": 123,
"has_secret": true,
"previous_secret_expires_at": "2023-11-07T05:31:56Z"
},
"webhook": {
"id": "<string>",
"base_slug": "orders",
"version": "<string>",
"name": "<string>",
"enabled": 0,
"criteria": "<string>",
"dataset": "<string>",
"mappings": "<string>",
"slug": "aBcXyz0123456789abcdefgh",
"require_signature": 0,
"ip_allowlist": "<string>",
"created_at": "2023-11-07T05:31:56Z",
"webhook_slug": "aBcXyz0123456789abcdefgh",
"public_path": "acme-3f9a/orders/aBcXyz0123456789abcdefgh",
"public_url": "https://app.hookie.ai/acme-3f9a/orders/aBcXyz0123456789abcdefgh",
"previous_public_url": "<string>",
"previous_url_expires_at": "2023-11-07T05:31:56Z"
}
}{
"error": "<string>",
"retry_after": 123,
"upgrade_url": "<string>",
"reason": "<string>",
"code": "agent_scope_insufficient",
"required_scope": "hookie:read",
"granted_scopes": [
"<string>"
],
"manage_url": "<string>",
"scheme": "<string>"
}{
"error": "<string>",
"retry_after": 123,
"upgrade_url": "<string>",
"reason": "<string>",
"code": "agent_scope_insufficient",
"required_scope": "hookie:read",
"granted_scopes": [
"<string>"
],
"manage_url": "<string>",
"scheme": "<string>"
}{
"error": "<string>",
"retry_after": 123,
"upgrade_url": "<string>",
"reason": "<string>",
"code": "agent_scope_insufficient",
"required_scope": "hookie:read",
"granted_scopes": [
"<string>"
],
"manage_url": "<string>",
"scheme": "<string>"
}{
"error": "<string>",
"retry_after": 123,
"upgrade_url": "<string>",
"reason": "<string>",
"code": "agent_scope_insufficient",
"required_scope": "hookie:read",
"granted_scopes": [
"<string>"
],
"manage_url": "<string>",
"scheme": "<string>"
}{
"error": "<string>",
"retry_after": 123,
"upgrade_url": "<string>",
"reason": "<string>",
"code": "agent_scope_insufficient",
"required_scope": "hookie:read",
"granted_scopes": [
"<string>"
],
"manage_url": "<string>",
"scheme": "<string>"
}{
"error": "<string>",
"retry_after": 123,
"upgrade_url": "<string>",
"reason": "<string>",
"code": "agent_scope_insufficient",
"required_scope": "hookie:read",
"granted_scopes": [
"<string>"
],
"manage_url": "<string>",
"scheme": "<string>"
}{
"error": "<string>",
"retry_after": 123,
"upgrade_url": "<string>",
"reason": "<string>",
"code": "agent_scope_insufficient",
"required_scope": "hookie:read",
"granted_scopes": [
"<string>"
],
"manage_url": "<string>",
"scheme": "<string>"
}Authorizations
The console's sealed session cookie, set by WorkOS AuthKit. Mutating requests also require the X-Requested-With CSRF header.
Headers
Makes a POST safe to retry for 24 hours: the same key, credential, path and body returns the stored response (Idempotent-Replayed: true) instead of running again. 422 if the key was used for a different request; 409 while the first is in flight.
1 - 255Path Parameters
Project id (UUID) belonging to the caller workspace.
Body
1 - 80The base slug (lowercase letters, digits and hyphens, max 47 characters). This is NOT the public URL - a separate high-entropy slug is generated and returned as webhook_slug. Optional: when omitted or empty it is derived from the name (accents folded, Cyrillic and Greek transliterated) plus a 4-character random suffix, and a name with no Latin spelling becomes endpoint-.
^[a-z0-9][a-z0-9-]{0,46}$Optional: when omitted or empty, the name as an identifier (folded to ASCII, d_ prefixed if it starts with a digit), or default.
^[A-Za-z][A-Za-z0-9_]{0,62}$Anything other than the literal false is treated as true.
Omitted or empty accepts any payload (the URL already scopes it). At most 10, the same limit as a rule's conditions; more is 400 "At most 10 criteria are allowed".
10Show child attributes
Show child attributes
Omitted or empty means identity - store the whole payload.
50Show child attributes
Show child attributes
IP addresses or CIDR ranges allowed to use this credential, validated exactly as the workspace and project allowlists are. [] or null removes the restriction (stored as null). A request must pass every allowlist that is set: this one, the project's and the workspace's. Refused requests get 403 'Source IP not allowed' and an ingest_ip_blocked audit row.
64An IPv4 or IPv6 address, or a CIDR range.
["203.0.113.0/24", "2001:db8::/32"]
Provider signature verification (#210). Ingest checks the signature over the raw request bytes before idempotency, the quota and the store; a missing header, a forged signature or a stale timestamp answers 401 {reason, scheme} and is neither stored nor counted, and writes an ingest_signature_rejected audit row. Applies on the public path-form URL and on /v1/webhooks/{ingest_key}/{slug}. Settings that do not apply to the scheme are refused with 400.
Show child attributes
Show child attributes
{ "scheme": "stripe", "secret": "whsec_..." }
ING-8: an https:// URL (no user:password) a browser's native form POST is sent to (303) once stored. null or "" clears it.
2048"https://example.com/thanks"
ING-8: origins whose fetch() may call the endpoint URL and read the answer, OPTIONS preflight included. [] or null allows none, which is the default. Stored normalised (a trailing slash dropped).
20An origin - scheme, host and optional port, no path - or "*" for any page.
["https://example.com"]
ING-10: the provider URL-verification challenge the endpoint answers without storing it. null answers none. Choosing one that takes no secret clears any stored handshake_secret.
slack, meta, graph, zoom, twitch, null Required with handshake zoom (the app's Secret Token, which signs the answer) or meta (the Verify Token compared with hub.verify_token), unless one is already stored for that same handshake. Refused with any other handshake. WRITE-ONLY: AES-256-GCM encrypted, never returned, never in the audit log (which records only that one was set).
1 - 256Response
Created at version 1, with a mirror mapping rule synced behind it. webhook_slug is the public URL credential and is the only place it is returned in full at creation time.
DEPRECATED shape: the versioned readable id, <base_slug>-v<version>. Matches no column and never appears in a URL. Use base_slug + version, or webhook_slug for the URL.
"orders-v1"
The high-entropy public URL segment. THIS IS THE CREDENTIAL — anyone holding it can post events. Never log it.
"aBcXyz0123456789abcdefgh"
The readable identity you supplied. NOT the public URL segment. Safe to log.
"orders"
Endpoint version within base_slug.
"1"
The endpoint's public location, {workspace}/{project}/{webhook_slug}. Pass this straight to a POST; it needs no assembly. It contains the credential, so treat it as a secret.
"acme-3f9a/orders/aBcXyz0123456789abcdefgh"
public_path as an absolute URL on this deployment. Contains the credential — treat it as a secret.
"https://app.hookie.ai/acme-3f9a/orders/aBcXyz0123456789abcdefgh"
An endpoint's signature verification as responses describe it: the scheme and its settings, never the secret.
Show child attributes
Show child attributes
The created webhook, as GET by id returns it (#201).
Show child attributes
Show child attributes