API Reference · Identity Service

Identity Backend V2 v2

API Reference for Leonardo's iOS smoke — client compatibility runs against the live server.

Base URL
https://identity.dotspark.app
Auth Mode
AUTH_ENABLED=false
Chain
Paseo Next v2 People
Writer Signer
5HEhiB3hMb8P36L6Edj716fR4wLPUSqoZZpcUrrYEod7f3qu
Purpose
Client compatibility smoke
Spec Version
2026-07-08
BASE https://identity.dotspark.app openapi.json ↓
!
Attestation is disabled for M0. Apple/Google provider verification is intentionally a no-op. JWT account proof and People Chain reads still run end-to-end.
01

Validation Flow

#

The iOS smoke drives this sequence end-to-end. Each step gates the next.

  1. POST /api/v1/auth/challenges  — challenge
  2. POST /api/v1/auth/app-attest/attestations  — returns 202 {} no-op
  3. POST /api/v1/auth/token  — sr25519 proof → JWT
  4. POST /api/v1/usernames/available?version=v1  — JWT-gated chain read
  5. POST /api/v1/usernames  — 202 outbox reservation
The full write path is verified end-to-end on this deployment (2026-07-09): a real-signature registration returned 202, was drained by the chain-writer (proxy-signed PeopleLite.attest), and finalized on People Chain — confirmed in Resources.UsernameOwnerOf. Invalid or dummy attestation payloads are still rejected by the runtime.
02

Liveness & Readiness

#
GET /healthcheck
200 public

Process health probe. No dependencies checked.

Response · 200 · application/json
json
{
  "status": "ok",
  "service": "identity-service"
}
200 OK
Process is up. No dependencies checked.
GET /livez
200 public

Kubernetes liveness probe — "should this pod be restarted".

Response · 200 · application/json
json
{
  "status": "alive",
  "service": "identity-service"
}
200 OK
Process is alive.
GET /readyz
200 503 public

Kubernetes readiness probe — checks db and chain subsystems.

Response · 200 · application/json
json
{
  "status": "ready",
  "service": "identity-service",
  "db": "up",
  "chain": "up"
}
Response · 503 · application/json
json
{
  "status": "unavailable",
  "service": "identity-service",
  "db": "down"
}
200 OK
Instance can take traffic; db and chain reachable.
503 UNAVAILABLE
A required subsystem is unavailable.
03

Discovery

#
GET /.well-known/jwks.json
200 public

Publish the Ed25519 public key so siblings can verify tokens.

Response · 200 · application/json
json
{
  "keys": [
    {
      "kty": "OKP",
      "crv": "Ed25519",
      "use": "sig",
      "alg": "EdDSA",
      "kid": "...",
      "x": "..."
    }
  ]
}
200 OK
JWT verification keyset (Ed25519 / OKP).
GET /api/v1/attester
200 public

GET /api/v1/attester — the attester authority public key (0x+hex, public).

Response · 200 · application/json
json
{
  "attester": "0xe4cd20d6d1e0e119d63a943afd2d7496fbbb0ac8e7cd99c3c5f16b63a68e7432"
}
200 OK
The on-chain attester account the backend writes as.
04

Authentication

#
POST /api/v1/auth/app-attest/attestations
202 401 public

Register an App Attest key: verify the attestation object and persist the credential key (no-op 202 {} while AUTH_ENABLED=false).

FieldTypeRequiredDescription
keyIdstringyesBase64 key identifier (SHA-256 of the credential public key).
challengestringyesBase64 challenge previously returned from /auth/challenges.
attestationstringyesBase64 CBOR attestation object from the platform.
Request Body
json
{
  "keyId": "s/134MbeEEZDZKCvOTf+jZgNhpoDwdXZ8cKfTym8FUg=",
  "challenge": "challenge-from-/auth/challenges",
  "attestation": "base64-attestation-object"
}
Response · 202 · application/json
json
{}
Response · 401 · application/json
json
{
  "_tag": "VERIFY_ATTESTATION_FAILED",
  "error": "attestation nonce mismatch"
}
202 ACCEPTED
Accepted. With attestation enabled, the key is verified against Apple and stored; while disabled, the payload is not verified.
401 attestation nonce mismatch
Attestation rejected (hard mode), or the challenge was unknown, expired, or already used. Soft mode logs failed verdicts and returns 202.
POST /api/v1/auth/challenges
201 public

Issue a fresh single-use challenge (201).

Response · 201 · application/json
json
{
  "challenge": "base64-32-byte-challenge"
}
201 CREATED
A fresh single-use challenge. No body required.
POST /api/v1/auth/token
200 401 403 503 public

Verify challenge + client proof and issue an access + refresh token pair.

HeaderTypeRequiredDescription
Auth-ClientIdstringyesBase64 of the 32-byte sr25519 public key.
Auth-ClientProofstringyesBase64 of the 64-byte sr25519 signature over the raw body.
Auth-ChallengestringyesChallenge previously minted via /auth/challenges.
Auth-iOS-PackagestringnoiOS bundle id, e.g. io.pcf.polkadotapp. Selects the App Attest verification path.
Auth-PayloadstringnoBase64 App Attest assertion (iOS) or the raw classic Play Integrity token (Android play-integrity), verified when attestation is enabled.
Auth-iOS-KeyIdstringnoBase64 App Attest key id registered via /auth/app-attest/attestations.
Auth-Android-PackagestringnoAndroid package name; required for play-integrity and sets the JWT platform claim for Android clients.
Auth-Attestation-TypestringnoAndroid attestation dispatch: key-attestation verifies the attestationChain field in the JSON body (base64 DER, leaf first); play-integrity verifies the token in Auth-Payload with self-managed response keys.
Request Body
json
{}
Response · 200 · application/json
json
{
  "token": "jwt",
  "refreshToken": "opaque-base64-token"
}
Response · 401 · application/json
json
{
  "error": "UNAUTHORIZED",
  "message": "unauthorized"
}
Response · 403 · application/json
json
{
  "error": "INTEGRITY_FAILED",
  "message": "attestation rejected"
}
Response · 503 · application/json
json
{
  "error": "ATTESTATION_CRL_UNAVAILABLE",
  "message": "attestation CRL unavailable"
}
200 OK
Verified. Access JWT + opaque refresh token.
401 UNAUTHORIZED
Bad or spent challenge, or invalid client proof.
403 INTEGRITY_FAILED
Platform attestation failed verification (hard enforcement).
503 ATTESTATION_CRL_UNAVAILABLE
Android attestation revocation list unavailable; retry with a fresh challenge (hard enforcement).
POST /api/v1/auth/token/refresh
200 401 public

Rotate a refresh token, returning a fresh access + refresh pair.

FieldTypeRequiredDescription
refreshTokenstringyesOpaque refresh token issued by /auth/token or a prior refresh.
Request Body
json
{
  "refreshToken": "opaque-base64-token"
}
Response · 200 · application/json
json
{
  "token": "jwt",
  "refreshToken": "new-opaque-base64-token"
}
Response · 401 · application/json
json
{
  "error": "UNAUTHORIZED",
  "message": "unauthorized"
}
200 OK
Rotated. New access JWT + new opaque refresh token.
401 UNAUTHORIZED
Unknown, expired, or already-spent refresh token.
05

Usernames

#
GET /api/v1/registration/queue
200 401 404 429 bearer JWT

Queue standing for the caller's pending username claim. This route exists only on deployments with the registration queue enabled (QUEUE_ENABLED); with the queue disabled the path serves the standard plain-text 404. Clients should treat ANY 404 here as "not (or no longer) queued" and assume the registration is proceeding — queued claims keep draining even if the queue is later disabled. When an account has several queued claims, the response reports the earliest-enqueued one until it drains.

Auth Bearer JWT required in the Authorization header.
Response · 200 · application/json
json
{
  "queuePosition": 17,
  "group": 2,
  "estimatedIterationsRemaining": 5
}
Response · 404 · application/json
json
{
  "error": "No queue entry found"
}
200 OK
The caller's queued claim: position, priority group, and the estimated advancer iterations until it is promoted for on-chain registration.
401 UNAUTHORIZED
Missing or invalid bearer token (the frozen problem-details trio).
404 No queue entry found
The account has no queued registration (JSON body), or the deployment runs queue-disabled (plain-text body). Either way: stop polling, the claim is proceeding without the queue.
429 RATE_LIMITED
Subject rate limit exceeded (problem details with Retry-After).
POST /api/v1/usernames
200 202 400 401 409 429 500 502 503 bearer JWT

Reserve a lite username and enqueue it for on-chain registration.

Auth Bearer JWT required in the Authorization header.
FieldTypeRequiredDescription
candidateAccountIdstringyesSS58 beneficiary account that will own the username.
usernamestringyesBase username (lowercase ASCII letters, 6..=29 chars).
preferredDigitsstringnoOptional two-digit suffix, e.g. "07"; random-free if omitted.
candidateSignaturestringyes0x-hex 64-byte sr25519 signature proving control of the candidate.
ringVrfKeystringyes0x-hex ring VRF key.
proofOfOwnershipstringyes0x-hex 64-byte ownership proof.
consumerRegistrationSignaturestringyes0x-hex 64-byte consumer registration signature.
identifierKeystringyes0x-hex 65-byte identifier key.
lifetimePoUDVoucherstringnoOptional single-use registration voucher (the INSTANT lane): bypasses the PoUD gate and the registration queue. Ignored unless REGISTRATION_VOUCHERS_ENABLED.
dotnsobjectno
dotns.*TypeRequiredDescription
signaturestringyes0x-hex 64-byte signature.
signedAtnumberyesUnix timestamp the reservation was signed at.
reservedUsernamestringnoOptional reserved-name override.
Request Body
json
{
  "candidateAccountId": "5FbRAkhDvNVecNzHLFxBNXFXNwvBaV69S1W3nfBbnxYypkkT",
  "username": "tallesx",
  "preferredDigits": "07",
  "candidateSignature": "0x...64 bytes...",
  "ringVrfKey": "0x...",
  "proofOfOwnership": "0x...64 bytes...",
  "consumerRegistrationSignature": "0x...64 bytes...",
  "identifierKey": "0x...65 bytes...",
  "lifetimePoUDVoucher": "base64url-voucher-key",
  "dotns": {
    "signature": "0x...64 bytes...",
    "signedAt": 1780000000,
    "reservedUsername": "reservedname"
  }
}
Response · 200 · application/json
json
{
  "registrationOutcome": "PAYMENT_REQUIRED",
  "paymentAddress": "5F...",
  "amountRequired": "10000000000"
}
Response · 202 · application/json
json
{
  "base_username": "tallesx",
  "digits": "07",
  "username": "tallesx.07"
}
Response · 400 · application/json
json
{
  "type": "https://problems-registry.smartbear.com/invalid-body-property-value",
  "title": "Invalid Body Property Value",
  "detail": "The request body contains an invalid body property value.",
  "status": 400,
  "errors": [
    {
      "detail": "Invalid signature.",
      "pointer": "#/candidateSignature"
    }
  ]
}
Response · 409 · application/json
json
{
  "error": "Preferred digits 07 already taken for username tallesx"
}
Response · 500 · application/json
json
{
  "error": "Failed to persist username registration"
}
Response · 502 · application/json
json
{
  "error": "iOS DeviceCheck verification failed"
}
Response · 503 · application/json
json
{
  "error": "Failed to mark iOS device as registered with Apple DeviceCheck"
}
200 OK
The device must pay to register (hard-mode DeviceCheck: free slot already used, or — with the payment lane on — a missing device token). Not an error — a 200 outcome. With PAYMENT_LANE_ENABLED the body carries the deposit instructions (paymentAddress, amountRequired in planck as a string) and the claim is stored; registration proceeds automatically on the confirmed deposit (poll GET /api/v1/usernames/payment-status). Lane off: the bare outcome, no payment path.
202 ACCEPTED
Reservation accepted into the outbox for on-chain registration. With the registration queue enabled (QUEUE_ENABLED) and the queue service live, the claim waits in the balance-priority queue and the body additionally carries registrationOutcome: "QUEUED" plus the claim's queue standing (poll GET /api/v1/registration/queue for updates). A down queue service falls back to the direct registration path (no queue fields). With REGISTRATION_VOUCHERS_ENABLED, a valid lifetimePoUDVoucher bypasses the device gate and the queue: the reservation goes straight to the writer and the body carries registrationOutcome: "INSTANT".
400 WRONG_DATA
Validation failed (problem details with errors), malformed JSON, or a lifetimePoUDVoucher that is unknown, already used, or expired ({"error": "Voucher already used"} — a voucher failure rejects the claim outright).
401 UNAUTHORIZED
Missing or invalid bearer token (the frozen problem-details trio), or hard-mode DeviceCheck required a usable Device-Token-iOS and none was present (payment lane off — with PAYMENT_LANE_ENABLED a missing token resolves to the 200 PAYMENT_REQUIRED outcome instead).
409 Preferred digits 07 already taken for username tallesx
Preferred digits taken, no digits available, or username taken.
429 RATE_LIMITED
Subject rate limit exceeded (problem details with Retry-After).
500 Failed to persist username registration
Persistence failure or unexpected error.
502 iOS DeviceCheck verification failed
Hard-mode DeviceCheck could not reach Apple to resolve the device.
503 Failed to mark iOS device as registered with Apple DeviceCheck
The DeviceCheck free slot could not be marked used at Apple after a successful gate (upstream write failure; retryable).
POST /api/v1/usernames/available?version=<version>
200 400 401 429 500 bearer JWT

Availability for each requested base.

Auth Bearer JWT required in the Authorization header.
QueryTypeRequiredDescription
versionstringnoResponse version; v1 is the only (and default) option. The legacy flat v0 record was removed 2026-07-23.
FieldTypeRequiredDescription
usernamesstring[]yesCandidate base usernames to validate and check (max 100).
Request Body
json
{
  "usernames": [
    "tallesx",
    "abc"
  ]
}
Response · 200 · application/json
json
{
  "_tag": "v1",
  "value": {
    "tallesx": {
      "status": "AVAILABLE",
      "availableDigits": [
        1,
        2,
        3
      ]
    },
    "abc": {
      "status": "INVALID"
    }
  }
}
Response · 400 · application/json
json
{
  "type": "https://problems-registry.smartbear.com/invalid-body-property-value",
  "title": "Invalid Body Property Value",
  "detail": "The request body contains an invalid body property value.",
  "status": 400,
  "errors": [
    {
      "detail": "Too big: expected array to have <=100 items",
      "pointer": "#/usernames"
    }
  ]
}
Response · 500 · application/json
json
{
  "error": "Oops! Something went wrong."
}
200 OK
Per-base availability read from People Chain UsernameOwnerOf plus pending outbox reservations, tagged {_tag: "v1", value} with availableDigits.
400 WRONG_DATA
Invalid version (query dialect), invalid usernames (body dialect), or malformed JSON.
401 UNAUTHORIZED
Missing or invalid bearer token (the frozen problem-details trio; a deliberate hardening divergence — legacy served this route unauthenticated).
429 RATE_LIMITED
Subject rate limit exceeded (problem details with Retry-After).
500 Oops! Something went wrong.
People Chain failure or reservation-outbox failure.
GET /api/v1/usernames/payment-status
200 401 404 429 bearer JWT

Whether the deposit for the caller's PAYMENT_REQUIRED claim has been detected on-chain. Poll after receiving registrationOutcome: "PAYMENT_REQUIRED". This route exists only on deployments with the payment lane enabled (PAYMENT_LANE_ENABLED); disabled, the path serves the standard plain-text 404. An expired quote answers 404 — re-claim for fresh deposit instructions.

Auth Bearer JWT required in the Authorization header.
Response · 200 · application/json
json
{
  "status": "PENDING"
}
Response · 404 · application/json
json
{
  "error": "No active payment request"
}
200 OK
PENDING: not yet detected, keep polling. CONFIRMED: deposit detected, username registration is in progress (~15s).
401 UNAUTHORIZED
Missing or invalid bearer token (the frozen problem-details trio).
404 No active payment request
No active payment request for this account (JSON body), or the deployment runs payment-disabled (plain-text body).
429 RATE_LIMITED
Subject rate limit exceeded (problem details with Retry-After).
06

Proof of compute

#
POST /api/v1/poc/issue
201 404 public

Issue a proof-of-compute puzzle for an unauthenticated caller.

Response · 201 · application/json
json
{
  "sessionId": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed",
  "timestamp": 1700000000000,
  "difficulty": 16,
  "checksum": "c8828951fd6c123fdbf6501f111d27dd3f260839344a7370e0dd8f20e2c40482"
}
201 CREATED
A fresh puzzle to solve. Mine a counter whose sha256(sessionId || timestamp || counter) has at least difficulty leading zero bits, then send Proof-Of-Compute: base64(sessionId:timestamp:difficulty:counter:checksum) on the search request. Callers holding an identity-service JWT do not need a puzzle.
404 NOT_FOUND
Proof of compute is disabled on this deployment (POC_ENABLED=false): the route is not mounted and the service-wide plain-text 404 answers instead.
07

Invitation Tickets

#
POST /api/v1/invitation-ticket/claim
200 400 401 409 422 429 500 bearer JWT

Claim an invitation ticket for a DIM, returning a signature (JWT-gated).

Auth Bearer JWT required in the Authorization header.
FieldTypeRequiredDescription
whostringyesSS58 address to claim a ticket for (any valid SS58 prefix).
dimstringyesDIM to claim a ticket for: Game or ProofOfInk.
Request Body
json
{
  "who": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
  "dim": "Game"
}
Response · 200 · application/json
json
{
  "publicKey": "0xda8ab326da384dd49d5f12543b58acae730af7388b9348c51af6ee3a0962864d",
  "inviter": "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM",
  "dim": "Game",
  "network": "paseo",
  "claimedBy": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
  "createdAt": "2026-07-01T10:20:30.400Z",
  "claimedAt": "2026-07-02T11:00:00.000Z",
  "signature": "0xa4a506e96aff250724590ef9527d8117ea2e9d633e813d22d32ad17e0b5c253c406cb22baf62283c6987f81b9bb55ea75f3bdff0259d84e5c282728043f77c88",
  "remaining": 41
}
200 OK
Ticket claimed: the oldest available ticket of the (dim, network) pool, atomically flipped to claimed, with an sr25519 signature by the ticket key over the claimant's decoded 32-byte account id.
400 WRONG_DATA
Body validation failed (RFC 9457 problem details with per-field errors); or plain-text Malformed JSON in request body when the body is not JSON.
401 UNAUTHORIZED
Missing/malformed Authorization header or failed token verification (RFC 9457 problem details).
409 CONFLICT
Ticket race lost — a concurrent request claimed the contended ticket.
422 422
Pool exhausted — no available ticket in the (dim, network) pool.
429 RATE_LIMITED
Per-subject rate limit exceeded (RFC 9457 problem details, with Retry-After).
500 INTERNAL
Unexpected internal failure (opaque legacy body).
08

DIM Tickets

#
POST /api/v1/dim-ticket
200 400 401 409 422 429 500 bearer JWT

Request a DIM ticket for an address (JWT-gated).

Auth Bearer JWT required in the Authorization header.
FieldTypeRequiredDescription
whostringyesSS58 address to request a ticket for. Only non-emptiness is validated here; SS58 validity is checked later (an invalid address is a 500 — legacy quirk).
dimstringyesDIM to request a ticket for: Game or ProofOfInk.
Request Body
json
{
  "who": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
  "dim": "Game"
}
Response · 200 · application/json
json
{
  "ticket": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
  "who": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
  "inviter": "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM",
  "network": "paseo",
  "dim": "Game",
  "status": "PENDING",
  "registered": false,
  "createdAt": "2026-07-01T10:20:30.400Z",
  "updatedAt": "2026-07-01T10:20:30.400Z"
}
200 OK
Ticket accepted as PENDING; the single-instance chain writer registers it on People Chain asynchronously (poll the status route).
400 WRONG_DATA
Body validation failed (RFC 9457 problem details with per-field errors); or plain-text Malformed JSON in request body when the body is not JSON. Note SS58 validity is NOT checked here — a well-formed but invalid address is a 500 (legacy quirk).
401 UNAUTHORIZED
Missing/malformed Authorization header or failed token verification (RFC 9457 problem details).
409 CONFLICT
The address already holds a ticket.
422 422
The inviter's on-chain invite quota is exhausted.
429 RATE_LIMITED
Per-subject rate limit exceeded (RFC 9457 problem details, with Retry-After).
500 INTERNAL
Unexpected internal failure (opaque legacy body) — including an SS58-invalid who, which legacy validated in the shell, not the schema.
GET /api/v1/dim-ticket/{who}
200 401 404 429 500 bearer JWT

Look up a DIM ticket's registration status (JWT-gated).

Auth Bearer JWT required in the Authorization header.
PathTypeRequiredDescription
whostringyesSS58 address the ticket was requested for (not validated — an arbitrary string is looked up verbatim, exactly like legacy).
Response · 200 · application/json
json
{
  "ticket": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
  "inviter": "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM",
  "network": "paseo",
  "dim": "Game",
  "status": "REGISTERED",
  "registered": true,
  "onchainData": {
    "blockIndex": "12345-7",
    "blockNumber": 12345,
    "blockHash": "0xabababababababababababababababababababababababababababababababab",
    "eventIndex": 7
  },
  "createdAt": "2026-07-01T10:20:30.400Z",
  "updatedAt": "2026-07-02T11:00:00.000Z"
}
200 OK
The ticket's current status. DB SUBMITTING reports as SUBMITTED on the wire; registered is the deprecated boolean mirror of status == "REGISTERED"; onchainData is null until registration.
401 UNAUTHORIZED
Missing/malformed Authorization header or failed token verification (RFC 9457 problem details).
404 NOT_FOUND
No ticket row for this address.
429 RATE_LIMITED
Per-subject rate limit exceeded (RFC 9457 problem details, with Retry-After).
500 INTERNAL
Unexpected internal failure (opaque legacy body).
09

TURN

#
POST /api/v1/turn/issue
201 400 401 429 bearer JWT

Issue short-lived TURN credentials for WebRTC ICE negotiation (JWT-gated).

Auth Bearer JWT required in the Authorization header.
FieldTypeRequiredDescription
regionHintstringnoOptional region hint (reserved for future use; accepted and ignored).
Request Body
json
{
  "regionHint": "eu-west"
}
Response · 201 · application/json
json
{
  "servers": [
    "stun:stun.example.com:3478",
    "turn:turn.example.com:3478?transport=udp"
  ],
  "username": "1784757652:0a79e3412921701a",
  "password": "qmg5g7d1bXzY0qZkRUqtIPEIKjA=",
  "ttl": 1800
}
201 CREATED
Credentials minted: username is {unixExpiry}:{hexId} (expiry = now + TTL), password is the base64 HMAC over username under the secret shared with the TURN relay (the coturn REST-API construction), servers echoes the configured ICE server list.
400 WRONG_DATA
Body validation failed (RFC 9457 problem details with per-field errors); or plain-text Malformed JSON in request body when the body is present but not JSON. A request with no JSON content-type skips validation entirely and succeeds (legacy quirk).
401 UNAUTHORIZED
Missing/malformed Authorization header or failed token verification (RFC 9457 problem details).
429 RATE_LIMITED
Per-subject rate limit exceeded (RFC 9457 problem details, with Retry-After).
POST /api/v1/turn/issue-with-proof
201 400 403 429 503 public

Redeem a personhood proof for the same credentials as /turn/issue.

FieldTypeRequiredDescription
productIdstringyesThe product the proof was made for. Its context is the one the proof is verified under, so a product proves under its own identifier. Must be one this deployment accepts.
collectionstringyesHex-encoded 32-byte collection id from the proof's TrUAPI ringLocation. Only the canonical People Lite and People collections are accepted.
proofstringyesRing-VRF proof over the derived message (hex), exactly as the host returns it — raw signature bytes, with no SCALE length prefix.
ringIndexnumberyesRing containing the proving member, used to locate the root.
ringRevisionnumberyesRevision of that ring the proof was made against, as the host reports it. Only that revision's root is tried, so a revision this deployment no longer holds is refused without verifying anything.
timestampnumberyesClient Unix seconds, bound into the proved message. Must be within the server's accepted skew.
Request Body
json
{
  "productId": "vox.dot",
  "collection": "0x706f703a706f6c6b61646f742e6e6574776f726b2f70656f706c652d6c697465",
  "proof": null,
  "ringIndex": 0,
  "ringRevision": 3,
  "timestamp": 1784757652
}
Response · 201 · application/json
json
{
  "servers": [
    "stun:stun.example.com:3478",
    "turn:turn.example.com:3478?transport=udp"
  ],
  "username": "1784757652:0a79e3412921701a",
  "password": "qmg5g7d1bXzY0qZkRUqtIPEIKjA=",
  "ttl": 1800
}
201 CREATED
Proof accepted; returns servers, username, password, and the configured TTL. Credentials expire ttl seconds after issuance; no alias appears in the response.
400 WRONG_DATA
Unparseable body, invalid hex, a collection outside the canonical People Lite/People allowlist, a proof that is not a single-context ring-VRF signature, a product this deployment does not accept, or a timestamp outside the accepted skew (RFC 9457 problem details).
403 FORBIDDEN
The proof did not verify against the named ring root under that product's context — including when (ringIndex, ringRevision) is outside the roots this deployment still holds (RFC 9457 problem details; deliberately unspecific).
429 RATE_LIMITED
This person's redemption limit was exceeded (RFC 9457 problem details, with Retry-After).
503 UNAVAILABLE
Verification unavailable: no ring-root snapshot yet (chain unreachable since boot), the bounded waiter queue is full, or all verification slots remained busy for the bounded wait. Saturation responses include Retry-After.
10

Notifications

#
POST /api/v1/notify
200 400 401 429 bearer JWT

Order mirrors the sibling services: verified JWT (401 problem details) → body parse (400 plain text on malformed JSON) → body validation (400 problem details with per-field errors) → per-subject rate limit (429 with Retry-After). Beyond that the relay always answers 200: on provider failure it preserves the legacy success: false body instead of an error status.

Auth Bearer JWT required in the Authorization header.
FieldTypeRequiredDescription
deviceTokenstringyesRecipient device token (APNs hex or FCM token); platform is auto-detected.
pushIdstringyesOpaque push id echoed to the client app (32 or 64 hex chars).
messagestringyesHex-encoded, already-encrypted message body (optional 0x prefix).
platformstringnoExplicit platform override; auto-detected from the token when omitted.
bundlerIdstringnoAPNs topic override (the app bundle id); VoIP derives <topic>.voip.
voipbooleannoEnable the iOS VoIP push type for a call.
Request Body
json
{
  "deviceToken": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
  "pushId": "5d41402abc4b2a76b9719d911017c592",
  "message": "0x1234567890abcdef",
  "platform": "ios",
  "bundlerId": "io.parity.brevity",
  "voip": false
}
Response · 200 · application/json
json
{
  "success": true,
  "platform": "ios",
  "sent": 1,
  "failed": 0,
  "messageId": null,
  "errors": null
}
200 OK
Relay accepted the request. The push result is echoed verbatim from the provider — including the legacy behavior of returning 200 with success: false on provider failure, so a non-200 status never signals a delivery failure.
400 WRONG_DATA
Body validation failed (RFC 9457 problem details with per-field errors); or plain-text Malformed JSON in request body when the body is not JSON.
401 UNAUTHORIZED
Missing/malformed Authorization header or failed token verification (RFC 9457 problem details).
429 RATE_LIMITED
Per-subject rate limit exceeded (RFC 9457 problem details, with Retry-After).
06

Error Model

#

All non-2xx responses share a single envelope. error is a stable machine code; message is a human-readable detail that may vary.

Error JSON Shape
json
{
  "error": "WRONG_DATA",
  "message": "human-readable detail"
}
400 WRONG_DATA
Malformed body/header, invalid username, empty search prefix.
401 UNAUTHORIZED
Missing/invalid bearer token, bad challenge, bad proof, spent refresh token.
403 FORBIDDEN
candidateAccountId does not match JWT subject.
409 CONFLICT
Preferred digits or username already taken/reserved.
429 RATE_LIMITED
Per-route rate limiter exceeded.
500 INTERNAL
Unexpected backend/database/chain failure.
07

Smoke Commands

#

Bare-bones curl sequence that exercises the public surface of the service. Safe to run from any shell.

smoke.sh
BASE="https://identity.dotspark.app"

curl -fsS "$BASE/healthcheck"
curl -fsS "$BASE/readyz"
curl -fsS -X POST "$BASE/api/v1/auth/challenges"
curl -fsS "$BASE/api/v1/attester"
curl -fsS "$BASE/.well-known/jwks.json"
Full token and username calls require the mobile app or a client that can produce the sr25519 proof headers. The bundled repo example can drive the flow against a base URL.