Skip to main content
Every signed Global Account action uses two key pairs: This page covers generating the client key pair, sending the public key to your backend, holding the session signing key, and signing payloads. Everything here runs on the client; your integrator backend only relays opaque byte strings.

1. Generate a client key pair

The key pair you generate here is your session key: you keep the private key on the device and use it directly to sign account actions. Grid never transmits a session key, so session-issuing responses carry no key material. Generate it as a P-256 signing key (ECDSA). Generate a fresh key pair for every authentication and session refresh. Where the public key goes depends on the flow:
  • PASSKEY: send it as clientPublicKey on POST /auth/credentials/{id}/challenge.
  • OAUTH: send it as clientPublicKey on POST /auth/credentials/{id}/verify.
  • EMAIL_OTP: put it inside encryptedOtpBundle on POST /auth/credentials/{id}/verify. See Encrypt the OTP code.
  • Session refresh: send it as clientPublicKey on both /auth/sessions/{id}/refresh calls.
Wallet export uses a different kind of key pair. See Wallet export key pair. Keep the private key in device-local secure storage (iOS Keychain, Android Keystore, or in the browser, IndexedDB encrypted under a non-extractable Web Crypto key). Send the public key through your integrator backend as a compressed SEC1 point in lowercase hex: 66 characters, a 02 or 03 prefix followed by the 32-byte X coordinate. Grid rejects an uppercase or uncompressed key with 400 INVALID_INPUT.
For local development, you can generate a P-256 key pair from the command line:
The private key must not leave the device. Your integrator backend only ever sees publicKeyHex.

Wallet export key pair

Wallet export encrypts the wallet credentials to the key you send, so it needs a P-256 key agreement (ECDH) key pair rather than a signing key. Generate a fresh one for each export and discard the private key after decrypting. Send its public key as clientPublicKey on both /export calls, as an uncompressed SEC1 point in lowercase hex: 130 characters, a 04 prefix followed by the 32-byte X and 32-byte Y coordinates. The samples below produce that form. See Exporting a wallet for the full flow.

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 public key, 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. public_key must be the compressed public key in lowercase hex: 66 characters starting with 02 or 03. The step 1 samples produce this form. The OTP login challenge is bound to this exact key, so the same compressed hex must be the publicKey in the Grid-Wallet-Signature stamp on the signed retry (see step 2); a different value there fails the login. In production, an uncompressed key makes the enclave reject the OTP code, even when the code is correct. The first verify call then returns:
Grid returns this same response for other rejected requests too, including a mistyped OTP code. If you get it with a code you know is correct, check the key format first. 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.
The private key of the pair you generated in step 1 stays on the device. It becomes the session signing key once verification completes (see Sign a payloadToSign).

2. Sign a payloadToSign

Grid returns payloadToSign strings from several endpoints:
  • POST /quotes (when the source is a Global Account) — the quote’s paymentInstructions[].accountOrWalletInfo.payloadToSign.
  • POST /auth/credentials/{id}/verify (EMAIL_OTP or SMS_OTP) — 202 response body; use the TEK private key generated for this login.
  • 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 tied EMAIL_OTP email updates — all 202 response bodies.
Stamp the exact UTF-8 bytes of the payloadToSign string as returned (do not re-parse, re-serialize, or trim whitespace). For OTP verification, use the matching TEK private key; for other signed wallet actions, use an active session signing key. Build a Grid wallet signature: take its compressed P-256 public key in lowercase hex, sign the payload with the private scalar, then base64url-encode the signature envelope:
Pass that full stamp as the Grid-Wallet-Signature header on the retry (and, for endpoints that use it, echo the 202 requestId as Request-Id).
Sign with real stamps in sandbox too. Sandbox verifies them exactly as production does. OTP verification requires a stamp over the returned payloadToSign with the TEK private key. Deprecated: the placeholder Grid-Wallet-Signature: sandbox-valid-signature, which some other signed wallet actions still accept in sandbox in place of a real stamp. It will be removed, so send a real stamp on every signed action.
Your backend adds the stamp to the retry request:

Session lifetime

Sessions are valid for 15 minutes by default. The AuthSession.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.
If the device is lost or compromised, the user should add a second credential from a trusted device and revoke the compromised one — see Managing credentials. To end the current browser or app session without touching credentials, see Sessions.