Skip to content
NessFlow
Menu
    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

    • 200 OK.
    • 401 Missing, invalid or expired token, or a member who left the team.
    • 403 The 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`).
    • 429 The 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

    • url string · required

      An HTTPS URL that resolves to a public address.

    • events string[] · required

      Values (crawl.completed, crawl.failed, crawl.stopped, export.ready, recommendations.ready, signal.detected)

    • description string | null · optional

    Responses

    • 201 OK.
    • 400 Malformed request, such as an invalid pagination cursor (`pagination.invalid_cursor`).
    • 401 Missing, invalid or expired token, or a member who left the team.
    • 403 The 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`).
    • 422 Invalid query or body values (`validation_failed`), with `errors` by field.
    • 429 The 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

    • webhook path · string (uuid) · required

      The webhook identifier.

    Responses

    • 200 OK.
    • 401 Missing, invalid or expired token, or a member who left the team.
    • 403 The 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`).
    • 404 No such resource for the token's team. Resources of another team answer 404 as well, never 403.
    • 429 The 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

    • webhook path · string (uuid) · required

      The webhook identifier.

    Request body

    • url string · optional

    • events string[] · optional

      Values (crawl.completed, crawl.failed, crawl.stopped, export.ready, recommendations.ready, signal.detected)

    • description string | null · optional

    • active boolean · optional

    Responses

    • 200 OK.
    • 401 Missing, invalid or expired token, or a member who left the team.
    • 403 The 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`).
    • 404 No such resource for the token's team. Resources of another team answer 404 as well, never 403.
    • 422 Invalid query or body values (`validation_failed`), with `errors` by field.
    • 429 The 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

    • webhook path · string (uuid) · required

      The webhook identifier.

    Responses

    • 204 Deleted.
    • 401 Missing, invalid or expired token, or a member who left the team.
    • 403 The 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`).
    • 404 No such resource for the token's team. Resources of another team answer 404 as well, never 403.
    • 429 The 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

    • webhook path · string (uuid) · required

      The webhook identifier.

    Responses

    • 200 OK.
    • 400 Malformed request, such as an invalid pagination cursor (`pagination.invalid_cursor`).
    • 401 Missing, invalid or expired token, or a member who left the team.
    • 403 The 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`).
    • 404 No such resource for the token's team. Resources of another team answer 404 as well, never 403.
    • 429 The 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

    • webhook path · string (uuid) · required

      The webhook identifier.

    Responses

    • 202 OK.
    • 400 Malformed request, such as an invalid pagination cursor (`pagination.invalid_cursor`).
    • 401 Missing, invalid or expired token, or a member who left the team.
    • 403 The 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`).
    • 404 No such resource for the token's team. Resources of another team answer 404 as well, never 403.
    • 429 The 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

    • webhook path · string (uuid) · required

      The webhook identifier.

    • limit query · integer · optional

      Items per page, 1 to 100 (default 25).

    • cursor query · string · optional

      Opaque 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

    • 200 OK.
    • 401 Missing, invalid or expired token, or a member who left the team.
    • 403 The 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`).
    • 404 No such resource for the token's team. Resources of another team answer 404 as well, never 403.
    • 422 Invalid query or body values (`validation_failed`), with `errors` by field.
    • 429 The 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"
        }
    }

    API reference Error codes API documentation

    This page in Markdown