BryanLabsBryanLabs
Menu

API conventions

Every BryanLabs API behaves the same way. This page is the contract: how to authenticate, how errors look, how limits are signalled, and how endpoints are retired.

Versioning

Every API lives under /api/<feature>/v1/ on https://bryanlabs.net. The path version only changes for a breaking change. Additive changes (new fields, new endpoints, new enum values) ship without a version bump, so parse responses leniently and ignore members you do not know. Each API has an OpenAPI 3.1 document on its docs page; the changelog records every behaviour change.

Authentication

Read endpoints are public. Endpoints that act as you accept either the site session (the browser sends it) or a personal API key sent as Authorization: Bearer blk_…. Keys are created under Account, shown once, stored hashed, and can be rotated or revoked at any moment. A key carries only the scopes it was created with; a missing scope answers 403 insufficient_scope, an unknown or revoked key 401 invalid_api_key with a WWW-Authenticate header.

ScopeAllows
snapshots:readRead the snapshot catalog and state-sync data.
snapshots:downloadMint signed snapshot download URLs.
faucet:requestRequest testnet tokens as this account.
tax:readRead tax balances, events and price series.
upgrades:webhooksManage upgrade notification webhooks.
wasm:readRead CosmWasm code and contract data.

Keys are secrets. Keep them out of URLs, client-side code and shared logs; rotate one the moment it may have leaked.

Request ids

Every API response carries X-Request-Id. Send your own (8 to 128 characters of letters, digits, ., _ or -, for example your trace id) and it is echoed back; otherwise one is minted. The same value appears as request_id in every error body and in our server logs, so quoting it lets us find the exact call.

bash
# Send your own request id and an API key; both come back in the response.
curl -sS https://bryanlabs.net/api/snapshots/v1/chains \
  -H 'authorization: Bearer blk_...' \
  -H 'x-request-id: build-4821-step-3' \
  -D - -o /dev/null

# Retry a POST safely: the same Idempotency-Key replays the first answer.
curl -sS -X POST https://bryanlabs.net/api/networks/v1/faucet/provider/request \
  -H 'content-type: application/json' \
  -H 'idempotency-key: 0b2f0d7e-faucet-cosmos1abc' \
  -d '{"address":"cosmos1..."}'

Errors

Every error is an RFC 9457 problem, served as application/problem+json. Branch on code, never on detail: the code is stable and documented, the detail is prose for people and may change. type links to the code's row below (or to the API's own docs for a service-specific code), instance is the path, request_id matches the header, and retry_after appears whenever the refusal is temporary. Validation problems add errors, a list of { path, message }.

bash
HTTP/2 429
content-type: application/problem+json
x-request-id: 3b1e4f0a-7c2d-4e8b-9a61-1f0c2d3e4a5b
retry-after: 1812
ratelimit: "hourly";r=0;t=1812
ratelimit-policy: "hourly";q=1000;w=3600

{
  "type": "https://bryanlabs.net/api-docs/conventions#rate_limited",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Hourly quota used. Try again in 30 minutes.",
  "code": "rate_limited",
  "instance": "/api/snapshots/v1/chains/cosmoshub-4/download",
  "request_id": "3b1e4f0a-7c2d-4e8b-9a61-1f0c2d3e4a5b",
  "retry_after": 1812
}

Compatibility: the bodies also carry error (same as detail) and success: false, the shape the APIs used before 2026-10-14. Both are deprecated and go away on 2027-04-01.

Shared codes

These codes mean the same thing on every API. Service-specific codes (a faucet refusal, a snapshot state) are listed in that API's OpenAPI document.

CodeStatusWhen
bad_request400The request could not be read: malformed JSON, a bad parameter.
unauthorized401No session and no API key on an endpoint that needs one.
invalid_api_key401The bearer key is unknown, revoked or expired.
forbidden403Authenticated, but not allowed to do this.
insufficient_scope403The API key does not carry the scope the endpoint needs.
not_found404The resource does not exist.
route_not_found404No API route at that path.
method_not_allowed405The route exists but not for this HTTP method.
conflict409The request conflicts with current state, for example a limit on how many of something you can have.
gone410The endpoint was retired. The Link header points at the successor.
validation_failed422The body or query was well formed but invalid. `errors` lists each field.
precondition_required428Something must be sent first. The faucet uses pow_required with the challenge inline.
rate_limited429Over a quota. Retry-After and the RateLimit headers say when.
idempotency_key_invalid400Idempotency-Key is not 1 to 255 characters of letters, digits, '.', '_', ':' or '-'.
idempotency_key_reused422The same Idempotency-Key was sent with a different body.
idempotency_in_progress409The first request with this key is still running. Retry shortly.
internal_error500Our fault. Quote the request_id when reporting it.
upstream_error502A backing service answered badly.
unavailable503Temporarily down or paused. Retry-After is set.
upstream_timeout504A backing service did not answer in time.

Rate limits

Limited endpoints describe their policy on every response with the IETF RateLimit header fields: RateLimit-Policy: "hourly";q=1000;w=3600 is the quota and window, RateLimit: "hourly";r=12;t=1812 is what is left and the seconds until it resets. The older X-RateLimit-Limit, -Remaining and -Reset headers are sent too. A refusal is 429 rate_limited with Retry-After; a paused or saturated service is 503 with Retry-After. Back off for at least that long, with jitter.

Published quotas are per client. Abuse controls that protect a service as a whole (for example the faucet's pool-wide allowance) are deliberately not published; a refusal from one of them is a plain busy with a retry time.

Idempotency

POST endpoints that create or spend accept an Idempotency-Key header (IETF draft). Retrying with the same key and the same body within 24 hours replays the first answer, marked Idempotent-Replayed: true, instead of acting twice. The same key with a different body is 422 idempotency_key_reused; a key whose first request is still running is 409 idempotency_in_progress. Keys are scoped to you and the endpoint, so a UUID per logical operation is the right choice. GET, PUT and DELETE are idempotent by design and ignore the header.

Deprecation

A retired endpoint keeps answering until its sunset date and says so on the wire: Deprecation (RFC 9745) carries the announcement date, Sunset (RFC 8594) the removal date, and Link points at the changelog entry and the successor. Removals are announced at least 90 days ahead. After the sunset date the path answers 410 gone.

Webhook signing

Deliveries from the Upgrades webhooks carry X-Bryanlabs-Timestamp (milliseconds since the epoch) and X-Bryanlabs-Signature: sha256=<hex>, an HMAC-SHA256 over "<timestamp>.<raw body>" with your endpoint's signing secret. Verify over the raw bytes before parsing, compare in constant time, and reject timestamps older than five minutes to defeat replays. Deliveries that fail are retried with backoff; make your handler idempotent on the event id.

python
import hmac, hashlib, time

def verify(secret: str, headers: dict, raw_body: bytes) -> bool:
    ts = headers["X-Bryanlabs-Timestamp"]
    sig = headers["X-Bryanlabs-Signature"].removeprefix("sha256=")
    if abs(time.time() * 1000 - int(ts)) > 5 * 60 * 1000:
        return False  # replayed or clock skew beyond five minutes
    expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)

Transport

HTTPS only, TLS 1.2 or newer, HSTS at the edge, certificates from Let's Encrypt with CAA records restricting issuance. JSON bodies are UTF-8; send content-type: application/json. Responses are compressed when the client accepts it. Cross-origin reads are allowed on the public APIs; credentialed CORS is limited to BryanLabs origins. The security page covers how the platform is run and how to report a vulnerability; the status page shows live availability.