Skip to content

External store contract

Gutternote sends one POST per operation to your endpoint. Implement the operations you want; return 501 for the rest and the widget hides those affordances.

Request

POST /gutternote HTTP/1.1
Content-Type: application/json
Gutternote-Key-Id: key_1
Gutternote-Signature: t=1767225600,v1=9f86d081...
{
"op": "append",
"threadId": "8f4c1e2a-0000-4000-8000-000000000001",
"handle": "cust_42",
"body": {
"Author": { "id": "guest_9a1", "kind": "guest", "displayName": "Dana Reed" },
"Markdown": "The CTA is 4px off.",
"Attachments": []
}
}

Operations

opReturnsNotes
create_thread{"handle": "..."}Also receives title and anchor. The handle is opaque to Gutternote and returned on every later call.
appenda body object
list{"bodies": [...]}Oldest first.
updatea body objectReceives bodyId.
delete204Receives bodyId.
delete_thread204Remove everything for the thread.

Status codes

  • 200/201 — success
  • 404 — unknown thread or body
  • 501 — operation not implemented; surfaces to the widget as unsupported
  • anything else >= 300 — treated as an upstream failure and returned to the client as 502, which the widget presents as retryable

Verifying the signature

The signature is HMAC-SHA256(secret, "<timestamp>.<raw body>"), hex encoded.

Reject timestamps more than a few minutes from your own clock — the timestamp is inside the signed material precisely so a captured request can’t be replayed indefinitely — and compare in constant time.

func verify(secret, header string, payload []byte) bool {
var ts, sig string
for _, part := range strings.Split(header, ",") {
k, v, _ := strings.Cut(part, "=")
switch k {
case "t":
ts = v
case "v1":
sig = v
}
}
unix, err := strconv.ParseInt(ts, 10, 64)
if err != nil || time.Since(time.Unix(unix, 0)).Abs() > 5*time.Minute {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(ts))
mac.Write([]byte("."))
mac.Write(payload)
return hmac.Equal([]byte(sig), []byte(hex.EncodeToString(mac.Sum(nil))))
}

Sign over the raw request bytes, not a re-serialised copy — key order will differ.