API: append lanes (sgit.ai)
The six append endpoints) the write-only vault-to-vault message transport. Four separated capabilities, the blind write response, server-assigned sortable filenames, idempotent mark-processed, and the limits.
Source: https://sgit.ai/api/append-lanes.html↗ · site v0.6.8 · this file is generated from the same content as the page, so the two cannot drift. Every page on this site has a .md twin; internal links below point at them.
Append lanes
A write-only channel attached to a vault, gated by a token the writer holds and nothing else. This is the transport behind vault-to-vault messaging↗ and behind sg.append↗ in the browser bridge, and it is deliberately generic: the same primitive carries messages, logs, signals, control messages and state flows.
Naming. This API was called inbox before v0.32.7. Every /api/vault/inbox/* URL is gone, and purge takes folder:"pending" or "processed", the old "inbox" value returns 400. If you are reading older material, translate.
The six endpoints
All POST, all under /api/vault/append/.
| Path | Gate | Does | |
|---|---|---|---|
configure/{vault_id} | x-sgraph-vault-write-key | Registers append_anchors, hashes of accepted senders, and enum_key_hash | |
write/{vault_id} | append_tokenin the body | Appends a payload. Account-less: no access token required | |
list/{vault_id} | x-sgraph-vault-enum-key | Paginated listing, optionally with inline content | |
fetch/{vault_id} | enum key | Fetch specific file IDs, batched | |
mark-processed/{vault_id} | enum key | Moves pending → processed. Idempotent | |
purge/{vault_id} | write key | Deletes. `folder: "pending" | "processed"` |
Why the gates are split this way
Four capabilities, and the split is the whole design:
| Capability | Holder | Can | Cannot |
|---|---|---|---|
append_token | the sender | write | list, fetch, read anything |
enum_key | the vault owner | list, fetch, mark-processed | write, purge |
write_key | the vault owner | configure, purge | none |
| private key | the vault owner | decrypt | never sent to the server |
A sender can put something into your vault and learn nothing at all, not the contents, not the volume, not whether anyone else writes there. The server stores SHA-256 of the first three capabilities and compares hashes; the fourth it never sees.
Four contracts worth relying on
- The write response is blind.
/writereturns exactly{"ok": true}, no file ID, no count, no metadata. This is a security property, not an oversight: a returned ID would leak lane state to a party who is only allowed to write. - Filenames are server-assigned as
{epoch_ms:013d}_{24-hex}.enc. Being chronologically sortable is what makes cursor pagination stable, pass the last file ID you saw asafter_file_id. - Metadata-only listing is free.
include_content: falsereads zero payloads. Poll with it, then fetch on demand; polling with content is the common way to hit the 3 MB ceiling for no reason. - mark-processed is idempotent. An already-moved file comes back in
missingrather than as an error, so retrying a batch after a timeout is safe.
From a vault app: one verb crosses vaults, five do not
This is the constraint to understand before designing anything that spans two vaults, and it was undocumented until the SG/API team's review of 6 September 2026 (quoted here CC BY 4.0). The bridge builds one client bound to the currently open vault, with the enum key derived from that vault's read key. So:
| Verb | Which vault it acts on | Bridge permission |
|---|---|---|
write | Any: it takes an explicit vault_id and posts with no headers at all | append.write |
list | Always the open vault. There is no way to address a remote vault with these from an app | append.list |
fetch | append.read, notappend.fetch | |
markProcessed | append.markProcessed | |
purge | append.purge | |
configure | append.configure |
All six are plain booleans, default-deny, not path-scoped. The asymmetry is deliberate rather than a gap: listing a remote lane needs the recipient's enum key, and shipping that inside a published app "would give every visitor read access to the whole lane."
Two things that are not gates on append, despite appearances. There is no read-only check anywhere in the append handler, sg.app.writable is irrelevant to it, and a read-key session can write to a lane given the grant. And the CSP is not an append rule: the frame ships connect-src blob: data:, so a direct fetch to these endpoints is blocked unless the app declares permissions.network: true, which reopens all egress and is the worse choice. Use the bridge. See the build brief↗, which had both of these wrong until the review corrected it.
The inbox field is the lane id, and today it is the token
A list response labels each entry with an inbox value. That value is the storage folder name, and the folder is named by the raw append token, byte for byte, while config.append_anchors stores sha256(token). So the design hashes the token in config and then writes the plaintext token into the object key.
The disclosure is bounded: list is gated by the enum key, so it reaches only the vault owner, who already holds every credential in play; and the token is a write-only lane address, closer to an email address than a password. The residual risk is storage rather than API, the raw token becomes an object key, so it lands in access logs, inventory reports and backups, "surfaces with a very different audience from the enum-key holder." The team intends to fold the folder name to sha256(token), which is a breaking change needing a migration. Documented here now so that nobody has to discover it from a debrief.
Lanes live at bare/append/{token}/pending/ and …/processed/, outside the version-controlled commit tree: appends never touch a branch and never conflict with a push.
Several senders on one vault
Register several append_anchors and each sender writes into their own lane, distinguished by their token. A listing can be scoped to one lane, so one correspondent flooding you does not bury another, and revoking one sender is removing one anchor, with no effect on the rest.
Limits and codes
| Limit | Value | On breach |
|---|---|---|
| Payload per write | 5 MB | 413 |
| Pending files per token | 1000 | 507 |
| File IDs per batch | 100 | 400 |
| Inline content when listing | 3 MB cumulative | 413 |
| Page size | 50 default, 200 max | clamped silently |
The append_token pattern is ^[0-9a-f]{16,128}$, hex only. A prefixed token returns 400, not 403; see errors↗ for why that distinction saves time.
See also
- Vault messaging↗, this API composed with PKI into a worked example
sg.append↗, the same six operations from a vault app- Errors and limits↗