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.1Content-Type: application/jsonGutternote-Key-Id: key_1Gutternote-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
op | Returns | Notes |
|---|---|---|
create_thread | {"handle": "..."} | Also receives title and anchor. The handle is opaque to Gutternote and returned on every later call. |
append | a body object | |
list | {"bodies": [...]} | Oldest first. |
update | a body object | Receives bodyId. |
delete | 204 | Receives bodyId. |
delete_thread | 204 | Remove everything for the thread. |
Status codes
200/201— success404— unknown thread or body501— operation not implemented; surfaces to the widget as unsupported- anything else
>= 300— treated as an upstream failure and returned to the client as502, 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.