In the Quickstart, a real delivery flew past inDocumentation Index
Fetch the complete documentation index at: https://docs.photon.codes/docs/llms.txt
Use this file to discover all available pages before exploring further.
console.log. This page is the spec — every header and every field your handler will see on every request, slowed down and labelled.
The body’s space, message, sender, and content fields are the same shapes you’d see from the spectrum-ts SDK — with one important difference: function-typed properties (.read(), .stream(), .reply(), .react(), etc.) are stripped before serialization, since functions can’t survive JSON.stringify.
Anatomy of a delivery
platform value are decided by each provider — not by Spectrum. The example above is real prod output for an iMessage delivery; WhatsApp Business and any future platform will use their own conventions. Don’t pattern-match on these strings; use them as opaque identifiers.
Headers
| Header | Value | Notes |
|---|---|---|
Content-Type | application/json | Always. The body is UTF-8 JSON. |
User-Agent | spectrum-webhook/<version> | Identifies the worker. Useful for IP/UA allow-listing. |
X-Spectrum-Event | Event type, e.g. messages | Mirrors the event field in the body. Lets you route without parsing the body first. |
X-Spectrum-Webhook-Id | UUID of the registered webhook | Identifies which of your URLs this delivery is for. Useful with multiple registrations and required for idempotency keys. |
X-Spectrum-Timestamp | UNIX epoch seconds at signing time | Required to verify the signature. Also reject deliveries older than ~5 minutes for replay protection. |
X-Spectrum-Signature | v0=<64-char hex> | HMAC-SHA256 of v0:{timestamp}:{rawBody} keyed by the webhook’s signing secret. See Verifying signatures. |
HTTP headers are case-insensitive. Most frameworks normalize to lowercase (
x-spectrum-event); use whichever your framework returns.Body shape
The body is a JSON object. Theevent field is a discriminator — every other field’s shape depends on which event you’re handling.
event: "messages" payload
This is the only event currently emitted. It fires once per inbound message that lands for your project.
Space
| Field | Type | Description |
|---|---|---|
id | string | Opaque, stable identifier for the conversation. Format varies by platform and space type — treat it as a string you store and pass back unchanged. For iMessage DMs, looks like any;-;+<E.164>; for groups, a chat GUID. |
platform | string | The platform that owns this space. See Providers for the current set of values; new platforms add new values without breaking existing payloads. |
space.id matches the space.id you’d see from the spectrum-ts SDK — pass it to Space.send(...) from a separately-running SDK instance to reply. There is no public HTTP send-message endpoint today.
Message
| Field | Type | Description |
|---|---|---|
id | string | Stable opaque identifier for the message. The current format is spc-msg-<uuid> but treat it as opaque — use it for idempotency, don’t parse it. |
platform | string | The platform that sourced the message. Same value as space.platform. |
direction | "inbound" | Always "inbound" — outbound messages are not delivered as webhooks. |
timestamp | string | ISO 8601 UTC timestamp from the platform (when the user sent it). |
sender | { id, platform } | The user who sent the message. The id format is platform-defined (for iMessage it’s the E.164 phone number +15551234567; for WhatsApp Business it’s the WA contact id). |
space | { id, platform } | A copy of the top-level space field, denormalized for convenience. |
content | object | The message content. Shape depends on the message type — see Content shapes below. |
Idempotency: the message.id rule
A single inbound message always carries the same message.id across every delivery it produces, no matter how many webhook URLs you have registered. If you have two URLs registered for one project and a message arrives, both URLs receive a POST in parallel — and both bodies have the same message.id. If a delivery is retried (after a 5xx or timeout on your side), the retry also carries the same message.id.
That makes message.id the right dedup key when one downstream consumer handles every webhook for the project:
Content shapes
content is a discriminated union tagged by type. The two shapes verified end-to-end against prod today are:
mimeType field is the discriminator for what kind of attachment it is:
mimeType prefix | Kind | Example values |
|---|---|---|
image/* | Photo or image attachment | image/heic, image/jpeg, image/png |
audio/* | Voice memo or audio file | audio/mp4, audio/x-m4a |
video/* | Video clip | video/mp4, video/quicktime |
application/* | Document or file | application/pdf, application/zip |
Content type defines additional arms (reaction, richlink, poll, contact, etc.) that may appear in future webhook deliveries. See Spectrum content types for the canonical list. If a new arm ships, your default: switch arm is what catches it gracefully — see the snippet below.
What you don’t get
A few things that may be in the SDK’sMessage type but are intentionally not in the webhook payload:
- Methods like
.reply()or.react(). They depend on a live SDK connection. To respond, runspectrum-tsin a separate process and callspace.send(...)against thespace.idyou got from the webhook. There is no HTTP send endpoint yet. - Internal provider state. Things like raw protocol headers, retry hints, and message acknowledgements are stripped before serialization.
- Outbound messages. Webhooks deliver inbound only. A message you sent does not echo back as a webhook.
Forward compatibility
The set of events will grow. To stay forward-compatible:-
Branch on
eventdefensively. Use aswitchwith adefaultarm that returns2xxand logs the unknown event. Never crash on unknown values. - Treat extra fields as unknown but tolerable. New optional fields may appear in existing payload shapes. They’ll never repurpose existing fields.
-
Don’t subscribe to specific event types. Today every URL receives every event. If a
subscriptionsfield lands in the future, the default will continue to be “all events” so existing webhooks keep working.
Quick reference card
Where to next
You now know exactly what arrives at your URL. The next two chapters cover trusting it and handling failures when something goes wrong.Verifying signatures
The four details every verifier has to get exactly right, with copy-paste implementations in four languages.
Delivery and retries
Retry policy, timeouts, idempotency, and what HTTP status codes mean to the worker.