webhooks.create
Register a webhook endpoint
POST/webhooks
Register an address to be POSTed to when work finishes, so an integration can stop polling. The signing secret is in the response and nowhere else. The URL is checked at registration AND again before every delivery — https outside development, no username or password, no fragment, no non-default port in production, and every address it resolves to must be public. The connection is then pinned to the address that was checked, and a redirect is refused rather than followed. At most 10 active endpoints per account.
curl 'https://api.gogoscreen.com/api/v1/webhooks' \
-X POST \
-H "Authorization: Bearer $GOGOSCREEN_API_KEY" \
-H "Idempotency-Key: webhooks-create-mubixu8b" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/gogoscreen",
"events": [
"render.succeeded",
"render.failed"
],
"description": "Production receiver"
}'{
"secret": "whsec_EXAMPLE_SECRET_SHOWN_ONCE",
"endpoint": {
"id": "3f2b0c1a-9d4e-4a61-b8c2-000000000014",
"url": "https://hooks.example.com/gogoscreen",
"events": [
"render.succeeded",
"render.failed"
],
"description": "Production receiver",
"status": "active",
"failureStreak": 0,
"disabledAt": null,
"disabledReason": null,
"lastDeliveryAt": null,
"createdAt": "2026-09-21T11:19:00.000Z",
"updatedAt": "2026-09-21T11:19:00.000Z"
}
}At a glance
| Fact | Detail |
|---|---|
| Scopes | webhooks:manage |
| Credentials | a signed in dashboard session or an API key. |
| Rate limit | 120 requests a minute per credential, in the write class. |
| Idempotency | Required. Send an Idempotency-Key header; a retry with the same key and the same body returns the stored answer with Idempotent-Replayed: true. How idempotency works. |
| MCP tool | webhooks_create |
| Always sets | Cache-Control: private, no-store |
| Verified email | Required. An account whose email address is not verified meets 403 email_unverified before the operation runs. |
| Audit | Every call is written to the audit log as webhook_management. |
Request body
A JSON body is required. Unknown fields are refused rather than ignored, so a typo is a 400 validation_failed rather than a setting that silently did nothing.
| Name | Type | Required | Description |
|---|---|---|---|
| events | string[] | Required | What to send. At least one; an unknown name is refused rather than ignored, because an endpoint subscribed to a typo would silently never fire.1–16 items |
| url | string | Required | Where to POST. https, a public address, no credentials in the URL.max 2048 characters |
| description | string | Optional | What this endpoint is for. Shown in the console and in the account's audit trail.max 200 characters |
201 Created
Register a webhook endpoint
| Name | Type | Description | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| endpoint | object | |||||||||||||||||||||||||||||||||||||
11 fields inside endpoint
| ||||||||||||||||||||||||||||||||||||||
| secret | string | THE SIGNING SECRET. This is the only time it is returned: it is stored encrypted and is never answered again. Verify every delivery with it — Gogoscreen-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>." + raw body)> — over the RAW request body, and reject a t more than five minutes from your clock. | ||||||||||||||||||||||||||||||||||||
| Header | Meaning |
|---|---|
Cache-Control | Always private, no-store. |
Idempotent-Replayed | Present only when this answer was stored by an earlier call with the same Idempotency-Key. Nothing new was done. |
Errors
Every refusal is { error: { code, message, details?, requestId } }. Branch on code, never on the sentence. The full catalogue is at Errors.
| Code | Status | When it happens | What to do |
|---|---|---|---|
| idempotency_key_required | 400 | A mutating operation arrived over HTTP with no Idempotency-Key header. It is refused before the body is parsed, so a caller missing both learns about both at once. | Add an Idempotency-Key header of up to 128 characters, unique to this request. Over MCP the same value goes in the idempotencyKey tool argument. |
| validation_failed | 400 | The merged path, query and body did not match the operation's schema. The schemas are strict, so an unknown field is a failure rather than something ignored, and a string carrying half of a character is refused before it can be stored. | Read details.issues. Each entry carries a path, a message and a machine readable code. Fix every named field and send the request again; the same body always fails the same way. |
| webhook_host_denied | 400 | The webhook host is on this deployment's denylist. The list matches a bare hostname and its subdomains. | Point the endpoint at a host that is not on the list, or ask the operator. |
| webhook_url_blocked | 400 | The webhook URL resolves to an address this API will not post to, or its hostname does not resolve at all. Every resolved address must be public, so one private answer among several is refused. | Point the endpoint at a public address. The fence runs again on every delivery attempt, so an address that changes later stops being delivered to. |
| webhook_url_invalid | 400 | The webhook URL is not a shape this API will post to: not https in production, a username or password in the URL, a fragment, a non default port, a single label hostname, or over 2048 characters. | Use an https URL with a full public hostname, no credentials, no fragment and the default port. details.reason names which rule it broke. |
| unauthorized | 401 | No credential was presented, or the key is unknown, revoked or expired, or an X-API-Key header carried something that is not an API key. details.reason says which. | Send Authorization: Bearer gsk_live_…. A revoked or expired key never starts working again, so issue a new one in the developer console. Put a dashboard session token in Authorization, never in X-API-Key. |
| email_unverified | 403 | The operation declares a verified email and the owning account's address is not verified. A key reads the account's current fact, not the fact that was true when the key was issued. | Verify the address on the account, then retry. The key does not need to be reissued. |
| forbidden_scope | 403 | The credential does not carry every scope the operation declares, or it is the wrong kind of credential for it. keys.* and assistant.* are user only and can never be called by a key. | details.requiredScopes and details.missingScopes name what is missing. A key's scopes cannot be changed after it is issued, so create a new key with them. When details.allowedActors is present, no key can call this operation at all. |
| not_found | 404 | No resource of that id belongs to this account. A resource that belongs to somebody else answers 404 as well, never 403. | Check the id. Do not treat this as a permissions problem. |
| conflict | 409 | The request conflicts with the state the resource is in, and no more specific code applies. | Re-read the resource and decide from its current state. Retrying the same request unchanged will not help. |
| idempotency_conflict | 409 | That idempotency key has already been used on this operation with a different request body. | Use a fresh key for a different request. A key stands for one intent, not for one attempt. |
| idempotency_in_progress | 409 | The first request under that key is still being worked. Two concurrent identical requests both reach the store and one of them is told this. | Wait the Retry-After seconds, which is 2, and send exactly the same request again with the same key. Do not change the body and do not mint a new key. |
| too_many_endpoints | 409 | The account already holds ten active webhook endpoints, which is the cap. | Delete or disable one, then register the new one. details.max and details.active carry the numbers. |
| payload_too_large | 413 | The request body is over the transport's limit: 256 kb over HTTP, 1 mb over MCP. | Send less. details.limit carries the limit. Version 1 has no upload operations, so a body this size is usually a mistake. |
| unsupported_media_type | 415 | The request carried a body whose Content-Type is not application/json, whose charset is not utf-8, or whose Content-Encoding this API does not decode. A body nobody can read would otherwise be silently discarded. | Send Content-Type: application/json and utf-8 bytes. details names the charset, encoding or content type that was objected to. |
| rate_limited | 429 | The credential's per minute budget, the account's per minute ceiling, or a daily cap was exceeded. details.scope is minute, day or account, or global when the product's own daily cap on voice previews is spent. | Wait the Retry-After seconds and retry. details.limit and details.resetAt say what was hit and when it reopens. The RateLimit-* headers on every answer let a client pace itself before it gets here. |
| internal_error | 500 | An unhandled failure inside this API. The original message is logged and never answered. | Retry once, then quote requestId to support. A 5xx deletes the idempotency claim rather than storing it, so retrying under the same key is safe. |