| Type | When to use it |
|---|---|
PASSKEY | Best default. Biometric, phishing-resistant, usable across the user’s devices via iCloud Keychain / Google Password Manager. |
OAUTH | Your platform already authenticates the user via OIDC (Google, Apple, your own IdP) and you want Grid to trust the same identity. |
EMAIL_OTP | Lowest-friction option. Works on any device with email access — no biometric hardware, identity provider, or client SDK required beyond the code entry field. |
PASSKEY and one EMAIL_OTP per account in v1.
Registration vs. verification
Every credential type starts the same way:POST /auth/credentialscreates the credential record and triggers the out-of-band channel (OTP email sent, WebAuthn attestation stored). The response is a plainAuthMethodfor all three credential types — registration alone does not issue a session.
EMAIL_OTPandOAUTH— callPOST /auth/credentials/{id}/verifywith the OTP value (or a fresh OIDC token) plus aclientPublicKey. The response carries theencryptedSessionSigningKey.PASSKEY— callPOST /auth/credentials/{id}/challengewith yourclientPublicKeyto receive a Grid-issued WebAuthnchallengeandrequestId, runnavigator.credentials.get()against that challenge, then callPOST /auth/credentials/{id}/verifywith the resulting assertion and theRequest-Idheader. Grid bakes theclientPublicKeyfrom the/challengecall into the session-creation payload, so it does not appear on the/verifybody.
POST /auth/credentials create call but otherwise follows the same per-type pattern. EMAIL_OTP re-auth needs a fresh OTP email, so call POST /auth/credentials/{id}/challenge first; PASSKEY re-auth uses the same /challenge (with clientPublicKey) → /verify two-step as the first authentication; OAUTH is the exception — since proof of control is a fresh OIDC token, there’s nothing to pre-issue, so /verify alone suffices.
Passkey
Passkey registration
Passkey registration spans four parties: the client (browser or app), your integrator backend, Grid, and the platform authenticator (Touch ID / Face ID / Windows Hello / a security key). Your backend issues the WebAuthn registration challenge; the first authentication challenge is requested explicitly viaPOST /auth/credentials/{id}/challenge — same call shape used for every subsequent reauthentication.
The challenge on POST /auth/credentials is the one your backend issued (registration only). The challenge used for the WebAuthn assertion comes from POST /auth/credentials/{id}/challenge — Grid bakes the clientPublicKey you send there into the session-creation payload, sealing the resulting session signing key to the device that generated it. Because that key plumbing happens on /challenge, the subsequent /verify call carries only the assertion (plus the Request-Id header).
Client sample code
The client never talks to Grid. It talks to your integrator backend — the snippets below simulate that call withfetch('/my-backend/...'), which your backend then relays to Grid.
WebAuthn → Grid parameter map
These are the fields you need to pass through on each hop. Registration (/auth/credentials):
Browser (credential) | Your backend payload | Grid request body |
|---|---|---|
credential.rawId | credentialId | attestation.credentialId |
response.clientDataJSON | clientDataJson | attestation.clientDataJson |
response.attestationObject | attestationObject | attestation.attestationObject |
response.getTransports() | transports | attestation.transports |
| (backend session state) | challenge | challenge (top-level, not under attestation) |
| (backend-chosen) | nickname | nickname |
| (Grid account id) | — | accountId |
/auth/credentials/{id}/challenge):
| Source | Your backend payload | Grid request body |
|---|---|---|
| (client-generated) | clientPublicKey | clientPublicKey |
/auth/credentials/{id}/verify):
Browser (credential) | Your backend payload | Grid request body |
|---|---|---|
credential.rawId | credentialId | assertion.credentialId |
response.clientDataJSON | clientDataJson | assertion.clientDataJson |
response.authenticatorData | authenticatorData | assertion.authenticatorData |
response.signature | signature | assertion.signature |
response.userHandle | userHandle | assertion.userHandle (optional) |
(from /challenge response) | requestId | Request-Id header |
The WebAuthn spec names the field
clientDataJSON. Grid spells it clientDataJson (lowercase json) for consistency with the rest of the API. The bytes are identical — only the field name changes.Passkey reauthentication
When a session expires the client re-verifies without recreating the credential. Reauthentication uses the same/challenge → /verify shape as the first authentication: generate a fresh client key pair, call POST /auth/credentials/{id}/challenge with the new clientPublicKey, run navigator.credentials.get() against the returned challenge, then call /verify with the assertion and the matching Request-Id header.
OAuth (OIDC)
Use an OAuth credential when your platform already authenticates the user with an OpenID Connect identity provider (Google, Apple, your own IdP) and you want Grid to trust that same identity.OAuth registration
Grid validates the OIDC token signature against the issuer’s JWKS on every call and requiresiat to be no more than 60 seconds older than the request. Use a fresh token for each verify call; cached tokens will fail.
201 AuthMethod with nickname populated from the OIDC token’s email claim.
OAuth verify / reauthentication
POST /auth/credentials/{id}/verify is also the reauthentication path — call it with a fresh OIDC token whenever the session expires.
Email OTP
The lowest-friction credential type — works on any device with email access and requires no biometric hardware, identity provider, or client-side setup beyond an input field for the code.Email OTP registration
Creating the credential triggers an OTP email to the address you pass. The user reads the code off the email and submits it through your UI.In sandbox, the OTP is always
000000 regardless of what’s emailed. Pass "otp": "000000" to skip the email round-trip when scripting tests. The encryptedSessionSigningKey returned in sandbox is a stub — see Client keys.Resending an OTP
If the code expires or the email didn’t arrive, re-issue the challenge withPOST /auth/credentials/{id}/challenge. This sends a fresh OTP email and leaves the AuthMethod otherwise untouched.
Email OTP reauthentication
Same pattern as the first activation: call/challenge to send a new OTP, then /verify with the new code and a fresh clientPublicKey.
Managing credentials
Every Global Account starts with a single credential — the one used in the quickstart. In production, encourage customers to register a second credential of a different type (e.g., an email OTP alongside a passkey) so the account is recoverable if their primary device is lost. Adding, revoking, and rotating credentials after the first all go through the same two-step signed-retry pattern.List credentials
The signed-retry pattern
Adding an additional credential, revoking a credential, revoking a session, and exporting a wallet all share the same shape: Key rules:- Always sign the
payloadToSignbyte-for-byte as Grid returned it. Do not re-parse, re-serialize, or modify whitespace. - Sign with the session private key held on the client — never ship it back to your backend.
- The retry must reach Grid before
expiresAt(typically 5 minutes from issue). - The
requestIdis single-use; reusing one yields401.
Add an additional credential
Requires an active session on an existing credential on the same account. The first call looks identical to the one used to create the first credential; Grid detects the pre-existing credential and responds202 instead of 201.
Client signs the payload
Send
payloadToSign to the client. The client signs with the session signing key from the existing credential’s active session — see signing payloads.Signed retry — credential is created
Re-run the same request with the signature and request id in headers:Response (201): a plain
AuthMethod. For EMAIL_OTP, Grid delivers the OTP email on this signed retry, not on the first call.Activate the new credential
Activate the new credential the same way you would activate the first credential of that type —
EMAIL_OTP and OAUTH go straight to POST /auth/credentials/{id}/verify with a fresh clientPublicKey; PASSKEY first calls POST /auth/credentials/{id}/challenge with the clientPublicKey to get a Grid-issued WebAuthn challenge, then POST /auth/credentials/{id}/verify with the assertion and the Request-Id header.Only one credential of each type (
EMAIL_OTP, PASSKEY) is allowed per internal account in v1. Registering a second credential of the same type returns 400 EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS or 400 PASSKEY_CREDENTIAL_ALREADY_EXISTS.Revoke a credential
A credential is revoked by signing with a session from a different credential on the same account. This prevents a compromised credential from revoking itself to lock the legitimate owner out. An account must keep at least one credential — if only one exists, the revoke call returns400.
Client signs with a different credential's session
The client signs
payloadToSign with the session signing key of an active session on any other credential (not the one being revoked).