# Notifications

Your server hears what happens in your customers’ accounts, from a failed run to a new connection, as signed requests.

Use them to email a customer, open a ticket, or count finished runs as they happen. This is everything your server can hear about your customers. Inside your page, the SDK’s [events](https://docs.gorunner.io/embed/events) say what happens in the open embed. Event streaming isn’t for customer accounts.

## Add a destination

1. Open **Platform › Notifications** and press **Add a destination…**.
2. Enter the **Address** your server listens on, starting with `https://`, and a **Description**.
3. Under **Events to send**, tick what it should hear. None is ticked for you.
4. Press **Add the destination** and enter your two-factor code.
5. Press **Copy the signing secret**. You won’t see it again. The dialog also shows code that checks each request.

![Platform › Notifications: a destination that hears 3 events, its last 24 hours, and the Deliveries log below.](https://docs.gorunner.io/img/embed/notifications-light.jpg)

Each row says what it hears and how its last 24 hours went, with **Send a test**. Its menu has **Change…**, **Rotate secret…**, **Replay failed since…**, **Turn off** and **Delete…**. An installation has up to 20 destinations.

**What if my server is on a private network?**

A destination must be a public address. One on the installation’s own machine or private network works only when its operator allows it with `OUTBOUND_ALLOW_PRIVATE`.

## The events

- `run.failed`: A production run of a customer’s workflow failed. A sub-flow’s failure is part of the run that called it.
- `run.completed`: A production run worked. Marked **High volume**: there can be many a day.
- `approval.waiting`: A run is waiting for someone to approve or reject.
- `workflow.created`: Made new, copied, imported, from a template, by the assistant or by your server.
- `workflow.changed`: Saved, a version brought back, or its variables, tags or folder changed. At most one every 5 minutes for each workflow.
- `workflow.published`: Switched on, or a new version published.
- `workflow.unpublished`: Switched off.
- `workflow.deleted`: Gone from the customer’s account.
- `connection.created`: Added, by a person or your server.
- `connection.changed`: Renamed, or signed in again.
- `connection.removed`: Removed.
- `connection.expired`: The service stopped accepting it. It needs signing in again.

A destination hears only what happens after it was added, and only in your customers’ accounts. Nothing new is noticed in an account while it is suspended or deleted. While the license lacks Embed, nothing is sent; deliveries still within 36 hours go out when it returns.

**Do I get new event types after an update?**

No. An update never ticks an event for you. All but `run.failed`, `approval.waiting` and `connection.expired` arrived in v2026.10.4. The dialog marks the newer ones **New**; tick each on the destinations that should hear it. A destination hears it from then on, never what happened before.

## The request

A `POST` with a JSON body in the [Standard Webhooks](https://www.standardwebhooks.com) shape. No header carries GoRunner’s name.

```json
{
  "id": "6f1c2a90-4b1e-4c55-9b7e-2e0d61f0a2c4",
  "type": "connection.expired",
  "timestamp": "2026-10-01T10:12:40Z",
  "data": {
    "test": false,
    "account": { "id": "acct-bakery", "name": "Bakery Ltd" },
    "workspace": { "id": null },
    "user": { "id": "ada-123" },
    "connection": {
      "id": "c0d1e2f3-0000-4000-8000-000000000001",
      "name": "Invoices sheet",
      "piece": { "id": "googlesheets", "name": "Google Sheets" },
      "managed": false,
      "externalId": null,
      "expiredAt": "2026-10-01T10:12:40Z"
    },
    "route": "/connections?connection=c0d1e2f3-0000-4000-8000-000000000001"
  }
}
```

**What each event carries**

- **Every event**: `account` and `user` carry your ids. `workspace` is `null` for the default one, and `user` is `null` when your server or your staff did it. `test` is `true` only on a sample. `route` is the page to open: pass `{ path: route }` to `mount()` or `navigate()`.
- `run.failed`, `run.completed`: `workflow` and `run`: `id`, `status`, `trigger`, `startedAt`, `firstStartedAt`, `finishedAt`, `durationMs`. `status` is `worked` on `run.completed`. `startedAt` is when the run was accepted, `firstStartedAt` when a worker first started it (`null` on a run from v2026.10.5 or earlier), and `durationMs` counts from it. A failed run adds `error`: `message`, cut at 2,000 bytes, and `step`.
- `approval.waiting`: `workflow`, `run` and `approval`: `step`, `title`, `message`, `expiresAt`, the title and message cut at 1,000 bytes. It never carries the Approve and Reject links.
- `workflow.*`: `workflow`: `id`, `name`, `status` (`draft`, `live` or `paused`; `null` once deleted), `updatedAt`, `version`. `workflow.changed` adds `changes` (`saved`, `version_restored`, `variables`, `tags`, `folder`), names the last person who made one, and arrives 30 seconds to 5½ minutes after the change.
- `connection.*`: `connection`: `id`, `name`, `piece`, and `managed` and `externalId` for one your server manages.
- **Who and when**: The `workflow.*` events, and `connection.created`, `.changed` and `.removed`, carry `by: { kind, user }` (`person`, `partner`, `assistant`, `staff`, `token` or `system`) and `occurredAt`.

An event never carries a workflow’s definition or a credential, but it does carry your customers’ names.

## Check a request is genuine

Each request has three headers: `webhook-id`, the same on every try; `webhook-timestamp`, in Unix seconds; and `webhook-signature`, `v1,` then an HMAC-SHA256 of `id.timestamp.body` with your signing secret (`whsec_…`).

```js
import { Webhook } from "standardwebhooks"

const wh = new Webhook(process.env.GORUNNER_SIGNING_SECRET)
// Throws if the signature is wrong, or the timestamp is
// more than 5 minutes from your clock. rawBody: the body
// exactly as it arrived, before any parsing.
const event = wh.verify(rawBody, {
  "webhook-id": req.headers["webhook-id"],
  "webhook-timestamp": req.headers["webhook-timestamp"],
  "webhook-signature": req.headers["webhook-signature"],
})
```

**Rotate secret…** gives a new secret, and the old one keeps working for 24 hours. Meanwhile each request carries both signatures, so a checker with either secret passes.

## Delivery and retries

- **First try**: Within about 15 seconds of the event.
- **Delivered**: Any answer from 200 to 299, within 15 seconds.
- **Retried**: Anything else, a redirect included, 7 more times over about 28 hours: after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours. Then it is **Failed**.
- **410 Gone**: Turns the destination off at once. Nothing is sent to it, not even a test, until you turn it on. What failed meanwhile isn’t resent.
- **Order**: Not guaranteed. Skip an event whose `webhook-id` you already handled, and drop one older than what you have, by `occurredAt` or `workflow.updatedAt`.

**Deliveries** keeps every delivery for 30 days, with each try and the first 512 bytes of its answer. Open one to see what was sent, and press **Replay** to send it again. **Replay failed since…** resends up to 1,000, never one already sent again. Deleting an account deletes its deliveries. Staff read a customer’s full audit trail on **Platform › Audit log**. A replay keeps the original `webhook-id`, so your server can tell it already did the work.

## Related

- [Events and the protocol](https://docs.gorunner.io/embed/events): What your page hears from the open embed
- [Security](https://docs.gorunner.io/embed/security): Check every request, and the rest of your checklist
- [Platform screens](https://docs.gorunner.io/embed/platform-screens): The Notifications page, part by part

Source: https://docs.gorunner.io/embed/notifications
