The three-step Apple App Attest handshake that lets an iOS app with no backend, no end-user account, and no secret prove it is a genuine, unmodified build on a real device and mint a per-device client token.
Device auth is off by default and enabled per project. When it is not enabled, every device route returns 400 invalid_request_error (device_attest_not_configured). All three routes are publishable-key only (rd_pk_…); any other credential is rejected 403 permission_error (publishable_key_required).
The flow: request a single-use challenge, then either attest (first-time onboarding) or assert (a returning device). Both mint a token on success. The derived end-user id is device:<hash>.
Every verification failure — a bad attestation chain, a wrong nonce, a reused challenge, a replayed counter — collapses to one opaque 401 authentication_error (invalid_device_attestation). The specific reason is logged, never returned, so a probe learns nothing about which check failed.

POST /v1/device/challenge

Issues a single-use challenge for the next attest or assert. No request body. This step carries the abuse bound: it is rate-limited per key (120/min) and per client IP (60/min), returning 429 rate_limit_error (mint_rate_limit_exceeded) with a Retry-After header. 201 Created:
The challenge is valid for 300 seconds and can be consumed exactly once.

POST /v1/device/attest

Onboarding: verify the App Attest attestation, persist the device public key and sign count, then mint. 201 Created — the same shape as /v1/client-tokens:

POST /v1/device/assert

A returning device: verify an assertion (signature plus a strictly-increasing counter), advance the counter, then mint. 201 Created — the same { token, expires_at, end_user_id } shape as /device/attest.

Errors

Failures use the error envelope:
  • 400 invalid_request_error — device auth not enabled for the project (device_attest_not_configured), or a malformed body (invalid_request).
  • 401 authentication_error — any attestation/assertion verification failure (invalid_device_attestation).
  • 403 permission_error — a non-publishable credential (publishable_key_required).
  • 429 rate_limit_errormint_rate_limit_exceeded (on /device/challenge).