Every inbound event — webhook, email, future source — reaches your service as the same object: a Nevo Event. Branching on the single type field is how you route by kind.
Schema
{
"id": "evt_01hk8…",
"schema": "nevo.event.v1",
"type": "webhook.received",
"origin": "live",
"created_at": "2026-04-17T19:22:04.123Z",
"project_id": "…",
"channel": { "id": "…", "label": "stripe events", "type": "webhook" },
"data": { /* source-specific payload */ },
"prompt_ready": "Stripe invoice.paid — $42.00 from acme.co. …",
"prompt_ready_version": 2
}
Fields
| Field | Type | Notes |
|---|---|---|
id | string | ULID. Stable across live + replay deliveries. |
schema | string | nevo.event.v1 today. Bumps on breaking changes. |
type | string | webhook.received, email.received, slack.event, cron.fired. |
origin | string | live or replay. Use for idempotency. |
created_at | ISO 8601 | Server-side ingest time. |
project_id | UUID | The Nevo project this event belongs to. |
channel | object | { id, label, type } — which channel received the event. |
data | object | Source-specific payload. Shape varies by type (below). |
prompt_ready | string | LLM-friendly text rendering of the event. See prompt-ready. |
prompt_ready_version | int | Renderer version stamp. Bumps when we change the renderer. |
data shape per type
webhook.received
{
"method": "POST",
"path": "/v1/webhook/abc…",
"headers": { "content-type": "application/json", "stripe-signature": "…" },
"body_encoding": "json",
"body": { /* parsed JSON body when Content-Type was JSON */ },
"body_raw_b64": "",
"body_size_bytes": 512
}
body_encoding tells you where the body lives:
json→ parsed payload inbodytext→ raw string inbodybase64→ original bytes base64-encoded inbody_raw_b64
slack.event
{
"team_id": "T0123456",
"team_name": "Acme",
"envelope_id": "Ev09876",
"event_type": "app_mention",
"event_ts": "1713536200.123456",
"channel_id": "C01GENERAL",
"user_id": "U0XYZ789",
"text": "<@U0BOTUSR> summarise today's stripe charges",
"thread_ts": "",
"raw": { /* the full outer Slack event preserved verbatim */ }
}
cron.fired
{
"schedule_id": "88888888-8888-8888-8888-888888888888",
"cron_expr": "0 14 * * THU",
"timezone": "America/New_York",
"scheduled_at": "2026-04-23T18:00:00Z",
"fired_at": "2026-04-23T18:00:02Z",
"payload": { /* user-attached static JSON */ }
}
fired_at - scheduled_at tells you if the scheduler was lagged — useful for observability.
email.received
{
"resend_email_id": "re_…",
"message_id": "<abc@mail.example>",
"from": "alice@acme.com",
"to": ["support@in.nevo.sh"],
"matched_address": "support@in.nevo.sh",
"cc": [],
"bcc": [],
"subject": "Re: API limits",
"text": "can we bump our monthly quota?",
"html": "<p>…</p>",
"attachments": [{ "filename": "usage.csv", "content_type": "text/csv", "size_bytes": 1024 }],
"received_at": "2026-04-17T19:22:03.999Z"
}
Live vs replay
origin is live for freshly-ingested events and replay when an operator re-delivered a historical event from the dashboard.
Search
The dashboard’s events list has a full-text search that matches substring
queries against the event’s prompt_ready, summary, and source label:
- Identifier lookups:
evt_01hk8,cus_AbcDef,#42. - Email addresses:
acme@foo.comor@acme.co. - Source / provider names:
stripe,github,support-inbox.
Searches combine with the usual source and status filters. See Events view for the dashboard walkthrough.
Delivery
The same event object is pushed to two places:
- SDK clients connected for the project.
- HTTP endpoints the project has configured, signed with the channel’s secret.
Either failing doesn’t fail the other. A handler that takes five minutes to process doesn’t block other events — each event gets its own task.
Failed endpoint deliveries retry on an exponential schedule per endpoint policy. After the policy is exhausted the delivery lands in the DLQ. SDK deliveries are at-least-once with auto-reconnect; there is no separate retry tier for SDK drops — use dashboard replay when your client misses events.
Rules can intervene
Before delivery, rules you’ve configured can:
- Drop the event — SDK clients and endpoints never see it. The event row stays in the DB with
status="dropped"so the dashboard shows which rule matched. - Transform fields in
data— redact secrets, uppercase subjects, etc. Only the projection SDKs + endpoints receive changes;raw_payloadon the stored event is untouched for audit. - Route to specific endpoints — overrides the default fan-out.
- Tag the event for downstream search.
Your handler only sees the post-rule shape. If a rule transforms data.email to lowercase, your handler gets lowercase — full stop.