Skip to content

Custom event envelope

The custom events webhook accepts one event per request. The custom events guide shows how to set it up; this page is the precise contract.

POST /webhook/custom
Authorization: Bearer xat_…
Content-Type: application/json

The full URL is on Settings → Organisation, in the Custom Events card; for gritz.dev it is https://gritz.dev/webhook/custom. The webhook only handles POST.

  • The request must carry Authorization: Bearer <token>. A request without it gets 401 bearer token required, even from a browser with a signed-in session: the webhook doesn’t accept session cookies.
  • The token is an API key (xat_…) from the Keys page. A wrong, deleted or expired key gets 401 invalid or expired token.
  • The token must hold the event:create scope, or the request gets 403 permission denied. API keys hold every scope. A task’s own token does not hold event:create.
  • The event belongs to the key’s organisation and is checked against that organisation’s custom event types. It has no actor, so only the organisation’s public routing rules can match it.

A JSON object of at most 1 MiB:

{
"type": "alert",
"url": "https://alerts.example.com/alerts/4211",
"body": "checkout-api error rate above 5% for 10 minutes",
"attrs": {"severity": ["critical"], "service": ["checkout-api"]},
"details": {"runbook": "https://wiki.example.com/runbooks/checkout-api"}
}

Fields other than the five below are rejected, as are fields of the wrong JSON type.

Field Type Required Validation
type string yes Must name one of the organisation’s custom event types, as defined on Events → Custom Schemas. A type named like a built-in GitHub or Jira type still only resolves to the custom one.
url string yes Must be non-empty and an absolute http or https URL with a host. Only GitHub and Jira URLs are normalised: see url.
body string no The event’s text. Defaults to the empty string.
attrs object of string → array of strings no Every key must be an attribute the type declares, other than body and url.
details object of string → string no Not validated.

The custom type’s name, which matches ^[a-z][a-z0-9_]{0,63}$. The type’s label becomes the event’s description on the task’s timeline.

The resource the event is about, and the key that routes it: an event wakes the tasks subscribed to its url, and a task created by the event is subscribed to it.

gritz normalises GitHub and Jira issue URLs to one key per issue, as it does for their native events, so a custom event about a GitHub pull request or a Jira issue wakes the task subscribed to it:

url Key
https://github.com/{owner}/{repo}/issues/{n} or /pull/{n}, with any further path segments, query or fragment https://github.com/{owner}/{repo}/issues/{n} or /pull/{n}
https://api.github.com/repos/{owner}/{repo}/issues/{n} or /pulls/{n}, with any further path segments, query or fragment https://github.com/{owner}/{repo}/issues/{n} or /pull/{n}
https://{site}.atlassian.net/browse/{KEY}, with any further path segments, query or fragment https://{site}.atlassian.net/browse/{KEY}
https://{site}.atlassian.net/rest/api/{version}/issue/{KEY}, with any further path segments, query or fragment https://{site}.atlassian.net/browse/{KEY}

The host must match exactly (github.com, not www.github.com), and the key always uses https. A Jira REST URL with a numeric issue id instead of a key, and a GitHub API comment URL (/repos/{owner}/{repo}/issues/comments/{id}), are not normalised.

Every other URL is used exactly as sent, so events about the same resource must send the same URL byte for byte.

The event’s text, passed to the agent. Rules match on it through the body attribute every custom type declares.

The values routing rule conditions match on, keyed by attribute. Each value is a list of strings; a condition matches when any value in the list satisfies it. An attribute the type declares may be omitted, in which case conditions on it don’t match. Sending body or url here is an error: send them as the top-level fields, from which their attributes are derived.

Free-form key/value strings stored with the event and shown with it to the agent and in the Web UI. Rules can’t match on them.

A valid event gets 200 OK with a JSON body:

{"routed": 1}

routed is the number of tasks the event was delivered to: tasks created by the matching rule, or existing tasks subscribed to the event’s url. It is 0 when no public rule for the type matches, or when a rule matches but no task is subscribed to the url and the rule doesn’t create tasks.

Errors are plain text.

Status Body Cause
200 {"routed": n} The event was valid and routed to n tasks, possibly none.
400 invalid envelope: … The body is not a JSON object matching the envelope: invalid JSON, an unknown field, a field of the wrong type, or more than 1 MiB.
400 unknown event type "…" type is empty or not one of the organisation’s custom types.
400 url is required url is missing or empty.
400 url "…" must be an absolute http or https URL url is relative, has no host, or has another scheme.
400 attr "…" must be sent as the envelope's "…" field attrs has a body or url key.
400 attr "…" not declared by event type "…" attrs has a key the type doesn’t declare.
401 bearer token required No Authorization: Bearer header.
401 invalid or expired token The bearer token is not a valid, unexpired API key or gritz token.
403 permission denied The token lacks the event:create scope.
500 internal error The server failed to load the custom types or to route the event.