bytes · operations surface
Bindings
| Primary | http |
|---|---|
| HTTP | prefix /v1 |
Operations
| Name | Kind | Binding | Request | Response | Parameters | Refusals |
|---|---|---|---|---|---|---|
get | read | GET /v1/blobs/{name} |
bytes | name: string (required) | not_found | |
get_range | read | GET /v1/blobs/{name} |
bytes | name: string (required), offset: int, length: int | not_found | |
get_stream | stream | GET /v1/blobs/{name} |
bytes | name: string (required) | not_found | |
has | read | POST /v1/stat |
StatRequest | StatResponse | ||
size | read | POST /v1/stat |
StatRequest | StatResponse | ||
put | mutation | POST /v1/blobs |
bytes | PutResponse | too_large | |
put_stream | mutation | POST /v1/blobs |
bytes | PutResponse | too_large | |
get_many | read | POST /v1/get |
NamesRequest | records | ||
put_many | mutation | POST /v1/put |
records | PutManyResponse | ||
livez | operational | GET /livez |
Health | |||
healthz | operational | GET /healthz |
Health | |||
describe | operational | GET /describe |
Describe | |||
describe_file | operational | GET /describe/{file...} |
file | file: string (required) | not_found |
Refusals
| Code | HTTP | Meaning |
|---|---|---|
not_found | 404 | the name asked for is not here |
too_large | 413 | the bytes exceed what this deployment accepts |
bad_request | 400 | a malformed name or request body: a protocol error of this binding, not a refusal of content |
Files served
- HTTP.md
- SURFACE.md
- openapi.json
- operations.json
- schema/Describe.schema.json
- schema/Error.schema.json
- schema/Health.schema.json
- schema/NamesRequest.schema.json
- schema/PutManyResponse.schema.json
- schema/PutResponse.schema.json
- schema/StatRequest.schema.json
- schema/StatResponse.schema.json
SURFACE.md
The surface
Contract: 2026-09-10, the day this surface last changed; served as
Ulab-Contract on every response and carried by the SDK as bnbytes.Contract.
Nine verbs, two refusals, one name grammar. This file is the contract; the
bindings (see HTTP.md) carry it, they do not extend it. The
operational routes of ulab-standard/OPERATIONS.md (/livez, /healthz,
/describe) are not part of it; the binding document states them.
Names
sha256:<64 lowercase hex digits>
The digest of the bytes, prefixed by the algorithm so a future algorithm is visible rather than silent. Names are compared as strings; there is no canonicalization beyond the grammar itself. A string that does not match the grammar is not a name — rejecting it is a protocol error of the binding, not a refusal of content, so it does not appear in the refusal list.
Verbs
| verb | in | out |
|---|---|---|
get |
name | the bytes |
get many |
names | blobs, keyed by name, unordered |
has |
names | which of them are here |
size |
names | byte count per name that is here |
get range |
name, offset, length | the overlapping bytes |
get stream |
name | the bytes, incrementally |
put |
bytes | name |
put many |
list of bytes | names, per item |
put stream |
bytes, incrementally | name |
has and size are plural. Singular existence-checking pays one round
trip per node, and the client that asks is usually deciding fetch-whole versus
stream for a set of children it just discovered, not for one blob. A plural
size quietly subsumes has — a name with a size is here, a name without one
is not — so a binding may carry both verbs on one operation. The singular case
is a set of one.
Pinned behaviors
These were once open questions (#3); they are now contract. None of them changes what the service is — which held only under these answers, and is why they were decided before an implementation could decide them by accident.
put stream commits atomically, at clean end-of-stream, under the hash of
exactly what arrived. Integrity under the full content's hash cannot be
violated — the server only knows the hash of what it received, and those bytes
self-verify. What this clause forbids is prefix-commit: an aborted stream's
prefix also self-verifies, so committing it under hash(prefix) breaks no
naming identity, yet a partial object becomes visible under a hash. Therefore:
any abort — client disconnect, or the server tripping its deployment cap
mid-stream — commits nothing. Dedup at end-of-stream is the ordinary
idempotent put; note that this short-circuit is necessarily server-side and
commit-time-only, unlike the has-before-put skip, which is the SDK's.
put many is per-item, not atomic across the batch. Batch atomicity would
falsify the README's "a crashed top-down put many leaves [a dangling node]",
and the leaves-first write convention exists precisely because batches land
partially. Retries re-put, dedup absorbs them, residue falls under "GC must be
driven by whoever holds the roots." An oversized item refuses alone; its
neighbors land.
get range past the end is a short read. Refusal would be legal — size is
structural, not semantic — but the refusal enumeration decides it: a range
refusal would grow the list of two. The store returns the overlap of the
requested range with the blob; a range entirely past the end overlaps nothing
and returns zero bytes. A range is the only read verify-on-read cannot check;
immutability makes size a reliable clamp, and a client that needs
verification fetches whole or streams.
get many is unordered, keyed by name. Ordering is most of what the
plural form would otherwise cost; keying is free, since verify-on-read hashes
every blob anyway, and it handles duplicate names and per-item absence — a
name that is not here is simply absent from the result, not an error.
get stream cancellation is nothing, server-side. The client stops
reading; the server stops sending. No state, no verb, no cleanup protocol.
Refusals
Only two, neither about content:
| refusal | meaning |
|---|---|
too large |
the bytes exceed what this deployment accepts |
not found |
the name asked for is not here |
There is no "malformed" — that is a judgement about a format, and there is no format here to judge against.
not found is a refusal only where a single blob's bytes were demanded
(get, get range, get stream). In the plural verbs absence is data —
an omitted key — because a batch that fails on its most absent member is
useless for the negotiation the plural verbs exist to serve.
Persistence, cancellation and uncertain outcomes
An acknowledged put, put stream or put many item is durable before it is
acknowledged: the bytes are written and flushed to their file, the file is
published under its name by one rename, and the containing directories are
flushed where the platform allows it. The blob survives a process crash and a
restart with the same data directory. Power-loss durability is bounded by the
filesystem and device honouring those flushes; the evidence behind this
paragraph is process-kill and restart tests, not power-loss tests.
Reads never return bytes that were not committed: a name is either absent or names the complete blob. A crash mid-stream leaves at most a temporary file that no verb can reach, never a partial object under a name.
A caller that cancels, times out or loses the connection after sending bytes
learns nothing about the outcome: the request either committed the whole blob
or committed nothing, never part of one. Because names are content-derived
and put is idempotent, the caller resolves the uncertainty by checking the
expected name with has or by sending the same bytes again; both are safe.
An acknowledgement can be lost after a commit, so an error after dispatch is
not proof that the blob is absent. Cancelling a get stream is nothing
server-side; cancelling a put stream commits nothing.
What the SDK adds, off the wire
Verify-on-read, cache tiers, skipping a put when has already says yes,
parallel fetch, failover across mirrors. These live in the client so that the
storage does not have to be trusted — only available. Nothing on the wire
knows about any of them.
HTTP.md
The HTTP binding
One binding of the surface. It carries the nine verbs on five routes; it adds nothing.
Application routes live under /v1. The version belongs to this binding, not
to the service: the contract's worth is that it does not change, and the
prefix is what lets that claim survive being wrong. The service's name is
bytes, with no version, and every response carries two headers:
Ulab-Service: bytes
Ulab-Contract: 2026-09-10
Ulab-Contract is the date on the surface's contract line. A client treats a
response without Ulab-Service: bytes as a protocol error, before reading
its body, whatever its status.
Routes
| route | verbs |
|---|---|
GET /v1/blobs/{name} |
get, get range, get stream |
POST /v1/stat |
has, size |
POST /v1/blobs |
put, put stream |
POST /v1/get |
get many |
POST /v1/put |
put many |
GET /v1/blobs/{name} — get, get range, get stream
Returns the bytes, 200, Content-Type: application/octet-stream, with
Content-Length when known. Whether the client treats the body as a whole
blob or a stream is the client's business; the wire is identical.
Query parameters offset and length (non-negative integers) select a
range. Per the surface, the response is the overlap: length is clamped to
the end, an offset at or past the end yields an empty 200. The HTTP
Range header is not part of this binding — its unsatisfiable-range semantics
(416) would smuggle in a third refusal.
Cancellation is the client closing the connection. The server sends nothing further and keeps nothing.
Errors: 404 if the name is not here.
POST /v1/stat — has, size
Request application/json:
{"names": ["sha256:…", "sha256:…"]}
Response 200 application/json:
{"sizes": {"sha256:…": 1024}}
sizes holds an entry for every requested name that is here, and no entry
for one that is not. This one operation is both plural verbs: presence of the
key is has, its value is size.
POST /v1/blobs — put, put stream
The request body is the bytes, raw. The server hashes what arrives and commits at clean end-of-body — for a streamed (chunked) request this is the atomic commit the surface pins: an incomplete body, whoever aborted it, commits nothing. Content already present short-circuits at commit time; the response does not distinguish it, because the verb is idempotent and the distinction is not the client's business.
Response 200 application/json:
{"name": "sha256:…", "size": 1024}
Errors: 413 if the bytes exceed the deployment cap — including when the cap
trips mid-stream, in which case nothing was committed.
POST /v1/get — get many
Request application/json:
{"names": ["sha256:…", "sha256:…"]}
Response 200 application/x-bn-blobs: a sequence of records, one per blob
that is here, in any order. Each record is an ASCII header line
<name> <size>\n
followed by exactly <size> raw bytes. The sequence ends at end-of-body.
Requested names that are not here produce no record. Duplicate requested
names produce one record.
POST /v1/put — put many
Request application/x-bn-blobs with anonymous records — the header line is
just <size>\n, since the name is what the server is being asked to compute
— each followed by exactly <size> raw bytes.
Response 200 application/json, one entry per record, in request order
(the items have no names until now, so order is the only key there is):
{"results": [
{"name": "sha256:…", "size": 3},
{"error": "too_large", "message": "the body exceeds the per-blob limit"}
]}
Each item commits independently, per the surface: an oversized item refuses alone, its neighbors land, and a batch cut off mid-body commits the records that arrived whole and nothing of the one that did not.
Operational routes
Four routes at the root, per ulab-standard/OPERATIONS.md, without credentials
and outside the surface. They are rows of kind operational in
operations.json.
| route | answers | status |
|---|---|---|
GET /livez |
the process is up | 200 always |
GET /healthz |
the store can serve: the data directory exists and a temporary file can be created where commits stage | 200, or 503 with the failing check |
GET /describe |
the service's description: name, contract date, build, bindings, the operation table, the refusal table, the served files | 200 |
GET /describe/{file} |
one committed contract file, verbatim, by a name describe.files lists; {file} spans slashes for schema/<Type>.schema.json |
200, 404 not_found otherwise |
/livez and /healthz answer the health envelope:
{"status":"ok","service":"bytes","contract":"2026-09-10",
"build":{"revision":"<vcs revision>","dirty":false,"go":"go1.26.3"},
"time":"2026-09-10T02:10:00Z",
"checks":[{"name":"store","status":"ok","detail":"data directory writable"}]}
/livez carries an empty checks array. build comes from the binary's
build information, or from the link-time variables
github.com/Bitspark/bn-bytes/impl/cloud/go/api.Revision and .Dirty when
the build carried none (-buildvcs=false), or is "unknown" and true.
A bytes store has no instance identity, so the envelope and the description
carry no identity member.
/describe serves the files embedded in the binary at build: SURFACE.md,
HTTP.md, operations.json, openapi.json and schema/<Type>.schema.json,
each verbatim as text/markdown or application/json. A drift test holds
the embedded files byte-equal to the committed ones.
Validation
Since 2026-09-10 the binding refuses what it does not document, per the
family's validation rule: on GET /v1/blobs/{name} any query parameter other
than offset and length, a repeated one, or an empty one is 400 bad_request;
a JSON body for /v1/stat or /v1/get must be one object whose only member is
names, given once, followed by nothing but whitespace, and any unknown or
repeated member, trailing data or non-array names is 400 bad_request. JSON
bodies are limited to 4 MiB. A client that misspells a parameter hears about
it instead of receiving the whole blob. The Go router's own 404 and 405
answers and its canonical-path redirects are outcomes of this binding, not
refusals of content; a validated SDK refuses redirects and is configured with
the final endpoint. Batch responses are buffered in proportion to the item
count; there is no item-count cap.
Errors
Error responses carry application/json:
{"error": "not_found", "message": "no such object or route"}
| status | error | where |
|---|---|---|
404 |
not_found |
GET /v1/blobs/{name} |
413 |
too_large |
POST /v1/blobs, per-item in POST /v1/put |
400 |
bad_request |
a malformed name or request body — a protocol error of this binding, not a refusal of content |
Nothing else is a refusal. 5xx means the server failed, not that it
refused; a client retries or fails over, which content addressing makes safe.
Schemas
schema/Describe.schema.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"Bindings": {
"properties": {
"primary": {
"type": "string"
},
"http": {
"$ref": "#/$defs/HTTPPrefix"
}
},
"type": "object",
"required": [
"primary",
"http"
],
"description": "Bindings names the primary binding and the base of each one offered."
},
"Build": {
"properties": {
"revision": {
"type": "string"
},
"dirty": {
"type": "boolean"
},
"go": {
"type": "string"
}
},
"type": "object",
"required": [
"revision",
"dirty",
"go"
],
"description": "Build identifies the binary that answered: the version-control revision it was built from and whether that tree was dirty, from the binary's build information or from values stamped at link time, else \"unknown\" and true."
},
"HTTPBinding": {
"properties": {
"method": {
"type": "string"
},
"path": {
"type": "string"
}
},
"type": "object",
"required": [
"method",
"path"
],
"description": "HTTPBinding carries an operation as one request and one response."
},
"HTTPPrefix": {
"properties": {
"prefix": {
"type": "string"
}
},
"type": "object",
"required": [
"prefix"
],
"description": "HTTPPrefix is where the application routes of the HTTP binding live."
},
"Operation": {
"properties": {
"name": {
"type": "string"
},
"kind": {
"type": "string"
},
"doc": {
"type": "string"
},
"params": {
"items": {
"$ref": "#/$defs/Param"
},
"type": "array"
},
"request": {
"type": "string",
"description": "body type; \"bytes\" or \"records\" for raw bodies"
},
"response": {
"type": "string",
"description": "result type; \"bytes\" or \"records\" for raw bodies"
},
"errors": {
"items": {
"type": "string"
},
"type": "array",
"description": "refusals beyond bad_request"
},
"http": {
"$ref": "#/$defs/HTTPBinding"
}
},
"type": "object",
"required": [
"name",
"kind",
"doc",
"params",
"errors",
"http"
],
"description": "Operation is one row of api/operations.json."
},
"Param": {
"properties": {
"name": {
"type": "string"
},
"kind": {
"type": "string"
},
"required": {
"type": "boolean"
},
"doc": {
"type": "string"
}
},
"type": "object",
"required": [
"name",
"kind",
"required",
"doc"
],
"description": "Param is one named input of an operation, outside its body."
},
"Refusal": {
"properties": {
"name": {
"type": "string"
},
"http": {
"type": "integer"
},
"meaning": {
"type": "string"
}
},
"type": "object",
"required": [
"name",
"http",
"meaning"
],
"description": "Refusal is one row of the refusal table: a name the surface defines, the HTTP status the binding maps it to, and its meaning."
}
},
"properties": {
"service": {
"type": "string"
},
"contract": {
"type": "string"
},
"build": {
"$ref": "#/$defs/Build"
},
"bindings": {
"$ref": "#/$defs/Bindings"
},
"operations": {
"items": {
"$ref": "#/$defs/Operation"
},
"type": "array"
},
"refusals": {
"items": {
"$ref": "#/$defs/Refusal"
},
"type": "array"
},
"files": {
"items": {
"type": "string"
},
"type": "array"
},
"identity": {
"type": "object"
}
},
"type": "object",
"required": [
"service",
"contract",
"build",
"bindings",
"operations",
"refusals",
"files"
],
"title": "Describe",
"description": "Describe is the document /describe returns."
}
schema/Error.schema.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"error": {
"type": "string"
}
},
"type": "object",
"required": [
"error"
],
"title": "Error",
"description": "ErrorResponse is the JSON envelope of every refusal and protocol error."
}
schema/Health.schema.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"Build": {
"properties": {
"revision": {
"type": "string"
},
"dirty": {
"type": "boolean"
},
"go": {
"type": "string"
}
},
"type": "object",
"required": [
"revision",
"dirty",
"go"
],
"description": "Build identifies the binary that answered: the version-control revision it was built from and whether that tree was dirty, from the binary's build information or from values stamped at link time, else \"unknown\" and true."
},
"Check": {
"properties": {
"name": {
"type": "string"
},
"status": {
"type": "string",
"description": "ok or failing"
},
"detail": {
"type": "string"
}
},
"type": "object",
"required": [
"name",
"status",
"detail"
],
"description": "Check is one dependency the service needs in order to serve."
}
},
"properties": {
"status": {
"type": "string"
},
"service": {
"type": "string"
},
"contract": {
"type": "string"
},
"build": {
"$ref": "#/$defs/Build"
},
"time": {
"type": "string"
},
"checks": {
"items": {
"$ref": "#/$defs/Check"
},
"type": "array"
},
"identity": {
"type": "object"
}
},
"type": "object",
"required": [
"status",
"service",
"contract",
"build",
"time",
"checks"
],
"title": "Health",
"description": "Health is the envelope of /livez and /healthz."
}
schema/NamesRequest.schema.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"names": {
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object",
"required": [
"names"
],
"title": "NamesRequest",
"description": "NamesRequest is the body of get many."
}
schema/PutManyResponse.schema.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"PutManyItem": {
"properties": {
"name": {
"type": "string"
},
"size": {
"type": "integer"
},
"error": {
"type": "string"
}
},
"type": "object",
"description": "PutManyItem is one entry of a put many response: a name and size, or an error for the item that refused alone."
}
},
"properties": {
"results": {
"items": {
"$ref": "#/$defs/PutManyItem"
},
"type": "array"
}
},
"type": "object",
"required": [
"results"
],
"title": "PutManyResponse",
"description": "PutManyResponse answers put many, one entry per record in request order."
}
schema/PutResponse.schema.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"name": {
"type": "string"
},
"size": {
"type": "integer"
}
},
"type": "object",
"required": [
"name",
"size"
],
"title": "PutResponse",
"description": "PutResponse acknowledges put and put stream."
}
schema/StatRequest.schema.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"names": {
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object",
"required": [
"names"
],
"title": "StatRequest",
"description": "StatRequest and NamesRequest are the JSON bodies of /v1/stat and /v1/get."
}
schema/StatResponse.schema.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"sizes": {
"additionalProperties": {
"type": "integer"
},
"type": "object"
}
},
"type": "object",
"required": [
"sizes"
],
"title": "StatResponse",
"description": "StatResponse answers has and size: an entry for every requested name that is here, none for one that is not."
}
OpenAPI
openapi.json (raw)
{
"components": {
"schemas": {
"Bindings": {
"description": "Bindings names the primary binding and the base of each one offered.",
"properties": {
"http": {
"$ref": "#/components/schemas/HTTPPrefix"
},
"primary": {
"type": "string"
}
},
"required": [
"primary",
"http"
],
"type": "object"
},
"Build": {
"description": "Build identifies the binary that answered: the version-control revision it was built from and whether that tree was dirty, from the binary's build information or from values stamped at link time, else \"unknown\" and true.",
"properties": {
"dirty": {
"type": "boolean"
},
"go": {
"type": "string"
},
"revision": {
"type": "string"
}
},
"required": [
"revision",
"dirty",
"go"
],
"type": "object"
},
"Check": {
"description": "Check is one dependency the service needs in order to serve.",
"properties": {
"detail": {
"type": "string"
},
"name": {
"type": "string"
},
"status": {
"description": "ok or failing",
"type": "string"
}
},
"required": [
"name",
"status",
"detail"
],
"type": "object"
},
"Describe": {
"description": "Describe is the document /describe returns.",
"properties": {
"bindings": {
"$ref": "#/components/schemas/Bindings"
},
"build": {
"$ref": "#/components/schemas/Build"
},
"contract": {
"type": "string"
},
"files": {
"items": {
"type": "string"
},
"type": "array"
},
"identity": {
"type": "object"
},
"operations": {
"items": {
"$ref": "#/components/schemas/Operation"
},
"type": "array"
},
"refusals": {
"items": {
"$ref": "#/components/schemas/Refusal"
},
"type": "array"
},
"service": {
"type": "string"
}
},
"required": [
"service",
"contract",
"build",
"bindings",
"operations",
"refusals",
"files"
],
"title": "Describe",
"type": "object"
},
"Error": {
"description": "ErrorResponse is the JSON envelope of every refusal and protocol error.",
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"title": "Error",
"type": "object"
},
"HTTPBinding": {
"description": "HTTPBinding carries an operation as one request and one response.",
"properties": {
"method": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": [
"method",
"path"
],
"type": "object"
},
"HTTPPrefix": {
"description": "HTTPPrefix is where the application routes of the HTTP binding live.",
"properties": {
"prefix": {
"type": "string"
}
},
"required": [
"prefix"
],
"type": "object"
},
"Health": {
"description": "Health is the envelope of /livez and /healthz.",
"properties": {
"build": {
"$ref": "#/components/schemas/Build"
},
"checks": {
"items": {
"$ref": "#/components/schemas/Check"
},
"type": "array"
},
"contract": {
"type": "string"
},
"identity": {
"type": "object"
},
"service": {
"type": "string"
},
"status": {
"type": "string"
},
"time": {
"type": "string"
}
},
"required": [
"status",
"service",
"contract",
"build",
"time",
"checks"
],
"title": "Health",
"type": "object"
},
"NamesRequest": {
"description": "NamesRequest is the body of get many.",
"properties": {
"names": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"names"
],
"title": "NamesRequest",
"type": "object"
},
"Operation": {
"description": "Operation is one row of api/operations.json.",
"properties": {
"doc": {
"type": "string"
},
"errors": {
"description": "refusals beyond bad_request",
"items": {
"type": "string"
},
"type": "array"
},
"http": {
"$ref": "#/components/schemas/HTTPBinding"
},
"kind": {
"type": "string"
},
"name": {
"type": "string"
},
"params": {
"items": {
"$ref": "#/components/schemas/Param"
},
"type": "array"
},
"request": {
"description": "body type; \"bytes\" or \"records\" for raw bodies",
"type": "string"
},
"response": {
"description": "result type; \"bytes\" or \"records\" for raw bodies",
"type": "string"
}
},
"required": [
"name",
"kind",
"doc",
"params",
"errors",
"http"
],
"type": "object"
},
"Param": {
"description": "Param is one named input of an operation, outside its body.",
"properties": {
"doc": {
"type": "string"
},
"kind": {
"type": "string"
},
"name": {
"type": "string"
},
"required": {
"type": "boolean"
}
},
"required": [
"name",
"kind",
"required",
"doc"
],
"type": "object"
},
"PutManyItem": {
"description": "PutManyItem is one entry of a put many response: a name and size, or an error for the item that refused alone.",
"properties": {
"error": {
"type": "string"
},
"name": {
"type": "string"
},
"size": {
"type": "integer"
}
},
"type": "object"
},
"PutManyResponse": {
"description": "PutManyResponse answers put many, one entry per record in request order.",
"properties": {
"results": {
"items": {
"$ref": "#/components/schemas/PutManyItem"
},
"type": "array"
}
},
"required": [
"results"
],
"title": "PutManyResponse",
"type": "object"
},
"PutResponse": {
"description": "PutResponse acknowledges put and put stream.",
"properties": {
"name": {
"type": "string"
},
"size": {
"type": "integer"
}
},
"required": [
"name",
"size"
],
"title": "PutResponse",
"type": "object"
},
"Refusal": {
"description": "Refusal is one row of the refusal table: a name the surface defines, the HTTP status the binding maps it to, and its meaning.",
"properties": {
"http": {
"type": "integer"
},
"meaning": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"name",
"http",
"meaning"
],
"type": "object"
},
"StatRequest": {
"description": "StatRequest and NamesRequest are the JSON bodies of /v1/stat and /v1/get.",
"properties": {
"names": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"names"
],
"title": "StatRequest",
"type": "object"
},
"StatResponse": {
"description": "StatResponse answers has and size: an entry for every requested name that is here, none for one that is not.",
"properties": {
"sizes": {
"additionalProperties": {
"type": "integer"
},
"type": "object"
}
},
"required": [
"sizes"
],
"title": "StatResponse",
"type": "object"
}
}
},
"info": {
"description": "Generated from bnbytes.Operations and the SDK's types (ulab-standard/API.md). The contract is SURFACE.md and HTTP.md: record framing, atomic commit at end of stream, per-item batch commit and short reads live there and not here. Verbs that share a route appear once, with every verb listed under x-ulab-verbs.",
"title": "bytes",
"version": "2026-09-10"
},
"openapi": "3.1.0",
"paths": {
"/describe": {
"get": {
"operationId": "describe",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Describe"
}
}
},
"description": "the result"
}
},
"summary": "who this is, which contract, which build, which bindings, which operations, which refusals",
"tags": [
"operational"
],
"x-ulab-verbs": [
"describe"
]
}
},
"/describe/{file}": {
"get": {
"operationId": "describe_file",
"parameters": [
{
"description": "a name listed by describe.files",
"in": "path",
"name": "file",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"text/markdown or application/json, per file": {
"schema": {
"format": "binary",
"type": "string"
}
}
},
"description": "the result"
},
"404": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
},
"description": "not_found"
}
},
"summary": "one committed contract file, verbatim",
"tags": [
"operational"
],
"x-ulab-verbs": [
"describe_file"
]
}
},
"/healthz": {
"get": {
"operationId": "healthz",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Health"
}
}
},
"description": "the result"
},
"503": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Health"
}
}
},
"description": "failing: the same envelope with the failing checks"
}
},
"summary": "the service can serve its contract: every dependency check passes",
"tags": [
"operational"
],
"x-ulab-verbs": [
"healthz"
]
}
},
"/livez": {
"get": {
"operationId": "livez",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Health"
}
}
},
"description": "the result"
}
},
"summary": "the process is up and can answer",
"tags": [
"operational"
],
"x-ulab-verbs": [
"livez"
]
}
},
"/v1/blobs": {
"post": {
"operationId": "put",
"requestBody": {
"content": {
"application/octet-stream": {
"schema": {
"format": "binary",
"type": "string"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PutResponse"
}
}
},
"description": "the result"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
},
"description": "bad_request"
},
"413": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
},
"description": "too_large"
}
},
"summary": "store bytes under their hash; idempotent",
"tags": [
"mutation"
],
"x-ulab-verbs": [
"put",
"put_stream"
]
}
},
"/v1/blobs/{name}": {
"get": {
"operationId": "get",
"parameters": [
{
"description": "sha256:\u003c64 lowercase hex\u003e",
"in": "path",
"name": "name",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "first byte, default 0",
"in": "query",
"name": "offset",
"required": false,
"schema": {
"minimum": 0,
"type": "integer"
}
},
{
"description": "byte count, default to the end",
"in": "query",
"name": "length",
"required": false,
"schema": {
"minimum": 0,
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/octet-stream": {
"schema": {
"format": "binary",
"type": "string"
}
}
},
"description": "the result"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
},
"description": "bad_request"
},
"404": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
},
"description": "not_found"
}
},
"summary": "the bytes of one blob",
"tags": [
"read"
],
"x-ulab-verbs": [
"get",
"get_range",
"get_stream"
]
}
},
"/v1/get": {
"post": {
"operationId": "get_many",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NamesRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/x-bn-blobs": {
"schema": {
"format": "binary",
"type": "string"
}
}
},
"description": "the result"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
},
"description": "bad_request"
}
},
"summary": "the blobs that are here among the names, as records keyed by name, unordered",
"tags": [
"read"
],
"x-ulab-verbs": [
"get_many"
]
}
},
"/v1/put": {
"post": {
"operationId": "put_many",
"requestBody": {
"content": {
"application/x-bn-blobs": {
"schema": {
"format": "binary",
"type": "string"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PutManyResponse"
}
}
},
"description": "the result"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
},
"description": "bad_request"
}
},
"summary": "store each anonymous record independently; results in request order",
"tags": [
"mutation"
],
"x-ulab-verbs": [
"put_many"
]
}
},
"/v1/stat": {
"post": {
"operationId": "has",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StatRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StatResponse"
}
}
},
"description": "the result"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
},
"description": "bad_request"
}
},
"summary": "which of the names are here: presence of a key in sizes",
"tags": [
"read"
],
"x-ulab-verbs": [
"has",
"size"
]
}
}
},
"servers": [
{
"description": "the standalone default listener",
"url": "http://127.0.0.1:8080"
}
]
}