Skip to main content
Every request to the Basaltic API is signed with BASALTIC-HMAC-SHA256. There is one credential type — an access key belonging to a service account — and one way to present it.

Getting credentials

Access keys belong to a service account, never to a person. Create one, give it a policy, then create a credential on it — see the quickstart. The secret is shown once, at creation, and never again. For short-lived credentials, POST /v1/assume-role on iam.basaltic.sh vends temporary ones against a role your account owns. They come with a session token that must be sent as X-Amz-Security-Token and included in SignedHeaders.

The signing scheme

BASALTIC-HMAC-SHA256 is an HMAC-SHA256 over a canonical form of your request, keyed by a value derived from your secret. It is close to AWS SigV4, with the differences called out below.

Headers you send

What actually gets signed

SignedHeaders is exactly host;x-date;x-nonce — plus x-amz-security-token when you are using temporary credentials.
X-Content-Sha256 is not in SignedHeaders. The body is bound to the signature through the last line of the canonical request instead. Adding x-content-sha256 to the signed-header list produces a signature the server will reject.

Authorization header

<date> is the YYYYMMDD day part of X-Date. <region> is the region code you are calling, or global for the global services. Note the comma and space between the three parameters.

Canonical request

Six lines, joined with \n:
  • canonical-path — the URL-escaped path, e.g. /v1/instances.
  • canonical-query — each key and value URL-encoded, sorted by key then value, joined with &. Empty string when there is no query.
  • canonical-headersname:value per signed header, name lowercased, value trimmed, sorted by name, each line ending in \n.
  • signed-headers — the same names, lowercased, sorted, joined with ;.
  • body-hash — lowercase-hex SHA-256 of the body, or the literal UNSIGNED-PAYLOAD for an empty body or a streaming upload you do not want to buffer just to hash.

String to sign

The second line is the credential date at midnight20260901T000000Z — not your X-Date value. This is the single most common reason a hand-rolled signer produces a rejected signature.

Signing key

Four chained HMACs, starting from your secret with a literal BASALTIC prefix:
A complete working implementation is in the quickstart.

Validity and replay

  • A signature is good for 5 minutes from its X-Date. If you get 401s on otherwise correct requests, check the clock on the calling machine.
  • X-Nonce is what makes each signature unique. X-Date has one-second resolution and a streaming body signs as UNSIGNED-PAYLOAD, so without a nonce two different requests in the same second can sign identically. Mutating requests are replay-guarded on it, and omitting it is rejected.
  • The nonce is signed, so a replayer cannot strip it.

Rate limits

There is no global request budget. A limit applies only where an operation documents a 429, and each such endpoint counts its own fixed window. Everything else is bounded by quota rather than by request rate. On this surface the limited endpoint is the public region catalogue, GET /v1/regions. Responses — 429 and 2xx alike — carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset so you can pace yourself instead of discovering the ceiling by hitting it. Read X-RateLimit-Limit rather than hard-coding a number. On a 429, honor Retry-After. Retrying sooner is refused and extends the window, because the refused attempt is itself counted. Some resources meter the work rather than the request — sending email, retrying a charge — and document their own 429 on the operation.