> For the complete documentation index, see [llms.txt](https://docs.currents.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.currents.dev/api/resources/webhooks.md).

# Webhooks

Manage HTTP webhooks for projects.

Webhooks allow you to receive HTTP POST notifications when certain events occur in your test runs:

* **RUN\_FINISH**: Run completed (passed or failed)
* **RUN\_START**: Run started
* **RUN\_TIMEOUT**: Run timed out
* **RUN\_CANCELED**: Run was cancelled

Webhooks are useful for integrating with external tools like n8n, Zapier, or custom automation platforms.

## List webhooks

> Get all generic webhooks for a project

```json
{"openapi":"3.0.2","info":{"title":"Currents REST API","version":"1.0.0"},"tags":[{"name":"Webhooks","description":"Manage HTTP webhooks for projects.\n\nWebhooks allow you to receive HTTP POST notifications when certain events occur in your test runs:\n- **RUN_FINISH**: Run completed (passed or failed)\n- **RUN_START**: Run started\n- **RUN_TIMEOUT**: Run timed out\n- **RUN_CANCELED**: Run was cancelled\n\nWebhooks are useful for integrating with external tools like n8n, Zapier, or custom automation platforms."}],"servers":[{"url":"https://api.currents.dev/v1","description":"Production API"},{"url":"https://api-staging.currents.dev/v1","description":"Staging API"},{"url":"http://localhost:4000/v1","description":"Local development"}],"security":[{"BearerAuth":[]},{"OAuth2":["webhooks:read"]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"An organization API key, sent as `Authorization: Bearer <key>`. A key carries one scope for the whole API: a `read` key reaches the GET endpoints, a `write` key reaches all of them. A `crnts_pat_` personal access token is accepted as a Bearer while its owner remains an eligible administrator in its active organization and the global rollout is enabled. Each operation requires the corresponding scope stored on that token."},"OAuth2":{"type":"oauth2","description":"An access token from the Currents authorization server, for an application acting as a user in one organization. Unlike an API key it is confined per endpoint: the operation lists the scope it needs, and a token that was not granted that scope gets 403.\n\n**The endpoints below are production's.** They are written out so this specification can be exercised against production, and they are wrong for every other deployment — a token minted by `id.currents.dev` is refused by a staging or on-premise API, whose `OAUTH_ISSUER` and `OAUTH_API_AUDIENCE` are its own. Against anything but production, and in a client generated from this document, discover them instead: `GET {api origin}/.well-known/oauth-protected-resource`, then the `authorization_servers[0]` document.\n\n**The audience named here is the REST API's.** `POST /mcp` is a second resource server on the same origin, identified by `{api origin}/mcp` and described by its own document at `/.well-known/oauth-protected-resource/mcp`. Every operation below refuses a token minted for it, and it refuses every token minted for these, so a client of this specification wants the audience above and not that one.","flows":{"authorizationCode":{"authorizationUrl":"https://id.currents.dev/api/auth/oauth2/authorize","tokenUrl":"https://id.currents.dev/api/auth/oauth2/token","refreshUrl":"https://id.currents.dev/api/auth/oauth2/token","scopes":{"projects:read":"See which projects exist and their names","projects:write":"Change project configuration, including a project's tags","results:read":"See runs, specs and failure detail — errors, stacks and artifacts","analytics:read":"See aggregate metrics: flake and failure rates, error counts, durations and trends","actions:read":"See quarantine and skip rules, and the tests they affect","actions:write":"Create, edit, enable, disable and archive quarantine and skip rules","issues:write":"Create and link issues in a connected issue tracker, and list its projects and issue types","runs:write":"Cancel and reset runs, and permanently delete runs and their artifacts","webhooks:read":"See webhook configuration, including destination URLs and headers","webhooks:write":"Create, edit and delete webhooks"}}}}},"parameters":{"ProjectIdQuery":{"name":"projectId","in":"query","required":true,"description":"The project ID","schema":{"type":"string"}}},"schemas":{"WebhookListResponse":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","enum":["OK"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/Webhook"}}}},"Webhook":{"type":"object","required":["hookId","projectId","url","hookEvents"],"properties":{"hookId":{"type":"string","format":"uuid","description":"Unique identifier for the webhook"},"projectId":{"type":"string","description":"Project this webhook belongs to"},"url":{"type":"string","format":"uri","maxLength":2048,"description":"URL to send webhook POST requests to"},"headers":{"type":"string","nullable":true,"maxLength":4096,"description":"Custom headers as a JSON object string (e.g., {\"Authorization\": \"Bearer token\"})"},"hookEvents":{"type":"array","items":{"$ref":"#/components/schemas/HookEvent"},"description":"Events that trigger this webhook"},"label":{"type":"string","nullable":true,"maxLength":255,"description":"Human-readable label for the webhook"},"createdAt":{"type":"string","format":"date-time","nullable":true,"description":"When the webhook was created"},"updatedAt":{"type":"string","format":"date-time","nullable":true,"description":"When the webhook was last updated"}}},"HookEvent":{"type":"string","enum":["RUN_FINISH","RUN_START","RUN_TIMEOUT","RUN_CANCELED"],"description":"The event that triggers the webhook:\n- `RUN_FINISH`: Run completed (passed or failed)\n- `RUN_START`: Run started\n- `RUN_TIMEOUT`: Run timed out\n- `RUN_CANCELED`: Run was cancelled"},"ErrorResponse":{"type":"object","required":["status","error"],"properties":{"status":{"type":"string","enum":["FAILED"]},"error":{"type":"string","description":"Error message"},"code":{"type":"string","description":"Stable machine-readable discriminator, present on refusals a client is expected to branch on (for example `AI_CONSENT_REQUIRED`). The `error` text is free-form; branch on `code`."}}}},"responses":{"BadRequest":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Unauthorized":{"description":"Authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Forbidden":{"description":"The credential authenticated but is not permitted to use this endpoint. Four refusals share the status: a `read` API key on an endpoint that writes, an OAuth token that was not granted the scope the endpoint needs (`insufficient_scope`), an OAuth token that was granted it but whose holder no longer has a role permitting it (`insufficient_role`), and an OAuth token on an endpoint that accepts an API key and nothing else (`api_key_required`). Discriminate on `code` rather than on the message, which is free-form. The first refusal predates the codes and sets none. Only `insufficient_scope` carries the challenge header, because it is the only one a client can act on by re-authorizing.","headers":{"WWW-Authenticate":{"description":"Present on the `insufficient_scope` refusal only, naming the scope to re-authorize for (RFC 6750 3.1): `Bearer error=\"insufficient_scope\", scope=\"webhooks:read\"`. Absent on `insufficient_role`, where the scope is already granted, and on `api_key_required`, where no scope would help.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"NotFound":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"paths":{"/webhooks":{"get":{"summary":"List webhooks","description":"Get all generic webhooks for a project","operationId":"listWebhooks","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/ProjectIdQuery"}],"responses":{"200":{"description":"List of webhooks","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```

## Create webhook

> Create a new generic webhook for a project

```json
{"openapi":"3.0.2","info":{"title":"Currents REST API","version":"1.0.0"},"tags":[{"name":"Webhooks","description":"Manage HTTP webhooks for projects.\n\nWebhooks allow you to receive HTTP POST notifications when certain events occur in your test runs:\n- **RUN_FINISH**: Run completed (passed or failed)\n- **RUN_START**: Run started\n- **RUN_TIMEOUT**: Run timed out\n- **RUN_CANCELED**: Run was cancelled\n\nWebhooks are useful for integrating with external tools like n8n, Zapier, or custom automation platforms."}],"servers":[{"url":"https://api.currents.dev/v1","description":"Production API"},{"url":"https://api-staging.currents.dev/v1","description":"Staging API"},{"url":"http://localhost:4000/v1","description":"Local development"}],"security":[{"BearerAuth":[]},{"OAuth2":["webhooks:write"]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"An organization API key, sent as `Authorization: Bearer <key>`. A key carries one scope for the whole API: a `read` key reaches the GET endpoints, a `write` key reaches all of them. A `crnts_pat_` personal access token is accepted as a Bearer while its owner remains an eligible administrator in its active organization and the global rollout is enabled. Each operation requires the corresponding scope stored on that token."},"OAuth2":{"type":"oauth2","description":"An access token from the Currents authorization server, for an application acting as a user in one organization. Unlike an API key it is confined per endpoint: the operation lists the scope it needs, and a token that was not granted that scope gets 403.\n\n**The endpoints below are production's.** They are written out so this specification can be exercised against production, and they are wrong for every other deployment — a token minted by `id.currents.dev` is refused by a staging or on-premise API, whose `OAUTH_ISSUER` and `OAUTH_API_AUDIENCE` are its own. Against anything but production, and in a client generated from this document, discover them instead: `GET {api origin}/.well-known/oauth-protected-resource`, then the `authorization_servers[0]` document.\n\n**The audience named here is the REST API's.** `POST /mcp` is a second resource server on the same origin, identified by `{api origin}/mcp` and described by its own document at `/.well-known/oauth-protected-resource/mcp`. Every operation below refuses a token minted for it, and it refuses every token minted for these, so a client of this specification wants the audience above and not that one.","flows":{"authorizationCode":{"authorizationUrl":"https://id.currents.dev/api/auth/oauth2/authorize","tokenUrl":"https://id.currents.dev/api/auth/oauth2/token","refreshUrl":"https://id.currents.dev/api/auth/oauth2/token","scopes":{"projects:read":"See which projects exist and their names","projects:write":"Change project configuration, including a project's tags","results:read":"See runs, specs and failure detail — errors, stacks and artifacts","analytics:read":"See aggregate metrics: flake and failure rates, error counts, durations and trends","actions:read":"See quarantine and skip rules, and the tests they affect","actions:write":"Create, edit, enable, disable and archive quarantine and skip rules","issues:write":"Create and link issues in a connected issue tracker, and list its projects and issue types","runs:write":"Cancel and reset runs, and permanently delete runs and their artifacts","webhooks:read":"See webhook configuration, including destination URLs and headers","webhooks:write":"Create, edit and delete webhooks"}}}}},"parameters":{"ProjectIdQuery":{"name":"projectId","in":"query","required":true,"description":"The project ID","schema":{"type":"string"}}},"schemas":{"CreateWebhookRequest":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","maxLength":2048,"description":"URL to send webhook POST requests to"},"headers":{"type":"string","maxLength":4096,"nullable":true,"description":"Custom headers as a JSON object string (e.g., {\"Authorization\": \"Bearer token\"})"},"hookEvents":{"type":"array","items":{"$ref":"#/components/schemas/HookEvent"},"default":[],"description":"Events that trigger this webhook"},"label":{"type":"string","minLength":1,"maxLength":255,"nullable":true,"description":"Human-readable label for the webhook"}}},"HookEvent":{"type":"string","enum":["RUN_FINISH","RUN_START","RUN_TIMEOUT","RUN_CANCELED"],"description":"The event that triggers the webhook:\n- `RUN_FINISH`: Run completed (passed or failed)\n- `RUN_START`: Run started\n- `RUN_TIMEOUT`: Run timed out\n- `RUN_CANCELED`: Run was cancelled"},"WebhookResponse":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","enum":["OK"]},"data":{"$ref":"#/components/schemas/Webhook"}}},"Webhook":{"type":"object","required":["hookId","projectId","url","hookEvents"],"properties":{"hookId":{"type":"string","format":"uuid","description":"Unique identifier for the webhook"},"projectId":{"type":"string","description":"Project this webhook belongs to"},"url":{"type":"string","format":"uri","maxLength":2048,"description":"URL to send webhook POST requests to"},"headers":{"type":"string","nullable":true,"maxLength":4096,"description":"Custom headers as a JSON object string (e.g., {\"Authorization\": \"Bearer token\"})"},"hookEvents":{"type":"array","items":{"$ref":"#/components/schemas/HookEvent"},"description":"Events that trigger this webhook"},"label":{"type":"string","nullable":true,"maxLength":255,"description":"Human-readable label for the webhook"},"createdAt":{"type":"string","format":"date-time","nullable":true,"description":"When the webhook was created"},"updatedAt":{"type":"string","format":"date-time","nullable":true,"description":"When the webhook was last updated"}}},"ErrorResponse":{"type":"object","required":["status","error"],"properties":{"status":{"type":"string","enum":["FAILED"]},"error":{"type":"string","description":"Error message"},"code":{"type":"string","description":"Stable machine-readable discriminator, present on refusals a client is expected to branch on (for example `AI_CONSENT_REQUIRED`). The `error` text is free-form; branch on `code`."}}}},"responses":{"BadRequest":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Unauthorized":{"description":"Authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Forbidden":{"description":"The credential authenticated but is not permitted to use this endpoint. Four refusals share the status: a `read` API key on an endpoint that writes, an OAuth token that was not granted the scope the endpoint needs (`insufficient_scope`), an OAuth token that was granted it but whose holder no longer has a role permitting it (`insufficient_role`), and an OAuth token on an endpoint that accepts an API key and nothing else (`api_key_required`). Discriminate on `code` rather than on the message, which is free-form. The first refusal predates the codes and sets none. Only `insufficient_scope` carries the challenge header, because it is the only one a client can act on by re-authorizing.","headers":{"WWW-Authenticate":{"description":"Present on the `insufficient_scope` refusal only, naming the scope to re-authorize for (RFC 6750 3.1): `Bearer error=\"insufficient_scope\", scope=\"webhooks:read\"`. Absent on `insufficient_role`, where the scope is already granted, and on `api_key_required`, where no scope would help.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"NotFound":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"paths":{"/webhooks":{"post":{"summary":"Create webhook","description":"Create a new generic webhook for a project","operationId":"createWebhook","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/ProjectIdQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookRequest"}}}},"responses":{"201":{"description":"Webhook created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```

## Get webhook

> Get a specific webhook by ID. The hookId is globally unique, so projectId is not required.

```json
{"openapi":"3.0.2","info":{"title":"Currents REST API","version":"1.0.0"},"tags":[{"name":"Webhooks","description":"Manage HTTP webhooks for projects.\n\nWebhooks allow you to receive HTTP POST notifications when certain events occur in your test runs:\n- **RUN_FINISH**: Run completed (passed or failed)\n- **RUN_START**: Run started\n- **RUN_TIMEOUT**: Run timed out\n- **RUN_CANCELED**: Run was cancelled\n\nWebhooks are useful for integrating with external tools like n8n, Zapier, or custom automation platforms."}],"servers":[{"url":"https://api.currents.dev/v1","description":"Production API"},{"url":"https://api-staging.currents.dev/v1","description":"Staging API"},{"url":"http://localhost:4000/v1","description":"Local development"}],"security":[{"BearerAuth":[]},{"OAuth2":["webhooks:read"]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"An organization API key, sent as `Authorization: Bearer <key>`. A key carries one scope for the whole API: a `read` key reaches the GET endpoints, a `write` key reaches all of them. A `crnts_pat_` personal access token is accepted as a Bearer while its owner remains an eligible administrator in its active organization and the global rollout is enabled. Each operation requires the corresponding scope stored on that token."},"OAuth2":{"type":"oauth2","description":"An access token from the Currents authorization server, for an application acting as a user in one organization. Unlike an API key it is confined per endpoint: the operation lists the scope it needs, and a token that was not granted that scope gets 403.\n\n**The endpoints below are production's.** They are written out so this specification can be exercised against production, and they are wrong for every other deployment — a token minted by `id.currents.dev` is refused by a staging or on-premise API, whose `OAUTH_ISSUER` and `OAUTH_API_AUDIENCE` are its own. Against anything but production, and in a client generated from this document, discover them instead: `GET {api origin}/.well-known/oauth-protected-resource`, then the `authorization_servers[0]` document.\n\n**The audience named here is the REST API's.** `POST /mcp` is a second resource server on the same origin, identified by `{api origin}/mcp` and described by its own document at `/.well-known/oauth-protected-resource/mcp`. Every operation below refuses a token minted for it, and it refuses every token minted for these, so a client of this specification wants the audience above and not that one.","flows":{"authorizationCode":{"authorizationUrl":"https://id.currents.dev/api/auth/oauth2/authorize","tokenUrl":"https://id.currents.dev/api/auth/oauth2/token","refreshUrl":"https://id.currents.dev/api/auth/oauth2/token","scopes":{"projects:read":"See which projects exist and their names","projects:write":"Change project configuration, including a project's tags","results:read":"See runs, specs and failure detail — errors, stacks and artifacts","analytics:read":"See aggregate metrics: flake and failure rates, error counts, durations and trends","actions:read":"See quarantine and skip rules, and the tests they affect","actions:write":"Create, edit, enable, disable and archive quarantine and skip rules","issues:write":"Create and link issues in a connected issue tracker, and list its projects and issue types","runs:write":"Cancel and reset runs, and permanently delete runs and their artifacts","webhooks:read":"See webhook configuration, including destination URLs and headers","webhooks:write":"Create, edit and delete webhooks"}}}}},"parameters":{"HookIdPath":{"name":"hookId","in":"path","required":true,"description":"The webhook ID (UUID)","schema":{"type":"string","format":"uuid"}}},"schemas":{"WebhookResponse":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","enum":["OK"]},"data":{"$ref":"#/components/schemas/Webhook"}}},"Webhook":{"type":"object","required":["hookId","projectId","url","hookEvents"],"properties":{"hookId":{"type":"string","format":"uuid","description":"Unique identifier for the webhook"},"projectId":{"type":"string","description":"Project this webhook belongs to"},"url":{"type":"string","format":"uri","maxLength":2048,"description":"URL to send webhook POST requests to"},"headers":{"type":"string","nullable":true,"maxLength":4096,"description":"Custom headers as a JSON object string (e.g., {\"Authorization\": \"Bearer token\"})"},"hookEvents":{"type":"array","items":{"$ref":"#/components/schemas/HookEvent"},"description":"Events that trigger this webhook"},"label":{"type":"string","nullable":true,"maxLength":255,"description":"Human-readable label for the webhook"},"createdAt":{"type":"string","format":"date-time","nullable":true,"description":"When the webhook was created"},"updatedAt":{"type":"string","format":"date-time","nullable":true,"description":"When the webhook was last updated"}}},"HookEvent":{"type":"string","enum":["RUN_FINISH","RUN_START","RUN_TIMEOUT","RUN_CANCELED"],"description":"The event that triggers the webhook:\n- `RUN_FINISH`: Run completed (passed or failed)\n- `RUN_START`: Run started\n- `RUN_TIMEOUT`: Run timed out\n- `RUN_CANCELED`: Run was cancelled"},"ErrorResponse":{"type":"object","required":["status","error"],"properties":{"status":{"type":"string","enum":["FAILED"]},"error":{"type":"string","description":"Error message"},"code":{"type":"string","description":"Stable machine-readable discriminator, present on refusals a client is expected to branch on (for example `AI_CONSENT_REQUIRED`). The `error` text is free-form; branch on `code`."}}}},"responses":{"BadRequest":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Unauthorized":{"description":"Authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Forbidden":{"description":"The credential authenticated but is not permitted to use this endpoint. Four refusals share the status: a `read` API key on an endpoint that writes, an OAuth token that was not granted the scope the endpoint needs (`insufficient_scope`), an OAuth token that was granted it but whose holder no longer has a role permitting it (`insufficient_role`), and an OAuth token on an endpoint that accepts an API key and nothing else (`api_key_required`). Discriminate on `code` rather than on the message, which is free-form. The first refusal predates the codes and sets none. Only `insufficient_scope` carries the challenge header, because it is the only one a client can act on by re-authorizing.","headers":{"WWW-Authenticate":{"description":"Present on the `insufficient_scope` refusal only, naming the scope to re-authorize for (RFC 6750 3.1): `Bearer error=\"insufficient_scope\", scope=\"webhooks:read\"`. Absent on `insufficient_role`, where the scope is already granted, and on `api_key_required`, where no scope would help.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"NotFound":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"paths":{"/webhooks/{hookId}":{"get":{"summary":"Get webhook","description":"Get a specific webhook by ID. The hookId is globally unique, so projectId is not required.","operationId":"getWebhook","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/HookIdPath"}],"responses":{"200":{"description":"Webhook details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```

## Update webhook

> Update an existing webhook. Supports partial updates. The hookId is globally unique, so projectId is not required.

```json
{"openapi":"3.0.2","info":{"title":"Currents REST API","version":"1.0.0"},"tags":[{"name":"Webhooks","description":"Manage HTTP webhooks for projects.\n\nWebhooks allow you to receive HTTP POST notifications when certain events occur in your test runs:\n- **RUN_FINISH**: Run completed (passed or failed)\n- **RUN_START**: Run started\n- **RUN_TIMEOUT**: Run timed out\n- **RUN_CANCELED**: Run was cancelled\n\nWebhooks are useful for integrating with external tools like n8n, Zapier, or custom automation platforms."}],"servers":[{"url":"https://api.currents.dev/v1","description":"Production API"},{"url":"https://api-staging.currents.dev/v1","description":"Staging API"},{"url":"http://localhost:4000/v1","description":"Local development"}],"security":[{"BearerAuth":[]},{"OAuth2":["webhooks:write"]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"An organization API key, sent as `Authorization: Bearer <key>`. A key carries one scope for the whole API: a `read` key reaches the GET endpoints, a `write` key reaches all of them. A `crnts_pat_` personal access token is accepted as a Bearer while its owner remains an eligible administrator in its active organization and the global rollout is enabled. Each operation requires the corresponding scope stored on that token."},"OAuth2":{"type":"oauth2","description":"An access token from the Currents authorization server, for an application acting as a user in one organization. Unlike an API key it is confined per endpoint: the operation lists the scope it needs, and a token that was not granted that scope gets 403.\n\n**The endpoints below are production's.** They are written out so this specification can be exercised against production, and they are wrong for every other deployment — a token minted by `id.currents.dev` is refused by a staging or on-premise API, whose `OAUTH_ISSUER` and `OAUTH_API_AUDIENCE` are its own. Against anything but production, and in a client generated from this document, discover them instead: `GET {api origin}/.well-known/oauth-protected-resource`, then the `authorization_servers[0]` document.\n\n**The audience named here is the REST API's.** `POST /mcp` is a second resource server on the same origin, identified by `{api origin}/mcp` and described by its own document at `/.well-known/oauth-protected-resource/mcp`. Every operation below refuses a token minted for it, and it refuses every token minted for these, so a client of this specification wants the audience above and not that one.","flows":{"authorizationCode":{"authorizationUrl":"https://id.currents.dev/api/auth/oauth2/authorize","tokenUrl":"https://id.currents.dev/api/auth/oauth2/token","refreshUrl":"https://id.currents.dev/api/auth/oauth2/token","scopes":{"projects:read":"See which projects exist and their names","projects:write":"Change project configuration, including a project's tags","results:read":"See runs, specs and failure detail — errors, stacks and artifacts","analytics:read":"See aggregate metrics: flake and failure rates, error counts, durations and trends","actions:read":"See quarantine and skip rules, and the tests they affect","actions:write":"Create, edit, enable, disable and archive quarantine and skip rules","issues:write":"Create and link issues in a connected issue tracker, and list its projects and issue types","runs:write":"Cancel and reset runs, and permanently delete runs and their artifacts","webhooks:read":"See webhook configuration, including destination URLs and headers","webhooks:write":"Create, edit and delete webhooks"}}}}},"parameters":{"HookIdPath":{"name":"hookId","in":"path","required":true,"description":"The webhook ID (UUID)","schema":{"type":"string","format":"uuid"}}},"schemas":{"UpdateWebhookRequest":{"type":"object","properties":{"url":{"type":"string","format":"uri","maxLength":2048,"description":"URL to send webhook POST requests to"},"headers":{"type":"string","maxLength":4096,"nullable":true,"description":"Custom headers as a JSON object string (e.g., {\"Authorization\": \"Bearer token\"})"},"hookEvents":{"type":"array","items":{"$ref":"#/components/schemas/HookEvent"},"description":"Events that trigger this webhook"},"label":{"type":"string","minLength":1,"maxLength":255,"nullable":true,"description":"Human-readable label for the webhook"}}},"HookEvent":{"type":"string","enum":["RUN_FINISH","RUN_START","RUN_TIMEOUT","RUN_CANCELED"],"description":"The event that triggers the webhook:\n- `RUN_FINISH`: Run completed (passed or failed)\n- `RUN_START`: Run started\n- `RUN_TIMEOUT`: Run timed out\n- `RUN_CANCELED`: Run was cancelled"},"WebhookResponse":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","enum":["OK"]},"data":{"$ref":"#/components/schemas/Webhook"}}},"Webhook":{"type":"object","required":["hookId","projectId","url","hookEvents"],"properties":{"hookId":{"type":"string","format":"uuid","description":"Unique identifier for the webhook"},"projectId":{"type":"string","description":"Project this webhook belongs to"},"url":{"type":"string","format":"uri","maxLength":2048,"description":"URL to send webhook POST requests to"},"headers":{"type":"string","nullable":true,"maxLength":4096,"description":"Custom headers as a JSON object string (e.g., {\"Authorization\": \"Bearer token\"})"},"hookEvents":{"type":"array","items":{"$ref":"#/components/schemas/HookEvent"},"description":"Events that trigger this webhook"},"label":{"type":"string","nullable":true,"maxLength":255,"description":"Human-readable label for the webhook"},"createdAt":{"type":"string","format":"date-time","nullable":true,"description":"When the webhook was created"},"updatedAt":{"type":"string","format":"date-time","nullable":true,"description":"When the webhook was last updated"}}},"ErrorResponse":{"type":"object","required":["status","error"],"properties":{"status":{"type":"string","enum":["FAILED"]},"error":{"type":"string","description":"Error message"},"code":{"type":"string","description":"Stable machine-readable discriminator, present on refusals a client is expected to branch on (for example `AI_CONSENT_REQUIRED`). The `error` text is free-form; branch on `code`."}}}},"responses":{"BadRequest":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Unauthorized":{"description":"Authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Forbidden":{"description":"The credential authenticated but is not permitted to use this endpoint. Four refusals share the status: a `read` API key on an endpoint that writes, an OAuth token that was not granted the scope the endpoint needs (`insufficient_scope`), an OAuth token that was granted it but whose holder no longer has a role permitting it (`insufficient_role`), and an OAuth token on an endpoint that accepts an API key and nothing else (`api_key_required`). Discriminate on `code` rather than on the message, which is free-form. The first refusal predates the codes and sets none. Only `insufficient_scope` carries the challenge header, because it is the only one a client can act on by re-authorizing.","headers":{"WWW-Authenticate":{"description":"Present on the `insufficient_scope` refusal only, naming the scope to re-authorize for (RFC 6750 3.1): `Bearer error=\"insufficient_scope\", scope=\"webhooks:read\"`. Absent on `insufficient_role`, where the scope is already granted, and on `api_key_required`, where no scope would help.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"NotFound":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"paths":{"/webhooks/{hookId}":{"put":{"summary":"Update webhook","description":"Update an existing webhook. Supports partial updates. The hookId is globally unique, so projectId is not required.","operationId":"updateWebhook","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/HookIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWebhookRequest"}}}},"responses":{"200":{"description":"Webhook updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```

## Delete webhook

> Delete a webhook. The hookId is globally unique, so projectId is not required.

```json
{"openapi":"3.0.2","info":{"title":"Currents REST API","version":"1.0.0"},"tags":[{"name":"Webhooks","description":"Manage HTTP webhooks for projects.\n\nWebhooks allow you to receive HTTP POST notifications when certain events occur in your test runs:\n- **RUN_FINISH**: Run completed (passed or failed)\n- **RUN_START**: Run started\n- **RUN_TIMEOUT**: Run timed out\n- **RUN_CANCELED**: Run was cancelled\n\nWebhooks are useful for integrating with external tools like n8n, Zapier, or custom automation platforms."}],"servers":[{"url":"https://api.currents.dev/v1","description":"Production API"},{"url":"https://api-staging.currents.dev/v1","description":"Staging API"},{"url":"http://localhost:4000/v1","description":"Local development"}],"security":[{"BearerAuth":[]},{"OAuth2":["webhooks:write"]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"An organization API key, sent as `Authorization: Bearer <key>`. A key carries one scope for the whole API: a `read` key reaches the GET endpoints, a `write` key reaches all of them. A `crnts_pat_` personal access token is accepted as a Bearer while its owner remains an eligible administrator in its active organization and the global rollout is enabled. Each operation requires the corresponding scope stored on that token."},"OAuth2":{"type":"oauth2","description":"An access token from the Currents authorization server, for an application acting as a user in one organization. Unlike an API key it is confined per endpoint: the operation lists the scope it needs, and a token that was not granted that scope gets 403.\n\n**The endpoints below are production's.** They are written out so this specification can be exercised against production, and they are wrong for every other deployment — a token minted by `id.currents.dev` is refused by a staging or on-premise API, whose `OAUTH_ISSUER` and `OAUTH_API_AUDIENCE` are its own. Against anything but production, and in a client generated from this document, discover them instead: `GET {api origin}/.well-known/oauth-protected-resource`, then the `authorization_servers[0]` document.\n\n**The audience named here is the REST API's.** `POST /mcp` is a second resource server on the same origin, identified by `{api origin}/mcp` and described by its own document at `/.well-known/oauth-protected-resource/mcp`. Every operation below refuses a token minted for it, and it refuses every token minted for these, so a client of this specification wants the audience above and not that one.","flows":{"authorizationCode":{"authorizationUrl":"https://id.currents.dev/api/auth/oauth2/authorize","tokenUrl":"https://id.currents.dev/api/auth/oauth2/token","refreshUrl":"https://id.currents.dev/api/auth/oauth2/token","scopes":{"projects:read":"See which projects exist and their names","projects:write":"Change project configuration, including a project's tags","results:read":"See runs, specs and failure detail — errors, stacks and artifacts","analytics:read":"See aggregate metrics: flake and failure rates, error counts, durations and trends","actions:read":"See quarantine and skip rules, and the tests they affect","actions:write":"Create, edit, enable, disable and archive quarantine and skip rules","issues:write":"Create and link issues in a connected issue tracker, and list its projects and issue types","runs:write":"Cancel and reset runs, and permanently delete runs and their artifacts","webhooks:read":"See webhook configuration, including destination URLs and headers","webhooks:write":"Create, edit and delete webhooks"}}}}},"parameters":{"HookIdPath":{"name":"hookId","in":"path","required":true,"description":"The webhook ID (UUID)","schema":{"type":"string","format":"uuid"}}},"schemas":{"WebhookDeleteResponse":{"type":"object","required":["status","data"],"properties":{"status":{"type":"string","enum":["OK"]},"data":{"type":"object","required":["hookId","projectId"],"properties":{"hookId":{"type":"string","format":"uuid","description":"ID of the deleted webhook"},"projectId":{"type":"string","description":"Project the webhook belonged to"}}}}},"ErrorResponse":{"type":"object","required":["status","error"],"properties":{"status":{"type":"string","enum":["FAILED"]},"error":{"type":"string","description":"Error message"},"code":{"type":"string","description":"Stable machine-readable discriminator, present on refusals a client is expected to branch on (for example `AI_CONSENT_REQUIRED`). The `error` text is free-form; branch on `code`."}}}},"responses":{"BadRequest":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Unauthorized":{"description":"Authentication failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Forbidden":{"description":"The credential authenticated but is not permitted to use this endpoint. Four refusals share the status: a `read` API key on an endpoint that writes, an OAuth token that was not granted the scope the endpoint needs (`insufficient_scope`), an OAuth token that was granted it but whose holder no longer has a role permitting it (`insufficient_role`), and an OAuth token on an endpoint that accepts an API key and nothing else (`api_key_required`). Discriminate on `code` rather than on the message, which is free-form. The first refusal predates the codes and sets none. Only `insufficient_scope` carries the challenge header, because it is the only one a client can act on by re-authorizing.","headers":{"WWW-Authenticate":{"description":"Present on the `insufficient_scope` refusal only, naming the scope to re-authorize for (RFC 6750 3.1): `Bearer error=\"insufficient_scope\", scope=\"webhooks:read\"`. Absent on `insufficient_role`, where the scope is already granted, and on `api_key_required`, where no scope would help.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"NotFound":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"paths":{"/webhooks/{hookId}":{"delete":{"summary":"Delete webhook","description":"Delete a webhook. The hookId is globally unique, so projectId is not required.","operationId":"deleteWebhook","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/HookIdPath"}],"responses":{"200":{"description":"Webhook deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDeleteResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.currents.dev/api/resources/webhooks.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
