Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 63 additions & 12 deletions mintlify/snippets/global-accounts/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,9 @@ The lowest-friction credential type — works on any device with email access an

### Default Email OTP credential

Grid creates the first `EMAIL_OTP` credential when the Global Account is provisioned. The credential uses the customer email on file for the internal account. To authenticate with it, send an OTP challenge and then verify the code.
Grid creates the first `EMAIL_OTP` credential when the Global Account is provisioned. The credential uses the customer email on file for the internal account. To authenticate with it, send an OTP challenge, then verify using the secure encrypted OTP flow.

The client never sends the plaintext OTP code. Instead, it HPKE-encrypts the code (together with a fresh public key) to an enclave bundle returned from the challenge. The server is a pass-through and never sees the plaintext.

```mermaid
sequenceDiagram
Expand All @@ -481,14 +483,20 @@ sequenceDiagram
C->>IB: POST /my-backend/otp/challenge { credentialId }
IB->>G: POST /auth/credentials/{id}/challenge
G->>E: deliver OTP email (to customer email on file)
G-->>IB: 200 AuthMethod
IB-->>C: ok
G-->>IB: 200 AuthMethod + otpEncryptionTargetBundle
IB-->>C: { otpEncryptionTargetBundle }
E-->>C: OTP code
C->>C: generateClientKeyPair()
C->>IB: POST /my-backend/otp/verify { otp, clientPublicKey }
IB->>G: POST /auth/credentials/{id}/verify { type: EMAIL_OTP, otp, clientPublicKey }
C->>C: generateClientKeyPair() (TEK)
C->>C: HPKE-encrypt { otp_code, public_key } → encryptedOtpBundle
C->>IB: POST /my-backend/otp/verify { encryptedOtpBundle }
IB->>G: POST /auth/credentials/{id}/verify { type: EMAIL_OTP, encryptedOtpBundle }
G-->>IB: 202 { payloadToSign, requestId }
IB-->>C: { payloadToSign, requestId }
C->>C: sign(payloadToSign, tekPrivateKey)
C->>IB: POST /my-backend/otp/verify/complete { stamp, requestId }
IB->>G: Same POST + Grid-Wallet-Signature + Request-Id
G-->>IB: 200 AuthSession
IB-->>C: { encryptedSessionSigningKey, expiresAt }
IB-->>C: { expiresAt }
```

```bash
Expand All @@ -504,26 +512,69 @@ curl -X POST "$GRID_BASE_URL/auth/credentials/AuthMethod:019542f5-b3e7-1d02-0000
"accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002",
"type": "EMAIL_OTP",
"nickname": "jane@example.com",
"otpEncryptionTargetBundle": "{\"version\":\"v1.0.0\",\"data\":\"7b22...\",\"dataSignature\":\"3045...\",\"enclaveQuorumPublic\":\"04a1...\"}",
"createdAt": "2026-04-19T12:00:00Z",
"updatedAt": "2026-04-19T12:00:00Z"
}
```

Then verify with the OTP value:
The client generates a fresh P-256 key pair (the TEK — Target Encryption Key), HPKE-encrypts `{otp_code, public_key}` under `otpEncryptionTargetBundle`, and submits the encrypted payload. See <a href="client-keys#encrypt-the-otp-code-email_otp-only">Encrypt the OTP code</a> for implementation details.

Then verify with the encrypted OTP bundle:

```bash
curl -X POST "$GRID_BASE_URL/auth/credentials/AuthMethod:019542f5-b3e7-1d02-0000-000000000004/verify" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"type": "EMAIL_OTP",
"otp": "123456",
"clientPublicKey": "04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"
"encryptedOtpBundle": "{\"encappedPublic\":\"044f631a...\",\"ciphertext\":\"1fa1023390...\"}"
}'
```

**Response (202):**

```json
{
"type": "EMAIL_OTP",
"payloadToSign": "eyJhbGciOiJFUzI1NiIsImtpZCI6InR1cm5rZXkifQ...",
"requestId": "Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21",
"expiresAt": "2026-04-19T12:05:00Z"
}
```

The client signs `payloadToSign` with the TEK private key (the same key whose public key was encrypted in the bundle), then retries with the stamp:

```bash
curl -X POST "$GRID_BASE_URL/auth/credentials/AuthMethod:019542f5-b3e7-1d02-0000-000000000004/verify" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-H "Grid-Wallet-Signature: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9" \
-H "Request-Id: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21" \
-d '{
"type": "EMAIL_OTP",
"encryptedOtpBundle": "{\"encappedPublic\":\"044f631a...\",\"ciphertext\":\"1fa1023390...\"}"
}'
```

**Response (200):**

```json
{
"id": "Session:019542f5-b3e7-1d02-0000-000000000003",
"accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002",
"type": "EMAIL_OTP",
"nickname": "jane@example.com",
"createdAt": "2026-04-19T12:00:01Z",
"updatedAt": "2026-04-19T12:00:01Z",
"expiresAt": "2026-04-19T12:15:01Z"
}
```

The TEK public key becomes the session API key. Unlike `OAUTH` and `PASSKEY` flows, `EMAIL_OTP` does **not** return `encryptedSessionSigningKey` — the client already holds the session signing key (the TEK private key it generated).

<Note>
**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 <a href="client-keys#3-decrypt-the-session-signing-key">Client keys</a>.
**In sandbox, the OTP code is always `000000`** — encrypt that value in the bundle. The sandbox runs real HPKE end-to-end; the only shortcut is skipping email delivery. See <a href="client-keys#encrypt-the-otp-code-email_otp-only">Client keys</a> for the encryption flow.
</Note>

### Resending an OTP
Expand All @@ -541,7 +592,7 @@ curl -X POST "$GRID_BASE_URL/auth/credentials/AuthMethod:019542f5-b3e7-1d02-0000

### 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`.
Same pattern as the first activation: call `/challenge` to send a new OTP and receive a fresh `otpEncryptionTargetBundle`, generate a new TEK key pair, build the `encryptedOtpBundle`, and complete the two-step verify flow.

### Changing the email OTP address

Expand Down
54 changes: 43 additions & 11 deletions mintlify/snippets/global-accounts/walkthrough.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,11 @@ curl -X POST "$GRID_BASE_URL/quotes" \

### 7. Authenticate and sign

The customer has an outstanding quote with a `payloadToSign`. Now we need a session signing key to sign it with. The flow is keypair → OTP challenge → verify → decrypt → sign.
The customer has an outstanding quote with a `payloadToSign`. Now we need a session signing key to sign it with. With `EMAIL_OTP`, the client generates a TEK (Target Encryption Key) pair, HPKE-encrypts the OTP code, and uses the TEK private key both to complete login and to sign the quote payload.

<Steps>
<Step title="Your backend requests a fresh OTP">
Ask Grid to send a fresh OTP email for the default `EMAIL_OTP` credential.
Ask Grid to send a fresh OTP email for the default `EMAIL_OTP` credential. The response includes `otpEncryptionTargetBundle` for the secure OTP flow.

```bash
curl -X POST "$GRID_BASE_URL/auth/credentials/AuthMethod:019542f5-b3e7-1d02-0000-000000000001/challenge" \
Expand All @@ -245,23 +245,56 @@ The customer has an outstanding quote with a `payloadToSign`. Now we need a sess
"accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002",
"type": "EMAIL_OTP",
"nickname": "jane@example.com",
"otpEncryptionTargetBundle": "{\"version\":\"v1.0.0\",\"data\":\"7b22...\",\"dataSignature\":\"3045...\",\"enclaveQuorumPublic\":\"04a1...\"}",
"createdAt": "2026-04-19T12:00:01Z",
"updatedAt": "2026-04-19T12:05:00Z"
}
```

Return `otpEncryptionTargetBundle` to the client.
</Step>
<Step title="Client enters the OTP and generates a key pair">
The client generates a fresh <a href="client-keys#1-generate-a-client-key-pair">P-256 client key pair</a> and posts the public key plus the OTP value to your backend. Grid uses the public key to seal the session signing key to that device.
<Step title="Client encrypts the OTP and verifies">
The client generates a fresh <a href="client-keys#1-generate-a-client-key-pair">P-256 key pair (the TEK)</a>, <a href="client-keys#encrypt-the-otp-code-email_otp-only">HPKE-encrypts</a> `{otp_code, public_key}` under `otpEncryptionTargetBundle`, and sends the encrypted bundle to your backend. In sandbox, use OTP code `000000`.

Your backend calls verify with the encrypted bundle:

```bash
curl -X POST "$GRID_BASE_URL/auth/credentials/AuthMethod:019542f5-b3e7-1d02-0000-000000000001/verify" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"type": "EMAIL_OTP",
"encryptedOtpBundle": "{\"encappedPublic\":\"044f631a...\",\"ciphertext\":\"1fa1023390...\"}"
}'
```

**Response (202):**

```json
{
"type": "EMAIL_OTP",
"payloadToSign": "eyJhbGciOiJFUzI1NiIsImtpZCI6InR1cm5rZXkifQ...",
"requestId": "Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21",
"expiresAt": "2026-04-19T12:10:00Z"
}
```

Return `payloadToSign` and `requestId` to the client.
</Step>
<Step title="Your backend verifies the OTP with Grid to mint a session">
<Step title="Client signs the verification token and completes login">
The client <a href="client-keys#4-sign-a-payloadtosign">stamps</a> `payloadToSign` with the TEK private key and sends the stamp back to your backend.

Your backend retries the same request with the stamp:

```bash
curl -X POST "$GRID_BASE_URL/auth/credentials/AuthMethod:019542f5-b3e7-1d02-0000-000000000001/verify" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-H "Grid-Wallet-Signature: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9" \
-H "Request-Id: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21" \
-d '{
"type": "EMAIL_OTP",
"otp": "123456",
"clientPublicKey": "04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"
"encryptedOtpBundle": "{\"encappedPublic\":\"044f631a...\",\"ciphertext\":\"1fa1023390...\"}"
}'
```

Expand All @@ -273,17 +306,16 @@ The customer has an outstanding quote with a `payloadToSign`. Now we need a sess
"accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002",
"type": "EMAIL_OTP",
"nickname": "jane@example.com",
"encryptedSessionSigningKey": "w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf",
"createdAt": "2026-04-19T12:05:01Z",
"updatedAt": "2026-04-19T12:05:01Z",
"expiresAt": "2026-04-19T12:20:01Z"
}
```

Return `encryptedSessionSigningKey` and `expiresAt` to the client.
The TEK public key is now the session API key. The TEK private key **is** the session signing key — the client already has it.
</Step>
<Step title="Client decrypts the session signing key and stamps the payload">
The client <a href="client-keys#3-decrypt-the-session-signing-key">decrypts</a> `encryptedSessionSigningKey` with the matching client private key, then <a href="client-keys#4-sign-a-payloadtosign">stamps the quote's `payloadToSign`</a> with the resulting session signing key. Return the full Turnkey API-key stamp to your backend.
<Step title="Client stamps the quote payload">
The client <a href="client-keys#4-sign-a-payloadtosign">stamps the quote's `payloadToSign`</a> with the same TEK private key. Return the full Turnkey API-key stamp to your backend.
</Step>
</Steps>

Expand Down
36 changes: 28 additions & 8 deletions mintlify/snippets/sandbox-global-account-magic.mdx
Original file line number Diff line number Diff line change
@@ -1,24 +1,44 @@
The Grid sandbox lets you exercise Global Account auth flows without moving real money. Email OTP uses the fixed sandbox code `000000`. Passkey auth can use the same browser WebAuthn ceremony as production, and signed wallet actions can use the same decrypted session signing key and `Grid-Wallet-Signature` stamp as production. OAuth uses JWT-shaped sandbox OIDC tokens: sandbox skips real IdP signature verification, but still validates token claims, freshness, credential identity, and verify-time nonce binding.
The Grid sandbox lets you exercise Global Account auth flows without moving real money. Email OTP uses the fixed sandbox code `000000` — HPKE-encrypt that code in the `encryptedOtpBundle` just like production. Passkey auth can use the same browser WebAuthn ceremony as production, and signed wallet actions can use the same session signing key and `Grid-Wallet-Signature` stamp as production. OAuth uses JWT-shaped sandbox OIDC tokens: sandbox skips real IdP signature verification, but still validates token claims, freshness, credential identity, and verify-time nonce binding.

Sandbox-only compatibility values are still available for some flows, but they do not exercise the production-shaped client implementation. Authentication failures return `401 UNAUTHORIZED` with a `reason` field that names the specific check that failed. A malformed OIDC JWT can return `400 INVALID_INPUT` before authentication starts.
Sandbox runs real HPKE end-to-end for EMAIL_OTP: clients build a real `encryptedOtpBundle` against the sandbox `otpEncryptionTargetBundle` and sign a real `verificationToken` with their TEK keypair. The only sandbox shortcut is the magic OTP code the user "receives" instead of a real email delivery.

Authentication failures return `401 UNAUTHORIZED` with a `reason` field that names the specific check that failed. A malformed OIDC JWT can return `400 INVALID_INPUT` before authentication starts.

### Email OTP code

Pass `000000` as the body `otp` on `POST /auth/credentials/{id}/verify` when the credential type is `EMAIL_OTP`. The sandbox skips OTP delivery and accepts this value as a valid response to the issued challenge.
HPKE-encrypt the code `000000` (together with your TEK public key) inside `encryptedOtpBundle`. The sandbox skips email delivery but runs real HPKE decryption and signature verification.

See <a href="/global-accounts/integration-guides/client-keys#encrypt-the-otp-code-email_otp-only">Encrypt the OTP code</a> for how to build the bundle. The flow is:

1. Call `POST /auth/credentials/{id}/challenge` to get `otpEncryptionTargetBundle`
2. Generate a TEK key pair and HPKE-encrypt `{otp_code: "000000", public_key: tekPublicKeyHex}`
3. Submit `encryptedOtpBundle` to `POST /auth/credentials/{id}/verify`
4. Receive `202` with `payloadToSign` and `requestId`
5. Sign `payloadToSign` with the TEK private key and retry with `Grid-Wallet-Signature` + `Request-Id` headers

```bash
# First leg — returns 202 with payloadToSign
curl -X POST https://api.lightspark.com/grid/2025-10-13/auth/credentials/AuthMethod:abc123/verify \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-H "Request-Id: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21" \
-d '{
"type": "EMAIL_OTP",
"otp": "000000",
"clientPublicKey": "04f45f2a..."
"encryptedOtpBundle": "{\"encappedPublic\":\"044f631a...\",\"ciphertext\":\"1fa1023390...\"}"
}'

# Signed retry — returns 200 with AuthSession
curl -X POST https://api.lightspark.com/grid/2025-10-13/auth/credentials/AuthMethod:abc123/verify \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-H "Grid-Wallet-Signature: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4i..." \
-H "Request-Id: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21" \
-d '{
"type": "EMAIL_OTP",
"encryptedOtpBundle": "{\"encappedPublic\":\"044f631a...\",\"ciphertext\":\"1fa1023390...\"}"
}'
```

Any other code returns `401 UNAUTHORIZED` with `reason: "Invalid OTP code"`.
Any other code (once decrypted) returns `401 UNAUTHORIZED` with `reason: "Invalid OTP code"`.

### Passkey WebAuthn ceremony

Expand Down Expand Up @@ -131,7 +151,7 @@ curl -X POST https://api.lightspark.com/grid/2025-10-13/auth/credentials/AuthMet

### Wallet signature header

After verifying an auth credential, decrypt `encryptedSessionSigningKey` with the private key matching the `clientPublicKey` you supplied on verify or refresh. Use the decrypted session signing key to build a Turnkey API-key stamp over the exact `payloadToSign` string returned by Grid, then pass that full stamp as the `Grid-Wallet-Signature` HTTP header on signed flows:
For `PASSKEY` and `OAUTH` credentials, decrypt `encryptedSessionSigningKey` with the private key matching the `clientPublicKey` you supplied on verify or refresh. For `EMAIL_OTP`, the TEK private key you generated for the encrypted OTP flow **is** the session signing key — no decryption step needed. Use the session signing key to build a Turnkey API-key stamp over the exact `payloadToSign` string returned by Grid, then pass that full stamp as the `Grid-Wallet-Signature` HTTP header on signed flows:

- `POST /auth/credentials` (add-additional-credential signed retry)
- `DELETE /auth/credentials/{id}` (revoke credential)
Expand Down
Loading
Loading