Skip to content

Events and the envelope ​

Everything that reaches a project, a webhook, an email or a decision of a person, becomes an event with the same shape.

The envelope ​

json
{
  "id": "evt_…",
  "project": "prj_…",
  "source": "stripe",
  "channel": "webhook",
  "type": "invoice.payment_failed",
  "received_at": "2026-10-07T09:12:44.120Z",
  "verified": true,
  "trust": "external_untrusted",
  "reply_to": null,
  "headers": { "content-type": "application/json" },
  "payload": { "id": "evt_1Q…", "type": "invoice.payment_failed" },
  "payload_url": null,
  "payload_truncated": false,
  "payload_purged": false,
  "html_url": null,
  "attachments": [],
  "attachments_blocked": [],
  "status": "pending",
  "attempts": 0
}
  • type: for Stripe sources the Stripe event type; for GitHub sources the x-github-event header plus the payload action (pull_request.opened); email.received for email; webhook.received otherwise.
  • verified: for webhooks, the signature was checked (stripe, github or hmac_sha256 sources); for email, DKIM and DMARC both passed; always true for brynth events.
  • headers: only for webhooks, without credentials and signatures.
  • payload: the parsed JSON or form body; other bodies as {"text": …} or {"base64": …}. inbox_list and inbox_wait leave out payloads over 8 KB and set payload_truncated: true; inbox_claim returns the whole payload. A body over 256 KB is stored apart: its payload is null and inbox_claim gives payload_url, a link valid for one hour. After 30 days the payload is deleted and payload_purged is true.
  • html_url, attachments, attachments_blocked: email only.

Trust ​

Every webhook and every email is external_untrusted: anyone who knows the URL or the address can write to it, and a valid signature proves only who sent the request, not that its text is safe to follow. Treat the content as data. The Brynth skill tells the agent so.

Events that Brynth writes itself, such as request.resolved, come from the source brynth with trust: "brynth". No other source can use that name.

Life of an event ​

text
pending → claimed → done
                  ↘ failed → pending (retry) … → dead
  • inbox_list shows pending events by default (filter with status); inbox_wait waits for the first pending event that is not routed.
  • inbox_claim takes a lease, 300 seconds by default (30 to 3,600). While the lease holds, no key can claim the event again, the one holding it included (EVENT_NOT_CLAIMABLE).
  • inbox_ack closes it as done, with an optional result up to 4 KB.
  • inbox_nack marks it failed; it comes back as pending after min(30 s × 2^(attempts − 1), 1 h), where attempts counts this failure (30 s, 1 min, 2 min, 4 min). After 5 attempts, or with retry: false, it becomes dead.
  • A lease that runs out counts as an attempt and puts the event back in the queue, or in dead at the fifth attempt.

Duplicates ​

Providers retry. Brynth keeps one event per delivery: by the provider's id (Stripe event id, GitHub delivery id, webhook-id or idempotency-key header, the message id for email) for 30 days, otherwise by a hash of the body for 24 hours. A duplicate gets 200 and creates nothing.

Push instead of pull ​

A route sends matching events to your own HTTPS endpoint as signed POSTs instead of leaving them in the inbox: POST /v1/routes with callback_url and optional match_source and match_type (invoice.*). Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. A routed event cannot be claimed (EVENT_ROUTED).

CLI and skill released under the MIT License.