Alerts
An alert tells you that something in a workspace needs attention, without anyone watching the console. Seamward sends it to a destination you add: a webhook your service receives, or an email to members of the workspace.
An alert carries only what the console already shows: the kind of event, the workspace, the connection, one summary sentence, and a link. It never contains payloads, headers, credentials, or record identifiers.
Events
| Event | Sent when |
|---|---|
incident.opened | An incident is opened for a connection. |
incident.resolved | An incident recovers and stays healthy through verification, or your team resolves it. |
connection.quiet | A Live connection has sent nothing for longer than its own history says is normal. |
repair.awaiting_approval | A proposed repair passed validation and is waiting for your approval. |
Each event is sent once per destination. An incident that stays open, or a connection that stays quiet, does not alert again.
A destination chooses which events it receives and for which modes. New destinations default to Live only, because Sandbox connections are often quiet or failing on purpose.
Add a destination
Owners and admins manage destinations in Workspace settings, then Alerts. Viewers can see the list and recent deliveries. A workspace can have up to ten destinations.
- Select Add destination.
- Choose Webhook or Email and give it a name.
- Enter the URL, or choose the recipients.
- Choose the events and modes, then select Add destination.
- Select Send test on the new destination, then open Recent deliveries to confirm it arrived.
On a workspace with no destinations, Email owners and admins creates an email destination for every event in Live in one step.
Webhook destinations
The URL must be a public HTTPS address. Addresses on local, private, or internal networks are refused, both when you add the destination and again each time an alert is sent.
When you add a webhook, Seamward shows its signing secret once. Store it where your receiver can read it. It cannot be shown again; to replace it, delete the destination and add a new one. Afterwards the console shows only the URL's host.
Email destinations
Email goes to members of the workspace with a verified address, never to an outside address:
- Workspace owners and admins follows the workspace. Whoever holds either role when an alert is sent receives it.
- Chosen members sends to up to ten members you pick.
Each recipient gets a separate message. A member who leaves the workspace stops receiving alerts immediately.
Receive a webhook
Seamward sends a POST with a JSON body and three headers:
| Header | Value |
|---|---|
x-seamward-signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
x-seamward-event | The event type, for example incident.opened. |
x-seamward-alert-id | The alert's id. It is the same on every attempt for that event. |
Code example
{ "schemaVersion": "seamward.alert/1", "id": "alr_3f9c2a7d41e85b06c9d2e7f4a1b83c50", "type": "incident.opened", "occurredAt": "2026-10-05T10:45:00.000Z", "workspace": { "slug": "acme", "name": "Acme" }, "mode": "live", "connection": { "id": "int_8Hq2mPz4", "name": "Order webhooks" }, "summary": "3 order outcomes missing after successful order.create events", "url": "https://seamward.com/app/acme/incidents/inc_5tR9wLk2?environment=live"}| Field | Type | Notes |
|---|---|---|
schemaVersion | string | Always seamward.alert/1 for this format. |
id | string | Stable for the event. Use it to ignore an alert you have already handled. |
type | string | One of the four events above, or test for a test alert. |
occurredAt | string | When the event happened, as an ISO 8601 UTC timestamp. |
workspace | object | The workspace's slug and name. |
mode | string | live or sandbox. |
connection | object | The connection's id and name. Absent on a test alert. |
summary | string | The sentence the console shows for the incident, silence, or repair. Up to 500 characters. |
url | string | Where to review it in the console. |
Verify the signature
Verify every request before you act on it. The signature is an HMAC-SHA256 of the timestamp, a full stop, and the exact request body, keyed with the destination's signing secret.
Three things matter: use the raw body bytes as received (parsing and re-serializing JSON changes them), compare in constant time, and reject old timestamps so a captured request cannot be replayed.
Code example
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 300;
export function isSeamwardAlert( rawBody: string, signatureHeader: string, signingSecret: string,): boolean { const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(signatureHeader); if (!match) return false; const timestamp = Number(match[1]); if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;
const expected = createHmac("sha256", signingSecret) .update(`${timestamp}.${rawBody}`, "utf8") .digest(); const given = Buffer.from(match[2]!, "hex"); return given.length === expected.length && timingSafeEqual(given, expected);}A handler that uses it, with Node's built-in server so the raw body is never touched:
Code example
import { createServer } from "node:http";
createServer(async (request, response) => { const chunks: Buffer[] = []; for await (const chunk of request) chunks.push(chunk as Buffer); const rawBody = Buffer.concat(chunks).toString("utf8");
const signature = request.headers["x-seamward-signature"]; if ( typeof signature !== "string" || !isSeamwardAlert( rawBody, signature, process.env.SEAMWARD_ALERT_SIGNING_SECRET!, ) ) { response.writeHead(401).end(); return; }
const alert = JSON.parse(rawBody) as { id: string; type: string; url: string; }; // Hand the alert to your own queue or pager here, keyed on alert.id. response.writeHead(204).end();}).listen(8080);If your framework parses JSON for you, configure it to keep the raw body for this route.
Respond, and what is retried
Answer with any 2xx status within 5 seconds. Do slow work after you have answered.
| Your response | What Seamward does |
|---|---|
2xx | Records the alert as delivered. |
408, 429, any 5xx, timeout, no answer | Tries again, up to six attempts over about four minutes. |
| Any other status, including a redirect | Records the alert as failed. It is not retried. |
A retry carries the same id, so a receiver that has already handled it can answer 2xx and do nothing. Redirects are never followed. A destination that keeps failing is not paused automatically.
Test, pause, and review deliveries
- Send test queues a
testalert through the same path as a real one, so a test that arrives proves the URL, the signature, and delivery. - Recent deliveries lists the last 20 alerts for a destination with their outcome, the number of attempts, and the reason for a failure. Delivery records are kept for 30 days.
- The Sending switch pauses a destination without deleting it. Nothing is sent to a paused destination, and events that happen while it is paused are not sent later.
Troubleshooting
| Recent deliveries shows | What to check |
|---|---|
| The destination answered with a status | Your receiver refused the request. A 401 usually means the signing secret or the raw-body handling is wrong. |
| The destination did not answer within 5 seconds | Answer first, then do the work. Check that the URL is reachable from the public internet. |
| The address resolved to a private network | The URL's name points at a private or internal address. Use a publicly reachable address. |
| The destination's name could not be resolved | The URL's host name does not exist or its DNS is failing. |
| No verified workspace member was available | An email destination has no recipient left. Check roles and email verification under Members. |
| At least one recipient's address was rejected | The mail service refused an address. The other recipients still received the alert. |
What is not available
Webhook and email are the two destination types today. Destinations for chat, paging, and ticketing tools are planned and are not available. A webhook can reach those tools through your own receiver.
