This page covers generating the client key pair, sending the public key to your backend, holding (or, in the legacy flow, decrypting) the session signing key, and signing payloads. Everything here runs on the client; your integrator backend only relays opaque byte strings.
Client-held session key
In the recommended client-held-key flow, the key pair you generate in step 1 is your session key: you keep the private key on the device and use it directly to sign account actions. There is no session key to receive or decrypt, and Grid never transmits one — so the verify, refresh, and challenge responses omitencryptedSessionSigningKey. This is how EMAIL_OTP and SMS_OTP have always worked; OAUTH and PASSKEY now follow the same model.
To use it, send clientPublicKey in compressed SEC1 form — a 66-character hex string starting with 02 or 03 (the prefix followed by the 32-byte X coordinate). Grid treats a compressed key as a request to adopt it as the session signing key and returns an AuthSession with no encryptedSessionSigningKey. You then skip steps 2–3 and sign each payloadToSign with the private key you kept, exactly as in step 4. Because this key signs directly, generate it as a P-256 signing key (ECDSA) — not the ECDH recipient key the step 1 samples below create for the legacy decrypt flow.
Sending
clientPublicKey in uncompressed SEC1 form (130 hex characters, 04 prefix) selects the deprecated legacy flow, in which Grid seals the session signing key to your public key and returns it as encryptedSessionSigningKey for you to decrypt — steps 2 and 3 below. New integrations should send the compressed key and hold their own session key.1. Generate a client key pair
Generate a fresh P-256 key pair for every authentication, session refresh, and wallet export. The public key is sent to Grid asclientPublicKey — for PASSKEY credentials this happens on POST /auth/credentials/{id}/challenge; for EMAIL_OTP and OAUTH it happens on POST /auth/credentials/{id}/verify; for session refresh it goes on both /auth/sessions/{id}/refresh calls; for wallet export it goes on both /export calls. Keep the private key in device-local secure storage (browser IndexedDB gated by Web Crypto’s non-extractable flag, iOS Keychain, Android Keystore). Send the public key hex-encoded through your integrator backend. For the recommended client-held-key flow, send the compressed form — a 66-character string starting with 02 or 03 (see Client-held session key). For the legacy flow, send the uncompressed form — a 130-character string starting with 04, which the Web Crypto, iOS, and Android APIs below produce natively.
The private key must not leave the device. Your integrator backend only ever sees
publicKeyHex.Encrypt the OTP code (EMAIL_OTP only)
EMAIL_OTP credentials never send the OTP code in plaintext. Instead, the client HPKE-encrypts the code (together with its publicKeyHex) to an enclave key, so the code is unreadable in transit and Grid is only a pass-through.
Grid returns an otpEncryptionTargetBundle whenever it initiates or reissues an OTP challenge, including POST /auth/credentials/{id}/challenge and add-EMAIL_OTP signed-retry responses. First-time EMAIL_OTP wallet bootstrap registration can omit it; if the registration response has no bundle, call POST /auth/credentials/{id}/challenge for that credential before verifying. The bundle is a signed enclave bundle whose data field is hex-encoded JSON carrying the enclave’s HPKE target key as targetPublic. Pull out targetPublic, HPKE-encrypt { otp_code, public_key } to it, and submit the library’s { encappedPublic, ciphertext } output as encryptedOtpBundle on POST /auth/credentials/{id}/verify.
Use an HPKE library so you don’t hand-roll the suite, info, or AAD. The
helper below assumes your crypto layer returns the JSON string Grid expects for
encryptedOtpBundle.
Web (TypeScript)
A production client should also verify the bundle’s
dataSignature against its enclaveQuorumPublic before trusting targetPublic.payloadToSign), so EMAIL_OTP responses omit encryptedSessionSigningKey.
2. Verify the credential and receive the encrypted session signing key
Steps 2 and 3 apply to the legacy flow only (uncompressed
clientPublicKey). In the recommended client-held-key flow the response has no encryptedSessionSigningKey to receive or decrypt — skip to step 4.publicKeyHex to your integrator backend along with whatever the credential type requires (OTP value, OIDC token, or WebAuthn assertion — see Authentication). Your backend calls POST /auth/credentials/{id}/verify and returns the encryptedSessionSigningKey from Grid’s response to the client.
Grid encrypts the session signing key with HPKE (RFC 9180) using the suite:
- KEM: DHKEM(P-256, HKDF-SHA256)
- KDF: HKDF-SHA256
- AEAD: AES-256-GCM
encappedPublicUncompressed || recipientPublicKeyUncompressed.
Sandbox supports the same decryptable
encryptedSessionSigningKey format for production-shaped PASSKEY and OAUTH tests. The legacy Grid-Wallet-Signature: sandbox-valid-signature shortcut is still accepted, but using real session bundles and stamps catches client-side format bugs before production.3. Decrypt the session signing key
4. Sign a payloadToSign
Grid returns payloadToSign strings from several endpoints:
POST /quotes(when the source is a Global Account) — the quote’spaymentInstructions[].accountOrWalletInfo.payloadToSign.POST /auth/credentials(adding an additional credential) — 202 response body.DELETE /auth/credentials/{id},DELETE /auth/sessions/{id},POST /auth/sessions/{id}/refresh,POST /internal-accounts/{id}/export,PATCH /internal-accounts/{id},PATCH /customers/{id}for tiedEMAIL_OTPemail updates — all 202 response bodies.
Grid-Wallet-Signature header on the retry (and, for endpoints that use it, echo the 202 requestId as Request-Id).
In sandbox, send
Grid-Wallet-Signature: sandbox-valid-signature for any signed account action. Sandbox skips the ECDSA check, so you don’t need a real session signing key or an extracted payloadToSign. The signing pattern below applies only to production.Session lifetime
Sessions are valid for 15 minutes by default. TheAuthSession.expiresAt field tells you exactly when the session signing key stops being accepted. After expiry, the client must re-verify the credential (see Authentication) to obtain a fresh session.