The four headers
All four are required. Sending some but not all returns
401 with code: signature_required.
Sending a dashboard session token returns 401 with code: jwt_not_accepted.
Some operations additionally require a permission on your service account. A valid signature
without the required permission returns
403, not 401 — if you are getting 403, your
signing is fine and the fix is a permission grant, not a code change. See
Service accounts.The canonical string
Seven fields, joined with a single line feed (\n, U+000A), in exactly this order. No trailing
newline.
string
Uppercase verb —
GET, POST, PUT, DELETE.string
The request path only — no scheme, host or query — with its leading slash, and byte-identical to
the path you actually send, including casing. Sign the decoded form: if your URL contains
percent-encoded segments,
/x/a%20b is signed as /x/a b.string
Empty string when there is no query. Otherwise: split into key/value pairs; sort by key in byte
order, ties broken by value; re-encode keys and values with RFC 3986 percent-encoding
(uppercase hex, space as
%20, never +); join as key=value with &.Byte order puts uppercase before lowercase — Z sorts before a. A locale-aware or
case-insensitive sort will produce a different string and fail verification.string
Byte-identical to the
Timestamp header. Seconds, not milliseconds.string
Byte-identical to the
Nonce header. Must parse as a UUID; use lowercase 8-4-4-4-12.string
Your service account’s API secret. It is part of the signed content — it is never sent as a
header.
string
Lowercase hex SHA-256 of the raw request body bytes. An empty body hashes to
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.RSASSA-PKCS1-v1_5 over SHA-256, then base64-encode the
result.
A worked example
What a signed request looks like on the wire
Troubleshooting
Every rejection carries acode in its problem+json body. Branch on that, never
on detail.
401 signature_required
401 signature_required
One or more of the four headers is missing. Partial header sets are treated the same as no
credentials at all.
401 jwt_not_accepted
401 jwt_not_accepted
You sent an
Authorization: Bearer … header. The public API does not accept dashboard session
tokens, and sending one alongside correct signature headers is rejected just the same — the
token is never parsed, so whether it is valid makes no difference. Remove the header and sign
the request.401 signature_invalid
401 signature_invalid
All four headers were present, but the signature did not verify. In order of likelihood: the
body you hashed is not byte-identical to the body you sent (re-serialising JSON between
hashing and sending is the usual cause); the query string was not canonicalised; you signed a
different path than you sent; or your clock is more than 60 seconds out.
401 signature_invalid on a retry
401 signature_invalid on a retry
Nonces are single-use, so a replayed one fails exactly like a bad signature. Generate a fresh
UUID for every attempt, including retries.
403 permission_denied
403 permission_denied
The signature verified — this is never a signing problem. Your service account lacks the
permission the operation requires; grant it in the
Developer Hub and retry. See
Service accounts.
403 feature_not_enabled
403 feature_not_enabled
The signature and the permissions are both fine. The operation belongs to a feature that is not
provisioned for your account, so no permission grant will open it — ask your StableMint contact
to enable it.
Next
Service accounts
Create credentials and grant permissions.
Quickstart
Make your first call.