Events

The unified shape every inbound event takes in Nevo.

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

FieldTypeNotes
idstringULID. Stable across live + replay deliveries.
schemastringnevo.event.v1 today. Bumps on breaking changes.
typestringwebhook.received, email.received, slack.event, cron.fired.
originstringlive or replay. Use for idempotency.
created_atISO 8601Server-side ingest time.
project_idUUIDThe Nevo project this event belongs to.
channelobject{ id, label, type } — which channel received the event.
dataobjectSource-specific payload. Shape varies by type (below).
prompt_readystringLLM-friendly text rendering of the event. See prompt-ready.
prompt_ready_versionintRenderer 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 in body
  • text → raw string in body
  • base64 → original bytes base64-encoded in body_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.

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.com or @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_payload on 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.