Graine AI

Create or replace a webhook subscription

Subscribe an endpoint to call events.

Subscribe an endpoint to call events.

The storage key is (organization, agent_id, callback_url), so POSTing the same agent and the same URL twice UPDATES the existing subscription rather than creating a twin that would deliver every event twice. That is what makes this endpoint safe to call from an idempotent provisioning script.

Scope. With agent_id set, it must be an agent in your organisation — any other value answers 404 — and the subscription applies to that agent only. Without it (or with an empty string) it becomes the organisation-wide default. Resolution is an OVERRIDE, not a union: if an agent has any enabled subscription of its own, only those are consulted for that agent's calls — even for an event none of them asked for. An agent whose only subscription is paused falls back to the organisation-wide default.

Events. Valid event types: all_processing_completed, call_completed, call_corrected, 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". An unknown event type is rejected here rather than silently dropped.

Secrets and headers are write-only. The signing secret and every header value are stored encrypted and are never returned by any endpoint — reads give you has_secret, a "***" preview, and the header NAMES. Keep your own copy of the secret.

The callback URL is checked twice. It is refused here if it points at a private, loopback, link-local, metadata or reserved address. A hostname is accepted without a DNS lookup, and the delivery engine re-checks the RESOLVED addresses before every send — so a URL accepted here can still be refused later, which shows up in the ledger as suppressed_reason: "url_rejected".

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 (bad event type, method, header or URL), 401, 403, 404 (agent_id is not an agent in your organisation), 422, 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.

POST
/v2/webhooks
/v2/webhooks

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_url
Required
Callback Url

Public https endpoint we POST the event to.

Minimum length: 1Maximum length: 2048

agent_idAgent Id | null

Scope this subscription to one agent. Omit it (or send an empty string) for the organisation-wide default.

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

POST or PUT.

Default: "POST"

headersHeaders | null

Extra request headers, at most 20. Values are stored encrypted and are NEVER returned. Content-Type, Content-Length, Host and any X-Graine-* name are refused.

secretSecret | null

HMAC-SHA256 signing secret. Sent back only as '***'; keep your own copy. Omit it to fall back to the organisation signing secret.

enabledEnabled

Set false to create it paused.

Default: true

Query Parameters

organization_idOrganization Id

Must match the organisation the API key belongs to.

Response Body

201

The stored subscription. Header values and the secret are never returned.

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

@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

@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 POST "https://api.graine.ai/v2/webhooks?organization_id=string" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "callback_url": "https://example.com/hooks/graine",
    "agent_id": "6c6f4cc3-f56c-479d-8422-7f76694daa29",
    "event_types": [
      "call_completed"
    ],
    "method_type": "POST",
    "headers": {
      "Authorization": "Bearer your-token"
    },
    "secret": "whsec_exampleSigningSecretDoNotUse",
    "enabled": true
  }'

The stored subscription. Header values and the secret are never returned.

{
  "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"
}