Documentation contents
Webhooks
Signed outbound events (crawl finished, export ready, recommendations ready, signal detected). Managed by team owners and admins.
List webhooks
GET
/v1/webhooks
The team's webhook subscriptions. Secrets are never returned here.
- Required scopes
- webhooks:read
- Rate-limit cost
- 1 unit(s)
Responses
-
200OK. -
401Missing, invalid or expired token, or a member who left the team. -
403The plan does not include the API (`api.not_in_plan`) or this verb (`api.write_not_in_plan`), or the token lacks a scope (`token.missing_scope`). -
429The plan rate limit is exhausted for one window.
cURL
curl "https://api.nessflow.com/v1/webhooks" \
-H "Authorization: Bearer $NESSFLOW_TOKEN"
PHP
$client = new GuzzleHttp\Client(['base_uri' => 'https://api.nessflow.com/v1/']);
$response = $client->request('GET', 'webhooks', [
'headers' => [
'Authorization' => 'Bearer '.getenv('NESSFLOW_TOKEN'),
],
]);
$data = json_decode((string) $response->getBody(), true);
JavaScript
const response = await fetch('https://api.nessflow.com/v1/webhooks', {
headers: {
Authorization: `Bearer ${process.env.NESSFLOW_TOKEN}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.get(
"https://api.nessflow.com/v1/webhooks",
headers={
"Authorization": f"Bearer {os.environ['NESSFLOW_TOKEN']}",
},
timeout=30,
)
response.raise_for_status()
data = response.json()
Response example
{
"data": [
{
"id": "0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57",
"url": "https://exemple.fr/",
"description": "string",
"events": [
"crawl.completed"
],
"active": true,
"disabled_reason": "consecutive_failures",
"disabled_at": "2026-10-01T09:30:00Z",
"consecutive_failures": 42,
"last_delivery_at": "2026-10-01T09:30:00Z",
"secret_rotation_ends_at": "2026-10-01T09:30:00Z",
"created_at": "2026-10-01T09:30:00Z"
}
]
}
Create a webhook
POST
/v1/webhooks
Subscribes an HTTPS URL to events. The response carries the signing secret, shown
this one time only. The URL is refused when it does not resolve to a public address
(webhook.url_refused). Counts against api.webhooks.max.
- Required scopes
- webhooks:write
- Rate-limit cost
- 1 unit(s)
Request body
-
urlstring · requiredAn HTTPS URL that resolves to a public address.
-
eventsstring[] · requiredValues (crawl.completed, crawl.failed, crawl.stopped, export.ready, recommendations.ready, signal.detected)
-
descriptionstring | null · optional
Responses
-
201OK. -
400Malformed request, such as an invalid pagination cursor (`pagination.invalid_cursor`). -
401Missing, invalid or expired token, or a member who left the team. -
403The plan does not include the API (`api.not_in_plan`) or this verb (`api.write_not_in_plan`), or the token lacks a scope (`token.missing_scope`). -
422Invalid query or body values (`validation_failed`), with `errors` by field. -
429The plan rate limit is exhausted for one window.
cURL
curl -X POST "https://api.nessflow.com/v1/webhooks" \
-H "Authorization: Bearer $NESSFLOW_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url":"https://exemple.fr/","events":["crawl.completed"],"description":"string"}'
PHP
$client = new GuzzleHttp\Client(['base_uri' => 'https://api.nessflow.com/v1/']);
$response = $client->request('POST', 'webhooks', [
'headers' => [
'Authorization' => 'Bearer '.getenv('NESSFLOW_TOKEN'),
],
'json' => [
'url' => 'https://exemple.fr/',
'events' => [
'crawl.completed',
],
'description' => 'string',
],
]);
$data = json_decode((string) $response->getBody(), true);
JavaScript
const response = await fetch('https://api.nessflow.com/v1/webhooks', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NESSFLOW_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"url": "https://exemple.fr/",
"events": [
"crawl.completed"
],
"description": "string"
}),
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.nessflow.com/v1/webhooks",
headers={
"Authorization": f"Bearer {os.environ['NESSFLOW_TOKEN']}",
},
json={
"url": "https://exemple.fr/",
"events": [
"crawl.completed",
],
"description": "string",
},
timeout=30,
)
response.raise_for_status()
data = response.json()
Response example
{
"data": {
"id": "0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57",
"url": "https://exemple.fr/",
"description": "string",
"events": [
"crawl.completed"
],
"active": true,
"disabled_reason": "consecutive_failures",
"disabled_at": "2026-10-01T09:30:00Z",
"consecutive_failures": 42,
"last_delivery_at": "2026-10-01T09:30:00Z",
"secret_rotation_ends_at": "2026-10-01T09:30:00Z",
"created_at": "2026-10-01T09:30:00Z",
"secret": "string"
}
}
Read a webhook
GET
/v1/webhooks/{webhook}
One subscription, its state and its failure counter.
- Required scopes
- webhooks:read
- Rate-limit cost
- 1 unit(s)
Parameters
-
webhookpath · string (uuid) · requiredThe webhook identifier.
Responses
-
200OK. -
401Missing, invalid or expired token, or a member who left the team. -
403The plan does not include the API (`api.not_in_plan`) or this verb (`api.write_not_in_plan`), or the token lacks a scope (`token.missing_scope`). -
404No such resource for the token's team. Resources of another team answer 404 as well, never 403. -
429The plan rate limit is exhausted for one window.
cURL
curl "https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57" \
-H "Authorization: Bearer $NESSFLOW_TOKEN"
PHP
$client = new GuzzleHttp\Client(['base_uri' => 'https://api.nessflow.com/v1/']);
$response = $client->request('GET', 'webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57', [
'headers' => [
'Authorization' => 'Bearer '.getenv('NESSFLOW_TOKEN'),
],
]);
$data = json_decode((string) $response->getBody(), true);
JavaScript
const response = await fetch('https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57', {
headers: {
Authorization: `Bearer ${process.env.NESSFLOW_TOKEN}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.get(
"https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57",
headers={
"Authorization": f"Bearer {os.environ['NESSFLOW_TOKEN']}",
},
timeout=30,
)
response.raise_for_status()
data = response.json()
Response example
{
"data": {
"id": "0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57",
"url": "https://exemple.fr/",
"description": "string",
"events": [
"crawl.completed"
],
"active": true,
"disabled_reason": "consecutive_failures",
"disabled_at": "2026-10-01T09:30:00Z",
"consecutive_failures": 42,
"last_delivery_at": "2026-10-01T09:30:00Z",
"secret_rotation_ends_at": "2026-10-01T09:30:00Z",
"created_at": "2026-10-01T09:30:00Z"
}
}
Update a webhook
PATCH
/v1/webhooks/{webhook}
Changes the URL, the events or the description, or enables and disables the subscription. Enabling it again clears an automatic disablement and its failure counter.
- Required scopes
- webhooks:write
- Rate-limit cost
- 1 unit(s)
Parameters
-
webhookpath · string (uuid) · requiredThe webhook identifier.
Request body
-
urlstring · optional -
eventsstring[] · optionalValues (crawl.completed, crawl.failed, crawl.stopped, export.ready, recommendations.ready, signal.detected)
-
descriptionstring | null · optional -
activeboolean · optional
Responses
-
200OK. -
401Missing, invalid or expired token, or a member who left the team. -
403The plan does not include the API (`api.not_in_plan`) or this verb (`api.write_not_in_plan`), or the token lacks a scope (`token.missing_scope`). -
404No such resource for the token's team. Resources of another team answer 404 as well, never 403. -
422Invalid query or body values (`validation_failed`), with `errors` by field. -
429The plan rate limit is exhausted for one window.
cURL
curl -X PATCH "https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57" \
-H "Authorization: Bearer $NESSFLOW_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url":"https://exemple.fr/","events":["crawl.completed"],"description":"string","active":true}'
PHP
$client = new GuzzleHttp\Client(['base_uri' => 'https://api.nessflow.com/v1/']);
$response = $client->request('PATCH', 'webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57', [
'headers' => [
'Authorization' => 'Bearer '.getenv('NESSFLOW_TOKEN'),
],
'json' => [
'url' => 'https://exemple.fr/',
'events' => [
'crawl.completed',
],
'description' => 'string',
'active' => true,
],
]);
$data = json_decode((string) $response->getBody(), true);
JavaScript
const response = await fetch('https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.NESSFLOW_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"url": "https://exemple.fr/",
"events": [
"crawl.completed"
],
"description": "string",
"active": true
}),
});
const data = await response.json();
Python
import os
import requests
response = requests.patch(
"https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57",
headers={
"Authorization": f"Bearer {os.environ['NESSFLOW_TOKEN']}",
},
json={
"url": "https://exemple.fr/",
"events": [
"crawl.completed",
],
"description": "string",
"active": True,
},
timeout=30,
)
response.raise_for_status()
data = response.json()
Response example
{
"data": {
"id": "0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57",
"url": "https://exemple.fr/",
"description": "string",
"events": [
"crawl.completed"
],
"active": true,
"disabled_reason": "consecutive_failures",
"disabled_at": "2026-10-01T09:30:00Z",
"consecutive_failures": 42,
"last_delivery_at": "2026-10-01T09:30:00Z",
"secret_rotation_ends_at": "2026-10-01T09:30:00Z",
"created_at": "2026-10-01T09:30:00Z"
}
}
Delete a webhook
DELETE
/v1/webhooks/{webhook}
Deletes the subscription and its delivery log. Never refused by billing.
- Required scopes
- webhooks:write
- Rate-limit cost
- 1 unit(s)
Parameters
-
webhookpath · string (uuid) · requiredThe webhook identifier.
Responses
-
204Deleted. -
401Missing, invalid or expired token, or a member who left the team. -
403The plan does not include the API (`api.not_in_plan`) or this verb (`api.write_not_in_plan`), or the token lacks a scope (`token.missing_scope`). -
404No such resource for the token's team. Resources of another team answer 404 as well, never 403. -
429The plan rate limit is exhausted for one window.
cURL
curl -X DELETE "https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57" \
-H "Authorization: Bearer $NESSFLOW_TOKEN"
PHP
$client = new GuzzleHttp\Client(['base_uri' => 'https://api.nessflow.com/v1/']);
$response = $client->request('DELETE', 'webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57', [
'headers' => [
'Authorization' => 'Bearer '.getenv('NESSFLOW_TOKEN'),
],
]);
JavaScript
const response = await fetch('https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${process.env.NESSFLOW_TOKEN}`,
},
});
Python
import os
import requests
response = requests.delete(
"https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57",
headers={
"Authorization": f"Bearer {os.environ['NESSFLOW_TOKEN']}",
},
timeout=30,
)
response.raise_for_status()
Response example
No response body.
Rotate the signing secret
POST
/v1/webhooks/{webhook}/rotate-secret
Issues a new signing secret, returned this one time. For 24 hours, deliveries carry
one v1 signature per secret, so the receiver can switch without dropping events.
- Required scopes
- webhooks:write
- Rate-limit cost
- 1 unit(s)
Parameters
-
webhookpath · string (uuid) · requiredThe webhook identifier.
Responses
-
200OK. -
400Malformed request, such as an invalid pagination cursor (`pagination.invalid_cursor`). -
401Missing, invalid or expired token, or a member who left the team. -
403The plan does not include the API (`api.not_in_plan`) or this verb (`api.write_not_in_plan`), or the token lacks a scope (`token.missing_scope`). -
404No such resource for the token's team. Resources of another team answer 404 as well, never 403. -
429The plan rate limit is exhausted for one window.
cURL
curl -X POST "https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57/rotate-secret" \
-H "Authorization: Bearer $NESSFLOW_TOKEN"
PHP
$client = new GuzzleHttp\Client(['base_uri' => 'https://api.nessflow.com/v1/']);
$response = $client->request('POST', 'webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57/rotate-secret', [
'headers' => [
'Authorization' => 'Bearer '.getenv('NESSFLOW_TOKEN'),
],
]);
$data = json_decode((string) $response->getBody(), true);
JavaScript
const response = await fetch('https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57/rotate-secret', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NESSFLOW_TOKEN}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57/rotate-secret",
headers={
"Authorization": f"Bearer {os.environ['NESSFLOW_TOKEN']}",
},
timeout=30,
)
response.raise_for_status()
data = response.json()
Response example
{
"data": {
"id": "0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57",
"url": "https://exemple.fr/",
"description": "string",
"events": [
"crawl.completed"
],
"active": true,
"disabled_reason": "consecutive_failures",
"disabled_at": "2026-10-01T09:30:00Z",
"consecutive_failures": 42,
"last_delivery_at": "2026-10-01T09:30:00Z",
"secret_rotation_ends_at": "2026-10-01T09:30:00Z",
"created_at": "2026-10-01T09:30:00Z",
"secret": "string"
}
}
Send a test event
POST
/v1/webhooks/{webhook}/test
Queues a signed webhook.test event (202), even to a disabled subscription, so a fix
can be checked before enabling it again. It is never retried and never counts as a
failure. Read the result in the delivery log.
- Required scopes
- webhooks:write
- Rate-limit cost
- 1 unit(s)
- Idempotency-Key
- Idempotency-Key accepted
Parameters
-
webhookpath · string (uuid) · requiredThe webhook identifier.
Responses
-
202OK. -
400Malformed request, such as an invalid pagination cursor (`pagination.invalid_cursor`). -
401Missing, invalid or expired token, or a member who left the team. -
403The plan does not include the API (`api.not_in_plan`) or this verb (`api.write_not_in_plan`), or the token lacks a scope (`token.missing_scope`). -
404No such resource for the token's team. Resources of another team answer 404 as well, never 403. -
429The plan rate limit is exhausted for one window.
cURL
curl -X POST "https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57/test" \
-H "Authorization: Bearer $NESSFLOW_TOKEN" \
-H "Idempotency-Key: $(uuidgen)"
PHP
$client = new GuzzleHttp\Client(['base_uri' => 'https://api.nessflow.com/v1/']);
$response = $client->request('POST', 'webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57/test', [
'headers' => [
'Authorization' => 'Bearer '.getenv('NESSFLOW_TOKEN'),
'Idempotency-Key' => bin2hex(random_bytes(16)),
],
]);
$data = json_decode((string) $response->getBody(), true);
JavaScript
const response = await fetch('https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57/test', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NESSFLOW_TOKEN}`,
'Idempotency-Key': crypto.randomUUID(),
},
});
const data = await response.json();
Python
import os
import uuid
import requests
response = requests.post(
"https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57/test",
headers={
"Authorization": f"Bearer {os.environ['NESSFLOW_TOKEN']}",
"Idempotency-Key": str(uuid.uuid4()),
},
timeout=30,
)
response.raise_for_status()
data = response.json()
Response example
{
"data": {
"id": "0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57",
"event_id": "0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57",
"event_type": "string",
"status": "pending",
"attempts": 42,
"response_status": 200,
"error_reason": "refused_address",
"duration_ms": 42,
"next_attempt_at": "2026-10-01T09:30:00Z",
"delivered_at": "2026-10-01T09:30:00Z",
"created_at": "2026-10-01T09:30:00Z"
}
}
List deliveries
GET
/v1/webhooks/{webhook}/deliveries
The delivery log, most recent first, kept 30 days. Only the status, the duration and a stable reason are kept, never the receiver's response body.
- Required scopes
- webhooks:read
- Rate-limit cost
- 1 unit(s)
Parameters
-
webhookpath · string (uuid) · requiredThe webhook identifier.
-
limitquery · integer · optionalItems per page, 1 to 100 (default 25).
-
cursorquery · string · optionalOpaque cursor from `meta.next_cursor`. It is bound to the request that issued it; reusing it with other filters returns `400 pagination.invalid_cursor`.
Responses
-
200OK. -
401Missing, invalid or expired token, or a member who left the team. -
403The plan does not include the API (`api.not_in_plan`) or this verb (`api.write_not_in_plan`), or the token lacks a scope (`token.missing_scope`). -
404No such resource for the token's team. Resources of another team answer 404 as well, never 403. -
422Invalid query or body values (`validation_failed`), with `errors` by field. -
429The plan rate limit is exhausted for one window.
cURL
curl "https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57/deliveries" \
-H "Authorization: Bearer $NESSFLOW_TOKEN"
PHP
$client = new GuzzleHttp\Client(['base_uri' => 'https://api.nessflow.com/v1/']);
$response = $client->request('GET', 'webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57/deliveries', [
'headers' => [
'Authorization' => 'Bearer '.getenv('NESSFLOW_TOKEN'),
],
]);
$data = json_decode((string) $response->getBody(), true);
JavaScript
const response = await fetch('https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57/deliveries', {
headers: {
Authorization: `Bearer ${process.env.NESSFLOW_TOKEN}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.get(
"https://api.nessflow.com/v1/webhooks/0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57/deliveries",
headers={
"Authorization": f"Bearer {os.environ['NESSFLOW_TOKEN']}",
},
timeout=30,
)
response.raise_for_status()
data = response.json()
Response example
{
"data": [
{
"id": "0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57",
"event_id": "0190f5a2-7c4e-7b1a-9d3e-2f6b8c1a4e57",
"event_type": "string",
"status": "pending",
"attempts": 42,
"response_status": 200,
"error_reason": "refused_address",
"duration_ms": 42,
"next_attempt_at": "2026-10-01T09:30:00Z",
"delivered_at": "2026-10-01T09:30:00Z",
"created_at": "2026-10-01T09:30:00Z"
}
],
"meta": {
"limit": 100,
"has_more": true,
"next_cursor": "string"
}
}