Skip to content

Commit 51e28c4

Browse files
authored
Merge pull request #30 from photon-hq/spectrum-webhook-dashboard-mention
docs(webhooks): developer-experience polish
2 parents 3bf6101 + 84ad22d commit 51e28c4

5 files changed

Lines changed: 111 additions & 42 deletions

File tree

docs-src/webhooks/events.mdx.vel

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,11 @@ title: Events
33
description: The exact wire format Spectrum sends — headers, body, and what each field contains
44
---
55

6-
import { TypeTooltip } from "/snippets/type-tooltip.mdx";
7-
8-
{% set space = symbol("ts:spectrum-ts#Space") %}
9-
{% set user = symbol("ts:spectrum-ts#User") %}
10-
11-
{# Note: We intentionally do NOT use TypeTooltip for `Content` or
12-
`InboundMessage` because:
13-
- `Content = z.infer<typeof contentSchema>` resolves to a Zod inference
14-
reference that's unhelpful to customers.
15-
- `InboundMessage` is generic and the extractor returns an empty signature.
16-
The Space and User types are plain interfaces that extract cleanly. #}
6+
{# We deliberately do NOT use TypeTooltip on the Space/User/Message types here.
7+
The SDK interfaces include methods (`.send()`, `.edit()`, `.getMessage()`,
8+
etc.) that don't survive `JSON.stringify` — showing them as the type of a
9+
webhook payload field is actively misleading. The serialized shapes on the
10+
wire are simpler: `{ id, platform }` for Space and User. #}
1711

1812
In the [Quickstart](/webhooks/quickstart), a real delivery flew past in `console.log`. This page is the spec — every header and every field your handler will see on every request, slowed down and labelled.
1913

@@ -92,8 +86,8 @@ This is the only event currently emitted. It fires once per inbound message that
9286
| Field | Type | Description |
9387
| --- | --- | --- |
9488
| `event` | `"messages"` | Discriminator. Always `"messages"` for this payload. |
95-
| `space` | <TypeTooltip name="Space" type={`{{ space.signature }}`} /> | The conversation context. See [Space](#space) below. |
96-
| `message` | object | The inbound message. See [Message](#message) below. |
89+
| `space` | `{ id, platform }` | The conversation context. See [Space](#space) below. |
90+
| `message` | `object` | The inbound message. See [Message](#message) below. |
9791

9892
#### Space
9993

@@ -102,7 +96,7 @@ This is the only event currently emitted. It fires once per inbound message that
10296
| `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. |
10397
| `platform` | `string` | The platform that owns this space. See [Providers](/spectrum-ts/providers) for the current set of values; new platforms add new values without breaking existing payloads. |
10498

105-
The `space.id` matches the `space.id` you'd see from the [`spectrum-ts` SDK](/spectrum-ts/spaces-and-users). To send a reply, pass it to <TypeTooltip name="Space" type={`{{ space.signature }}`} />`.send(...)` from a separately-running SDK instance — there is no public HTTP send-message endpoint today.
99+
The `space.id` matches the `space.id` you'd see from the [`spectrum-ts` SDK](/spectrum-ts/spaces-and-users)pass it to `Space.send(...)` from a separately-running SDK instance to reply. There is no public HTTP send-message endpoint today.
106100

107101
#### Message
108102

@@ -112,8 +106,8 @@ The `space.id` matches the `space.id` you'd see from the [`spectrum-ts` SDK](/sp
112106
| `platform` | `string` | The platform that sourced the message. Same value as `space.platform`. |
113107
| `direction` | `"inbound"` | Always `"inbound"` — outbound messages are not delivered as webhooks. |
114108
| `timestamp` | `string` | ISO 8601 UTC timestamp from the platform (when the user sent it). |
115-
| `sender` | <TypeTooltip name="User" type={`{{ user.signature }}`} /> | The user who sent the message — `{ id, platform }`. The `id` format is platform-defined (e.g. for iMessage it's the E.164 phone number `+15551234567`; for WhatsApp Business it's the WA contact id). |
116-
| `space` | <TypeTooltip name="Space" type={`{{ space.signature }}`} /> | A copy of the top-level `space` field, denormalized for convenience. |
109+
| `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). |
110+
| `space` | `{ id, platform }` | A copy of the top-level `space` field, denormalized for convenience. |
117111
| `content` | object | The message content. Shape depends on the message type — see [Content shapes](#content-shapes) below. |
118112

119113
#### Idempotency: the `message.id` rule

webhooks/delivery.mdx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@ description: How Spectrum decides when to retry, when to give up, and what your
55

66
You know what arrives ([Events](/webhooks/events)) and how to prove it's real ([Verifying signatures](/webhooks/verifying-signatures)). This page picks up the moment *after* the worker computes a signature and starts the `POST` to your URL — what it does when your server is fast, slow, broken, or unreachable. The contract is simple but worth knowing exactly, because it determines how fault-tolerant you need to be on your end.
77

8-
## The contract in one paragraph
8+
## The contract at a glance
99

10-
Spectrum tries to deliver each event for up to four attempts within a ~6.2 second window. Each attempt has a 10-second per-request timeout. Retries happen on `5xx`, `408`, `429`, network errors, and our own timeouts; other `4xx` codes mean "give up." Successful deliveries acknowledge with any `2xx`. After the budget is exhausted the event is dropped from the worker's memory — there is no durable retry queue. Customers who care about every event must dedupe and tolerate occasional misses.
10+
- **Strong retry behaviour.** Up to 4 attempts per event, with backoff on `5xx`, `408`, `429`, network errors, and worker-side timeouts. The vast majority of deliveries land on attempt 1; the retries are there for the occasional bad minute on your side.
11+
- **Fast acknowledgement.** Any `2xx` ends it — the worker stops as soon as your server says ok.
12+
- **Fast permanent failure.** Other `4xx` codes (`400`/`401`/`404`/etc.) are treated as fatal — we don't waste your retry budget when the request will never succeed.
13+
- **Bounded budget.** 10-second per-attempt timeout, ~6.2 second total across all retries. If your server is still down after that, the event is logged and the worker moves on.
14+
- **At-least-once delivery.** A retry after your server timed out can re-deliver an event you already processed — always dedupe in your handler (see [Be idempotent](#be-idempotent) below).
15+
16+
This is a bounded-retry contract, not zero-loss delivery. If your use case requires *every* event regardless of downtime (financial audit, transactional state machines), pair webhooks with periodic reconciliation against the [Spectrum API](/api-reference/introduction) — covered later on this page.
1117

1218
## What the worker does on each attempt
1319

webhooks/managing-webhooks.mdx

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,47 @@ Project credentials are scoped to a single project. They never expire — rotate
2828

2929
## Register a webhook
3030

31-
```sh
31+
<CodeGroup>
32+
```sh curl
3233
curl -X POST "https://spectrum.photon.codes/projects/$PROJECT_ID/webhooks/" \
3334
-u "$PROJECT_ID:$PROJECT_SECRET" \
3435
-H "Content-Type: application/json" \
3536
-d '{"webhookUrl":"https://your-app.com/spectrum-webhook"}'
3637
```
3738

39+
```ts JavaScript
40+
const auth = Buffer.from(`${PROJECT_ID}:${PROJECT_SECRET}`).toString('base64');
41+
42+
const res = await fetch(
43+
`https://spectrum.photon.codes/projects/${PROJECT_ID}/webhooks/`,
44+
{
45+
method: 'POST',
46+
headers: {
47+
Authorization: `Basic ${auth}`,
48+
'Content-Type': 'application/json',
49+
},
50+
body: JSON.stringify({ webhookUrl: 'https://your-app.com/spectrum-webhook' }),
51+
}
52+
);
53+
54+
const { data } = await res.json();
55+
console.log('signingSecret:', data.signingSecret); // save this — only shown once
56+
```
57+
58+
```py Python
59+
import httpx, os
60+
61+
res = httpx.post(
62+
f"https://spectrum.photon.codes/projects/{os.environ['PROJECT_ID']}/webhooks/",
63+
auth=(os.environ['PROJECT_ID'], os.environ['PROJECT_SECRET']),
64+
json={"webhookUrl": "https://your-app.com/spectrum-webhook"},
65+
)
66+
67+
data = res.json()["data"]
68+
print("signingSecret:", data["signingSecret"]) # save this — only shown once
69+
```
70+
</CodeGroup>
71+
3872
Response (`200 OK`):
3973

4074
```json
@@ -70,11 +104,36 @@ The `signingSecret` is **only returned in this response**. There is no `GET` end
70104

71105
## List registered webhooks
72106

73-
```sh
107+
<CodeGroup>
108+
```sh curl
74109
curl -u "$PROJECT_ID:$PROJECT_SECRET" \
75110
"https://spectrum.photon.codes/projects/$PROJECT_ID/webhooks/"
76111
```
77112

113+
```ts JavaScript
114+
const auth = Buffer.from(`${PROJECT_ID}:${PROJECT_SECRET}`).toString('base64');
115+
116+
const res = await fetch(
117+
`https://spectrum.photon.codes/projects/${PROJECT_ID}/webhooks/`,
118+
{ headers: { Authorization: `Basic ${auth}` } }
119+
);
120+
121+
const { data } = await res.json();
122+
console.log(data); // array of { id, webhookUrl, createdAt, updatedAt }
123+
```
124+
125+
```py Python
126+
import httpx, os
127+
128+
res = httpx.get(
129+
f"https://spectrum.photon.codes/projects/{os.environ['PROJECT_ID']}/webhooks/",
130+
auth=(os.environ['PROJECT_ID'], os.environ['PROJECT_SECRET']),
131+
)
132+
133+
print(res.json()["data"]) # list of { id, webhookUrl, createdAt, updatedAt }
134+
```
135+
</CodeGroup>
136+
78137
Response:
79138

80139
```json
@@ -105,12 +164,32 @@ The list response **does not include `signingSecret`**. It's only ever returned
105164

106165
## Delete a webhook
107166

108-
```sh
167+
<CodeGroup>
168+
```sh curl
109169
curl -X DELETE \
110170
-u "$PROJECT_ID:$PROJECT_SECRET" \
111171
"https://spectrum.photon.codes/projects/$PROJECT_ID/webhooks/6a4d2e8c-7b1f-4d3a-9a8e-2c5d6f7e8a9b/"
112172
```
113173

174+
```ts JavaScript
175+
const auth = Buffer.from(`${PROJECT_ID}:${PROJECT_SECRET}`).toString('base64');
176+
177+
await fetch(
178+
`https://spectrum.photon.codes/projects/${PROJECT_ID}/webhooks/${webhookId}/`,
179+
{ method: 'DELETE', headers: { Authorization: `Basic ${auth}` } }
180+
);
181+
```
182+
183+
```py Python
184+
import httpx, os
185+
186+
httpx.delete(
187+
f"https://spectrum.photon.codes/projects/{os.environ['PROJECT_ID']}/webhooks/{webhook_id}/",
188+
auth=(os.environ['PROJECT_ID'], os.environ['PROJECT_SECRET']),
189+
)
190+
```
191+
</CodeGroup>
192+
114193
Response:
115194

116195
```json

webhooks/overview.mdx

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,13 @@ The next six pages build on each other. Skim straight to the topic you need, or
4040
5. **[Managing webhooks](/webhooks/managing-webhooks)** — operate at scale: register, list, delete, and rotate signing secrets via the API or the dashboard.
4141
6. **[Troubleshooting](/webhooks/troubleshooting)** — common symptoms, root causes, and fixes when something goes wrong.
4242

43-
<Tip>
44-
**Three ways to register and manage webhooks** — pick whichever fits how you work:
43+
## Three ways to manage webhooks
4544

46-
- **Dashboard.** Open your workspace at [app.photon.codes/dashboard](https://app.photon.codes/dashboard) and use the **Webhook** tab to add, list, and remove endpoints with point-and-click. No terminal, no auth headers — friendly enough to hand to a non-engineer.
47-
- **Interactive API reference.** Every endpoint is live in the [API reference](/api-reference/introduction) — paste your project credentials once and fire `List webhooks`, `Register webhook`, or `Delete webhook` straight from your browser. Fastest way to confirm credentials work and sanity-check a URL.
48-
- **`curl` or any HTTP client.** The terminal flow used throughout this guide and the [Quickstart](/webhooks/quickstart). Scriptable, automatable, CI-friendly.
45+
You can register, list, and delete webhooks through any of three surfaces — same three operations under the hood, different ergonomics:
4946

50-
Same three operations under the hood — just three different surfaces over them.
51-
</Tip>
47+
- **[Dashboard](https://app.photon.codes/dashboard).** Open your workspace and use the **Webhook** tab to add and remove endpoints with point-and-click. No terminal, no auth headers — the friendliest entry point, and fine to hand to a non-engineer.
48+
- **[Interactive API reference](/api-reference/introduction).** Every endpoint runs straight from your browser. Paste your project credentials once and fire `List webhooks`, `Register webhook`, or `Delete webhook`. Fastest way to confirm credentials work or sanity-check a URL.
49+
- **`curl` or any HTTP client.** The terminal flow used in the [Quickstart](/webhooks/quickstart) and the rest of this guide. Scriptable, automatable, CI-friendly.
5250

5351
## When to use webhooks
5452

@@ -96,9 +94,7 @@ Four ideas cover everything else in these docs:
9694

9795
## Currently emitted events
9896

99-
| Event | Header value | Payload shape |
100-
| --- | --- | --- |
101-
| Inbound message | `X-Spectrum-Event: messages` | `{ event, space, message }` — see [Events](/webhooks/events) |
97+
Today there is one event: **`messages`**. Each delivery carries `X-Spectrum-Event: messages` and a body of shape `{ event, space, message }` — see [Events](/webhooks/events) for every field.
10298

10399
The set will grow (reactions, typing indicators, custom provider events). New event types are additive: existing handlers that ignore unknown values keep working without changes.
104100

@@ -113,16 +109,10 @@ Your signing secret is returned exactly once, in the response of `POST /webhooks
113109
## Where to next
114110

115111
<Columns cols={2}>
116-
<Card title="Quickstart" icon="rocket" href="/webhooks/quickstart">
117-
Register a URL, verify a signature, receive your first event in 5 minutes.
112+
<Card title="Start: Quickstart" icon="rocket" href="/webhooks/quickstart">
113+
Wire up a URL, verify a signature, receive a real message — five minutes, end-to-end.
118114
</Card>
119-
<Card title="Events" icon="rss" href="/webhooks/events">
120-
The exact wire format — headers, body, and what each field contains.
121-
</Card>
122-
<Card title="Verifying signatures" icon="shield-halved" href="/webhooks/verifying-signatures">
123-
The security model with complete verifier code for Node, Bun, and Python.
124-
</Card>
125-
<Card title="Delivery and retries" icon="repeat" href="/webhooks/delivery">
126-
Retry policy, timeouts, idempotency, and what HTTP status codes mean to us.
115+
<Card title="Skim: How it works" icon="diagram-project" href="#how-it-works">
116+
The mechanics first, in case you'd rather build a mental model before touching code.
127117
</Card>
128118
</Columns>

webhooks/verifying-signatures.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Verifying signatures
3-
description: Confirm each delivery is genuine, unmodified, and recent — copy-paste verifier code for Node, Bun, and Python
3+
description: Confirm each delivery is genuine, unmodified, and recent — copy-paste verifier code for Node, Bun, Python, and Go
44
---
55

66
You saw the verifier as a copy-paste in the [Quickstart](/webhooks/quickstart), and you saw the `X-Spectrum-Signature` header itself in [Events](/webhooks/events). This page is the *why* — what each line of that verifier is doing and how to port it to a different stack.

0 commit comments

Comments
 (0)