REST API
Endpoints
All paths below are rooted at https://attestiaprotocol.xyz/api/v1. Private routes require request signing. Rate limits are per access key (see Limits & errors).
Endpoint index
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /ping | None | Connectivity test. |
| GET | /time | None | Server time (ms). |
| GET | /health | None | Service metadata. |
| GET | /attester/media | Signed + stake | List open assets. |
| GET | /attester/media/:id | Signed + stake | Single open asset. |
| GET | /attester/media/stream | Signed + stake | SSE stream of open assets. |
| POST | /attester/media/:id/scores | Signed + stake | Submit a score for an open asset. |
| POST | /submitter/media | Signed + stake | Register media asset. |
https://attestiaprotocol.xyz/api/v1/attester/media
List open media (attester)
Counts toward your per-key rate limit (Limits & errors).
| Name | Type | Mandatory | Description |
|---|---|---|---|
| includeMyScore | INT | NO | If 1, adds myScore for the key wallet when you already submitted a score for that asset. |
Request headers
Attestia-Access-Key, Attestia-Timestamp, Attestia-Signature — signing string uses GET and path+query exactly as sent, e.g. /api/v1/attester/media?includeMyScore=1, empty body hash.
Response 200
{
"assets": [
{
"id": "…",
"title": "…",
"mediaContext": "…",
"contentHash": "0x…",
"uri": "ipfs://…",
"status": "open",
"mediaType": "image",
"turnaround": "standard",
"scoreCount": 3,
"myScore": { "submitted": true, "deepfakeRiskScore": 42, "…": "…" }
}
],
"stake": { "stakedWei": "…", "minStakeWei": "…" }
}https://attestiaprotocol.xyz/api/v1/attester/media/:id
Get open media by id (attester)
| Name | Type | Mandatory | Description |
|---|---|---|---|
| id | STRING | YES | Path segment: asset id. |
| includeMyScore | INT | NO | Same as list endpoint. |
404 if the asset is not in status open.
https://attestiaprotocol.xyz/api/v1/attester/media/stream
Open media stream (attester, SSE)
Response Content-Type: text/event-stream. Same signing headers as GET; use the exact path /api/v1/attester/media/stream (no query) unless you add one.
Attester workers can use the new-open-assets delta event as the trigger that newly registered media is available to review. The open-assets snapshot is also sent whenever the open set changes (use it for reconciliation or initial state). If your deployment exposes the websocket gateway, it carries the same authenticated feed over WebSocket.
Events
| event | data (JSON) |
|---|---|
| ready | { stake: { stakedWei, minStakeWei } } |
| new-open-assets | { ids: string[], assets: MediaAsset[] } delta when the open set expands |
| open-assets | { ids: string[], assets: MediaAsset[] } snapshot |
| ping | { t: number } heartbeat |
| error | { message: string } |
https://attestiaprotocol.xyz/api/v1/attester/media/:id/scores
Submit score (attester)
Submit a verification score for an open asset. The request must be signed like other v1 routes; the body JSON must be the exact string you sign.
Off-chain EAS attestations are generated and signed by the Attestia server using the attester’s linked Privy wallet. API clients should not construct or submit an off-chain attestation payload.
Request headers
Attestia-Access-Key, Attestia-Timestamp, Attestia-Signature — signing string uses POST and path exactly as sent, e.g. /api/v1/attester/media/asset_123/scores, and SHA-256 of the JSON body.
| Field | Type | Mandatory | Description |
|---|---|---|---|
| deepfakeRiskScore | NUMBER | YES | 0..100 (percentage) |
| algorithm | STRING | YES | Short algorithm/model label |
| notes | STRING | NO | Optional notes |
| signature | STRING | NO | Reserved for future use (ignored today). |
The server derives attesterAddress from the API key wallet and will reject attempts to submit scores “as” another address.
Requirements: your attester must have a Privy wallet linked in the dashboard (so the server can sign), and meet the stake gate for attesters.
Response 200
{
"score": { "id": "…", "assetId": "…", "attesterAddress": "0x…", "deepfakeRiskScore": 42, "…" : "…" },
"aggregate": { "…" : "…" }
}https://attestiaprotocol.xyz/api/v1/submitter/media
Register media (submitter)
Body JSON must be the string you sign. ownerEmail and ownerAddress are always taken from the access key (ignored if present in JSON).
| Field | Type | Mandatory | Description |
|---|---|---|---|
| title | STRING | YES | |
| mediaContext | STRING | YES | Short description hint (≤ 200 chars; may be empty). |
| contentType | STRING | NO | Detector content hint: auto | face | landscape | … (default auto). |
| contentHash | STRING | YES | bytes32 hex 0x + 64 hex. |
| uri | STRING | YES | Typically ipfs://… from /api/media/upload. |
| contributorAttestationUid | STRING | YES | bytes32 uid. |
| verificationDeadline | STRING | YES | ISO-8601, must be in the future. |
| mediaType | ENUM | NO | image | audio | video_short | video_long | text |
| turnaround | ENUM | NO | standard | priority_5min |
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: {
"Content-Type": "application/json",
"Attestia-Access-Key": ACCESS_KEY,
"Attestia-Timestamp": ts,
"Attestia-Signature": signature,
},
};
}
const path = "/api/v1/submitter/media";
const body = JSON.stringify({
title: "My asset",
mediaContext: "Context here",
contentHash: "0x" + "ab".repeat(32),
uri: "ipfs://…",
contributorAttestationUid: "0x" + "cd".repeat(32),
verificationDeadline: new Date(Date.now() + 12 * 3600 * 1000).toISOString(),
mediaType: "image",
turnaround: "standard",
});
const { headers } = signRequest('POST', path, body);
const res = await fetch(ORIGIN + path, { method: 'POST', headers, body });
console.log(res.status, await res.text());