Authentication
Every /v1 request is signed. You never send your secret: you send a short-lived token signed with it.
API keys
An API key is a pair, created in the console under API keys or with POST /v1/api_keys:
| Part | Looks like | Where it goes |
|---|---|---|
| Key ID | 7Hx2Qp9LmZ | The api_key query parameter of every request. Public: it identifies the key and grants nothing on its own. |
| Signing secret | your_secret | Only on your server. It signs a token for each request and is shown once, at creation or rotation. |
Signing a request
Each request carries Authorization: Bearer <jwt>, where the JWT is signed with HS256 using the whole secret string as the key.
GET /v1/objects/user-42?api_key=7Hx2Qp9LmZ
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFrXzdIeDJRcDlMbVoifQ.eyJpYXQiOjE3OTAwMDAwMDAsImV4cCI6MTc5MDAwMDA2MCwiaHRtIjoiR0VUIiwiaHR1IjoiL3YxL29iamVjdHMvdXNlci00MiJ9.…The header and claims:
json
{ "alg": "HS256", "typ": "JWT", "kid": "7Hx2Qp9LmZ" }json
{
"iat": 1790000000,
"exp": 1790000060,
"jti": "5f0c8e8e2b7d4c1e9a3d6b2f1c0e4a7b",
"htm": "GET",
"htu": "/v1/objects/user-42"
}| Claim | Required | Meaning |
|---|---|---|
iat | yes | Issued at, in Unix seconds. |
exp | yes | Expiry. At most 300 seconds after iat. |
jti | no | A unique ID for the token. |
htm | no | Binds the token to one HTTP method. |
htu | no | Binds the token to one path (or a full URL, of which only the path is compared). |
kidin the JWT header can replace theapi_keyquery parameter. If you send both, they must match.- The server allows ±60 seconds of clock skew on
iatandexp. - Binding with
htmandhtuis optional but recommended: a leaked token then only works for the one request it was made for. The SDKs always bind, and sign a new 60-second token per request. - Tokens in the query string (
access_token,token,jwt) are rejected, because URLs end up in logs.
Every failure (unknown, inactive or revoked key, bad signature, expired or future-dated token, wrong method or path) is the same generic 401:
json
{ "error": { "code": "unauthorized", "message": "unauthorized" } }Examples
bash
b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }
sign() { # sign METHOD PATH
local now=$(date +%s) h p
h=$(printf '{"alg":"HS256","typ":"JWT","kid":"%s"}' "$ANALYZE_API_KEY" | b64url)
p=$(printf '{"iat":%d,"exp":%d,"htm":"%s","htu":"%s"}' "$now" "$((now + 60))" "$1" "$2" | b64url)
printf '%s.%s.%s' "$h" "$p" \
"$(printf '%s.%s' "$h" "$p" | openssl dgst -sha256 -hmac "$ANALYZE_SECRET" -binary | b64url)"
}
curl "https://api.sigwise.ai/v1/me?api_key=$ANALYZE_API_KEY" \
-H "Authorization: Bearer $(sign GET /v1/me)"ts
import { createHmac, randomUUID } from "node:crypto";
function sign(apiKey: string, secret: string, method: string, path: string): string {
const b64 = (v: object) => Buffer.from(JSON.stringify(v)).toString("base64url");
const now = Math.floor(Date.now() / 1000);
const head = b64({ alg: "HS256", typ: "JWT", kid: apiKey });
const body = b64({ iat: now, exp: now + 60, jti: randomUUID(), htm: method, htu: path });
const sig = createHmac("sha256", secret).update(`${head}.${body}`).digest("base64url");
return `${head}.${body}.${sig}`;
}python
import base64, hashlib, hmac, json, time, uuid
def sign(api_key: str, secret: str, method: str, path: str) -> str:
b64 = lambda v: base64.urlsafe_b64encode(json.dumps(v).encode()).rstrip(b"=").decode()
now = int(time.time())
head = b64({"alg": "HS256", "typ": "JWT", "kid": api_key})
body = b64({"iat": now, "exp": now + 60, "jti": uuid.uuid4().hex, "htm": method, "htu": path})
sig = hmac.new(secret.encode(), f"{head}.{body}".encode(), hashlib.sha256).digest()
return f"{head}.{body}." + base64.urlsafe_b64encode(sig).rstrip(b"=").decode()go
func sign(apiKey, secret, method, path string) string {
enc := base64.RawURLEncoding
now := time.Now().Unix()
head, _ := json.Marshal(map[string]string{"alg": "HS256", "typ": "JWT", "kid": apiKey})
body, _ := json.Marshal(map[string]any{"iat": now, "exp": now + 60, "htm": method, "htu": path})
msg := enc.EncodeToString(head) + "." + enc.EncodeToString(body)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(msg))
return msg + "." + enc.EncodeToString(mac.Sum(nil))
}php
function sign(string $apiKey, string $secret, string $method, string $path): string
{
$b64 = fn (string $v) => rtrim(strtr(base64_encode($v), '+/', '-_'), '=');
$now = time();
$head = $b64(json_encode(['alg' => 'HS256', 'typ' => 'JWT', 'kid' => $apiKey]));
$body = $b64(json_encode(['iat' => $now, 'exp' => $now + 60, 'htm' => $method, 'htu' => $path], JSON_UNESCAPED_SLASHES));
return "$head.$body." . $b64(hash_hmac('sha256', "$head.$body", $secret, true));
}Sign the decoded path
htu is compared with the request's decoded path. For an object ID with characters that need escaping, like user 42, request /v1/objects/user%2042 but sign /v1/objects/user 42.
Rotating and revoking keys
- Rotate (
POST /v1/api_keys/{key_id}/rotate) keeps the key ID and issues a new secret. The old secret stops working immediately. - Deactivate (
PATCHwith{"status": "inactive"}) pauses a key; set it back toactiveto resume. - Revoke (
DELETE) retires a key for good. Its ID is never reissued.
Every console user of the account gets an email when a key is created, rotated or revoked. See API keys.
Console tokens
The SigWise console authenticates people, not integrations: it signs in with an email and password (or Google or GitHub) and gets a console token from the /auth/console/* endpoints. Those endpoints are documented under Console in the API reference, but integrations should always use API keys.
Two-factor authentication and lockout
Console users can turn on two-factor authentication with an authenticator app from the account menu. Once it is on, signing in with a password also needs a 6-digit code, or one of the eight single-use recovery codes shown when you turned it on. Signing in with Google or GitHub uses that provider's own security.
Repeated failed sign-ins lock the account, and the address they came from, out for 15 minutes. The API answers 429 with a Retry-After header.
Deprecated: legacy keys
Keys created before request signing have no signing secret. Until rotated, they still authenticate by sending the old long key as Authorization: Bearer your_key_id or X-API-Key, or by exchanging it at POST /auth/token. Both paths log a warning and will be removed. Rotate a legacy key to get a signing secret.