REST API
Request signing
Private v1 routes require an access key and a signing secret (created in the app dashboard). Each request must include a timestamp and an HMAC signature over a canonical string. The signing secret is never sent on the wire after initial creation.
API keys
In the Attestia web app, open Dashboard (or your role workspace) and create an attester or submitter API key. You receive an accessKey and a signingSecret. Copy the signing secret immediately; it is shown only once. Revoke keys from the same screen when you no longer need them.
{
"accessKey": "ak_att_0123456789abcdef",
"signingSecret": "<shown once>",
"key": { "id": "0123456789abcdef", "display": "ak_att_01234567••••••••", "kind": "attester", "..." : "..." },
"warning": "Store signingSecret now (shown once). ..."
}For automation against your own session, you can call POST /api/dashboard/api-keys with Authorization: Bearer <session_access_token> and body { "kind": "attester" | "submitter", "label": "my-bot" }. Revoke with DELETE /api/dashboard/api-keys?id=<key_id> using the same session token.
HTTP headers (signed requests)
| Name | Mandatory | Description |
|---|---|---|
| Attestia-Access-KeyAlias: X-Attestia-Access-Key | YES | Public access key from the dashboard (e.g. ak_att_… or ak_sub_…). |
| Attestia-TimestampAlias: X-Attestia-Timestamp | YES | Unix timestamp in milliseconds. Must fall within the server receive window. |
| Attestia-SignatureAlias: X-Attestia-Signature | YES | Lowercase hex HMAC-SHA256 of the canonical payload, using signingSecret as UTF-8 key. |
Header names are case-insensitive. You may use the X-Attestia-* aliases if your stack requires an X- prefix on custom headers.
Canonical payload
Concatenate exactly four lines (newline character \n between lines, no trailing newline):
1) <Attestia-Timestamp as string, e.g. "1735689600000">
2) <HTTP_METHOD in UPPERCASE, e.g. GET or POST>
3) <path + query exactly as in the request URL, e.g. "/api/v1/attester/media?includeMyScore=1">
4) <lowercase hex SHA-256 of the raw request body bytes interpreted as UTF-8 string (empty string for GET)>Line 3 must match what your HTTP client sends: start with /, include ? and the query string when present. Only the path and query are signed, not the scheme or host (for example under https://attestiaprotocol.xyz/api/v1 the signed path begins with /api/v1/).
signature = HMAC_SHA256(
key = UTF8(signingSecret),
message = UTF8(canonicalPayload)
)
Attestia-Signature = lowercaseHex(signature)Timestamp window
Attestia-Timestamp must be within 30 seconds of server time unless your deployment documentation states otherwise. On 401 responses that indicate clock skew, compare your clock with GET /api/v1/time.
Example: signed GET
import { createHash, createHmac } from "node:crypto";
const ORIGIN = "https://attestiaprotocol.xyz";
const ACCESS_KEY = process.env.ATTESTIA_ACCESS_KEY ?? "";
const SIGNING_SECRET = process.env.ATTESTIA_SIGNING_SECRET ?? "";
function sha256HexUtf8(s: string) {
return createHash("sha256").update(s, "utf8").digest("hex");
}
function signRequest(method: string, pathWithQuery: string, body: string) {
const ts = String(Date.now());
const bodyHash = sha256HexUtf8(body);
const payload = [ts, method.toUpperCase(), pathWithQuery, bodyHash].join(String.fromCharCode(10));
const signature = createHmac("sha256", SIGNING_SECRET).update(payload, "utf8").digest("hex");
return {
headers: {
"Attestia-Access-Key": ACCESS_KEY,
"Attestia-Timestamp": ts,
"Attestia-Signature": signature,
},
};
}
const path = "/api/v1/attester/media";
const url = ORIGIN + path;
const { headers } = signRequest("GET", path, "");
const res = await fetch(url, { headers });
console.log(res.status, await res.text());