Go Scribi

Webhooks

When something happens in a workspace, Go Scribi can tell another system about it — so a build can start when a card reaches a column, or an index can refresh when a note changes, instead of asking every few minutes whether anything has happened yet.

Setting one up

Settings → Integrations → Webhooks, which the owner and admins can reach. Give it an address, tick the events you want, and send yourself a test event before anything real happens. The address has to be on the public internet, and a redirect is a failure rather than something we follow: a signed delivery goes where you said or nowhere.

The events

EventWhen
note.createda note is made, however it was made — including a diary's day
note.updatedits words, its list, its board or its map change
note.deletedit goes to the bin, or is destroyed for good
note.restoredit comes back out of the bin
card.moveda card changes column, with whether that column finishes

There is no card.completed, and that is deliberate: a card is done when it is in the column that means done, so completion is a move. card.moved carries completes, which is true when the card entered the finishing column and false when it left it.

How much a delivery says

Each subscription chooses one of three, and the choice is about what leaves this application — not about convenience. Everything a delivery carries is in somebody else's logs a second later, where we cannot take it back.

SettingCarriesUse it when
Just what changed
the default
Identifiers, versions, times, who did it. Your receiver has an API key and can fetch what it needs. Nothing can be disclosed that it could not already read.
And the title The above, plus data.title. A chat channel, where "note 218 changed" is not a message anybody wants to read.
And the note itself The above, plus exactly what GET /notes/:id answers with — body, rows, columns or nodes, by type. A mirror or an index that would otherwise fetch every note it is told about. No second request, and no way back.

A title on a deleted note still arrives, because we keep it for the thirty days a delivery log lives — a deletion nobody can name is the one event that most needs naming. The whole note does not: there is nothing left to send, and the payload says "gone": true.

What a delivery looks like

The same event, at each setting. These are generated by the code that does the sending, so they are the shapes you will actually receive.

Just what changed

POST /your/endpoint
Content-Type: application/json
X-Scribi-Event: note.updated
X-Scribi-Delivery: dl_4123
X-Scribi-Timestamp: 1790048461
X-Scribi-Signature: v1=6f1c…

{
  "version": 1,
  "event": "note.updated",
  "id": "ev_9012",
  "delivery": "dl_4123",
  "workspace_id": 4,
  "occurred_at": "2026-11-07T01:18:58Z",
  "actor": {
    "kind": "member",
    "id": 7,
    "name": "Kate"
  },
  "data": {
    "note_id": 218,
    "note_type": "markdown",
    "folder_id": 12,
    "version": 34
  }
}

And the title

{
  "version": 1,
  "event": "note.updated",
  "id": "ev_9012",
  "delivery": "dl_4123",
  "workspace_id": 4,
  "occurred_at": "2026-11-07T01:18:58Z",
  "actor": {
    "kind": "member",
    "id": 7,
    "name": "Kate"
  },
  "data": {
    "note_id": 218,
    "note_type": "markdown",
    "folder_id": 12,
    "version": 34,
    "title": "Harbour survey"
  }
}

And the note itself

{
  "version": 1,
  "event": "note.updated",
  "id": "ev_9012",
  "delivery": "dl_4123",
  "workspace_id": 4,
  "occurred_at": "2026-11-07T01:18:58Z",
  "actor": {
    "kind": "member",
    "id": 7,
    "name": "Kate"
  },
  "data": {
    "note_id": 218,
    "note_type": "markdown",
    "folder_id": 12,
    "version": 34,
    "title": "Harbour survey"
  },
  "note": {
    "id": 218,
    "title": "Harbour survey",
    "type": "markdown",
    "folder": {
      "id": 12,
      "name": "Estuary"
    },
    "diary_date": null,
    "subject": null,
    "created_at": "2026-11-04T01:18:58Z",
    "updated_at": "2026-11-07T01:18:58Z",
    "lock_version": 34,
    "url": "https://goscribi.com/w/4/notes/218"
  },
  "properties": {},
  "body": "The count was the highest since the survey began.\n"
}

version at the top is the shape of the payload. data.version is the note's own version, which is what to fetch by: if you ask the API for the note and get a higher one, you already have something newer than this event describes. At the third setting the note is read when the delivery is sent rather than when the event happened, so a retry an hour later carries what the note says then — and its lock_version tells you which that is.

Proving it came from us

Every delivery is signed with your subscription's secret, shown on its own page. The signature is an HMAC-SHA256 over the timestamp, a full stop, and the exact body you received — verify it against the raw body, before parsing.

expected = "v1=" + hmac_sha256(secret, timestamp + "." + raw_body)
reject unless constant_time_equal(expected, signature_header)
reject if now - timestamp > 300

The timestamp is inside what is signed, so a delivery captured off the wire cannot be posted at you an hour later. Refuse anything older than about five minutes.

Replacing the secret takes effect immediately, and deliveries signed with the old one will start failing at your end — change it there first if you can.

At least once, which means duplicates

A delivery that fails is tried again — six attempts spread over about an hour — and any delivery can be sent again by hand from the log. Every attempt carries the same id and a new delivery.

So keep a note of the event ids you have handled and ignore repeats. This is not a precaution against a bug in our sending; it is what at-least-once delivery means, and a receiver that assumes otherwise will do the same work twice sooner or later.

When it stops

Loops

If your receiver writes back through the API, its own writing becomes an event, which comes straight back to it. On the subscription's page you can name the application your integration signs in as, and we will not tell it about its own changes. The ceiling above is the backstop for the loops that rule cannot see.

What is not built