Graine AI

Update a webhook subscription

Change one subscription.

Change one subscription. Only the fields you send are touched.

Three behaviours to know before you use it:

  • headers REPLACES the whole map. Send the complete set you want, or {} to clear it. There is no per-header patch, because the stored values are encrypted and cannot be read back to merge against.
  • secret: send a new value to rotate it, or an empty string to remove it and fall back to the organisation signing secret. It is never returned.
  • Changing callback_url issues a new subscription_id. The URL is part of the storage key that keeps you from being subscribed twice to the same endpoint, so a new URL is a new row; the old one is removed for you. Always read the subscription_id out of the response rather than assuming it survived.

Pausing is the reversible half of deleting: enabled: false keeps the URL, headers, secret and event list, and stops every delivery until you set it back. A pause is applied directly and never rewrites anything else.

Any other change re-writes the stored document. If the stored secret or a stored header value can no longer be decrypted (secret_unavailable), it cannot be carried forward and must be re-supplied in the same request.

The API runs several processes, each with its own 60-second subscription cache. A change is live immediately for the process that served this request and within 60 seconds everywhere else.

Errors: 400, 401, 403, 404, 422 (nothing to update), 429, 503.

Rate limit: 1000 requests per minute per organization (bucket default). Exceeding it returns 429 with Retry-After; the X-RateLimit-* response headers report your remaining allowance on every call.

PATCH
/v2/webhooks/{subscription_id}
/v2/webhooks/b91d2f77-3ea5-4c58-9d10-6f8b4a2c7e31

The Authorization access token

Authorization

Authorization
Required
Bearer <token>

Your Graine API key. Create one in the dashboard under Developers, or via POST /v2/api-keys. Send it as Authorization: Bearer <key>.

In: header

Request Body

application/jsonRequired

callback_urlCallback Url | null

Changing this issues a NEW subscription_id: the callback URL is part of the storage key.

Minimum length: 1Maximum length: 2048

event_typesEvent Types | null

Valid event types: all_processing_completed, call_completed, call_corrected, call_ringing, call_started, client_analysis_completed, platform_analysis_completed, recording_completed. Omitting event_types, or sending an empty list, resolves to all_processing_completed — there is no value that means "every event".

method_typeMethod Type | null

POST or PUT.

headersHeaders | null

REPLACES the stored header map. Send {} to clear it.

secretSecret | null

Replaces the signing secret. Send an empty string to remove it.

enabledEnabled | null

Pause or resume this subscription.

Path Parameters

subscription_id
Required
Subscription Id

The subscription_id returned on create.

Query Parameters

organization_idOrganization Id

Must match the organisation the API key belongs to.

Response Body

200

The stored subscription after the change.

subscription_id
Required
Subscription Id

Stable id for this subscription.

organization_id
Required
Organization Id

Always the organisation the API key belongs to.

agent_idAgent Id | null

The agent this subscription is scoped to, or null for the organisation-wide default.

scope
Required
Scope

'agent' when agent_id is set, 'organization' for the org-wide default. An agent that has ANY enabled subscription of its own uses only those; the org-wide default is a fallback, not a union.

event_types
Required
Event Types

Resolved event list — never null, never empty.

@minItems 0

@minItems 0

callback_url
Required
Callback Url

The endpoint we POST to.

method_type
Required
Method Type

POST or PUT.

headers
Required
Headers

Configured header names, values masked as '***'. Values are never returned.

header_names
Required
Header Names

The same header names as a sorted list, for clients that want it flat.

@minItems 0

@minItems 0

enabled
Required
Enabled

A disabled subscription receives nothing.

has_secret
Required
Has Secret

True when a signing secret is configured.

secretSecret | null

Masked preview ('***') when a secret is set, otherwise null.

secret_unavailable
Required
Secret Unavailable

True when the stored secret can no longer be decrypted. Deliveries go out UNSIGNED while this is true and your signature check will never pass — PATCH a fresh secret to fix it.

created_atCreated At | null

ISO-8601 UTC.

updated_atUpdated At | null

ISO-8601 UTC.

created_byCreated By | null

Who created it.

400

1000 — malformed input the schema could not reject: an unknown event type, a method other than POST/PUT, a reserved header name, or a callback_url pointing at a private, loopback or metadata address.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

401

1100 — missing, unknown or inactive API key. 1101 — a browser session token was presented instead of a gat_ key.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

403

1102 — the key is valid but lacks the scope this endpoint requires, or names another organisation. GET /v2/scopes reports which scopes the key holds.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

404

1200 — no such subscription or delivery in this organisation. Cross-tenant reads answer 404, never 403.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

422

1001 — the body or query failed validation; the message names the first offending field.

error
Required
integer

Stable integer code from the error table. Branch on this, not on the message.

message
Required
string

One human-readable sentence. Wording may change; the code will not.

429

1300 — per-organisation rate limit exceeded. Carries Retry-After.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

503

1501 — the webhook subscription store is unreachable.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

504

1502 — an upstream did not respond in time.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

curl -X PATCH "https://api.graine.ai/v2/webhooks/b91d2f77-3ea5-4c58-9d10-6f8b4a2c7e31?organization_id=string" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "callback_url": "https://example.com/hooks/graine",
    "event_types": [
      "call_completed",
      "recording_completed"
    ],
    "method_type": "POST",
    "headers": {
      "X-Tenant": "acme"
    },
    "secret": "whsec_exampleSigningSecretDoNotUse",
    "enabled": true
  }'

The stored subscription after the change.

{
  "subscription_id": "b91d2f77-3ea5-4c58-9d10-6f8b4a2c7e31",
  "organization_id": "string",
  "agent_id": "6c6f4cc3-f56c-479d-8422-7f76694daa29",
  "scope": "agent",
  "event_types": [
    "all_processing_completed"
  ],
  "callback_url": "https://example.com/hooks/graine",
  "method_type": "POST",
  "headers": {
    "Authorization": "***"
  },
  "header_names": [
    "X-Tenant"
  ],
  "enabled": true,
  "has_secret": true,
  "secret": "***",
  "secret_unavailable": false,
  "created_at": "2026-08-20T13:06:01.724Z",
  "updated_at": "2026-08-20T13:27:49.073Z",
  "created_by": "dev_1a2b3c4d5e6f7a8b"
}