diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml
index 70e482bc..b8830b17 100644
--- a/mintlify/openapi.yaml
+++ b/mintlify/openapi.yaml
@@ -2482,10 +2482,11 @@ paths:
or has direct pull functionality (e.g. ACH pull with an external account).
When the quote's `source` is an internal account of type `EMBEDDED_WALLET`,
- the request must include a `Grid-Wallet-Signature` header. The signature is
- produced by signing the `payloadToSign` value from the quote's
- `paymentInstructions[].accountOrWalletInfo` entry with the session private
- key of a verified authentication credential on the source Embedded Wallet.
+ the request must include a `Grid-Wallet-Signature` header. The header value
+ is the full Turnkey API-key stamp built over the `payloadToSign` value from
+ the quote's `paymentInstructions[].accountOrWalletInfo` entry with the
+ session private key of a verified authentication credential on the source
+ Embedded Wallet.
Once executed, the quote cannot be cancelled and the transfer will be processed.
operationId: executeQuote
@@ -2512,10 +2513,10 @@ paths:
- name: Grid-Wallet-Signature
in: header
required: false
- description: Signature over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet and base64-encoded. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types.
+ description: Full Turnkey API-key stamp over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types.
schema:
type: string
- example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE=
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
responses:
'200':
description: |
@@ -4606,7 +4607,7 @@ paths:
challenge:
summary: Session refresh challenge
value:
- payloadToSign: '{"type":"ACTIVITY_TYPE_CREATE_READ_WRITE_SESSION_V2","timestampMs":"1746736509954","organizationId":"org_abc123","parameters":{"targetPublicKey":"04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"}}'
+ payloadToSign: '{"organizationId":"org_abc123","parameters":{"targetPublicKey":"04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"},"timestampMs":"1746736509954","type":"ACTIVITY_TYPE_CREATE_READ_WRITE_SESSION_V2"}'
requestId: Request:019542f5-b3e7-1d02-0000-000000000010
expiresAt: '2026-04-08T15:35:00Z'
'400':
@@ -5617,10 +5618,10 @@ paths:
- name: Grid-Wallet-Signature
in: header
required: false
- description: Signature over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet and base64-encoded. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types.
+ description: Full Turnkey API-key stamp over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types.
schema:
type: string
- example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE=
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
responses:
'200':
description: 'Action submitted successfully. If the agent''s policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated. Note: if approval is required, the underlying quote may expire before the platform approves — in that case the action will transition to `FAILED`.'
@@ -10765,7 +10766,7 @@ components:
description: Discriminator value identifying this as Embedded Wallet payment instructions.
payloadToSign:
type: string
- description: JSON-encoded transaction signing payload that must be signed, as-is (byte-for-byte, without re-serialization), with the session private key of a verified authentication credential on the source Embedded Wallet. The resulting signature is base64-encoded and passed as the `Grid-Wallet-Signature` header on `POST /quotes/{quoteId}/execute` to authorize the outbound transfer from the wallet.
+ description: JSON-encoded transaction signing payload that must be stamped, as-is (byte-for-byte, without re-serialization), with the session private key of a verified authentication credential on the source Embedded Wallet. The resulting Turnkey API-key stamp is passed as the `Grid-Wallet-Signature` header on `POST /quotes/{quoteId}/execute` to authorize the outbound transfer from the wallet.
example: '{"type":"ACTIVITY_TYPE_SIGN_TRANSACTION_V2","timestampMs":"1746736509954","organizationId":"org_abc123","parameters":{"signWith":"wallet_abc123def456","unsignedTransaction":"ea69b4bf05f775209f26ff0a34a05569180f7936579d5c4af9377ae550194f72","type":"TRANSACTION_TYPE_ETHEREUM"},"generateAppProofs":true}'
PaymentInstructions:
type: object
@@ -15588,7 +15589,7 @@ components:
description: |-
Whether to immediately execute the quote after creation. If true, the quote will be executed and the transaction will be created at the current exchange rate. It should only be used if you don't want to lock and view rate details before executing the quote. If you are executing a pre-existing quote, use the `/quotes/{quoteId}/execute` endpoint instead. This is false by default.
This can only be used for quotes with a `source` which is either an internal account, or has direct pull functionality (e.g. ACH pull with an external account).
- Not supported when the `source` is an internal account of type `EMBEDDED_WALLET`: those transfers require a `Grid-Wallet-Signature` over the `payloadToSign` returned in the quote response, which is not available in a combined create-and-execute call. Create the quote first with `immediatelyExecute: false` and then call `POST /quotes/{quoteId}/execute` with the signature header.
+ Not supported when the `source` is an internal account of type `EMBEDDED_WALLET`: those transfers require a `Grid-Wallet-Signature` over the `payloadToSign` returned in the quote response, which is not available in a combined create-and-execute call. Create the quote first with `immediatelyExecute: false` and then call `POST /quotes/{quoteId}/execute` with the `Grid-Wallet-Signature` stamp header.
example: false
description:
type: string
diff --git a/mintlify/snippets/global-accounts/authentication.mdx b/mintlify/snippets/global-accounts/authentication.mdx
index 52451543..77af938f 100644
--- a/mintlify/snippets/global-accounts/authentication.mdx
+++ b/mintlify/snippets/global-accounts/authentication.mdx
@@ -10,22 +10,22 @@ A single internal account can hold one `EMAIL_OTP` credential and multiple disti
## Registration vs. verification
-Every credential type starts the same way:
+Global Accounts are initialized with an `EMAIL_OTP` credential tied to the customer email on the internal account. Use that credential to mint the first session, or use an already-registered `OAUTH` or `PASSKEY` credential after you add one.
-1. **`POST /auth/credentials`** creates the credential record and triggers the out-of-band channel (OTP email sent, WebAuthn attestation stored). The response is a plain `AuthMethod` for all three credential types — registration alone does not issue a session.
-
-To produce the first session:
+To produce a session:
- **`EMAIL_OTP`** and **`OAUTH`** — call **`POST /auth/credentials/{id}/verify`** with the OTP value (or a fresh OIDC token) plus a `clientPublicKey`. The response carries the `encryptedSessionSigningKey`.
- **`PASSKEY`** — call **`POST /auth/credentials/{id}/challenge`** with your `clientPublicKey` to receive a Grid-issued WebAuthn `challenge` and `requestId`, run `navigator.credentials.get()` against that challenge, then call **`POST /auth/credentials/{id}/verify`** with the resulting assertion and the `Request-Id` header. Grid bakes the `clientPublicKey` from the `/challenge` call into the session-creation payload, so it does **not** appear on the `/verify` body.
+To add another credential, call **`POST /auth/credentials`** with the new credential details. Because the account already has an `EMAIL_OTP` credential, Grid returns a `202` signed-retry challenge. Stamp that `payloadToSign` with an active session signing key, then retry the same request with `Grid-Wallet-Signature` and `Request-Id`. The signed retry returns the new `AuthMethod`.
+
Re-authentication after a session expires skips the original `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 via `POST /auth/credentials/{id}/challenge` — same call shape used for every subsequent reauthentication.
+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. Registering the passkey on Grid is an additional-credential signed-retry flow, authorized by an active session from an existing credential. The first authentication challenge is requested explicitly via `POST /auth/credentials/{id}/challenge` — same call shape used for every subsequent reauthentication.
```mermaid
sequenceDiagram
@@ -41,6 +41,11 @@ sequenceDiagram
A-->>C: attestation (credentialId, clientDataJson, attestationObject)
C->>IB: POST /my-backend/passkey/register (attestation)
IB->>G: POST /auth/credentials { type: PASSKEY, challenge, attestation, … }
+ G-->>IB: 202 { payloadToSign, requestId, expiresAt }
+ IB-->>C: { payloadToSign, requestId }
+ C->>C: stamp(payloadToSign, existingSessionPrivateKey)
+ C->>IB: { stamp, requestId }
+ IB->>G: Same POST
Grid-Wallet-Signature: stamp
Request-Id: requestId
G-->>IB: 201 AuthMethod { id, type, accountId, … }
IB-->>C: { credentialId }
C->>C: generateClientKeyPair()
@@ -57,7 +62,7 @@ sequenceDiagram
C->>C: decrypt with private key, hold session signing key
```
-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).
+The `challenge` on `POST /auth/credentials` is the one your backend issued for WebAuthn registration. The signed-retry `payloadToSign` is the Turnkey registration payload that the customer's current session authorizes. The challenge used for the WebAuthn assertion comes later 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).
Passkeys are domain-bound. Before shipping, set up your `/.well-known/apple-app-site-association` and `/.well-known/assetlinks.json` entries so the platform authenticator binds the passkey to your origin and app bundles:
@@ -96,7 +101,7 @@ const attestation = (await navigator.credentials.create({
})) as PublicKeyCredential;
const att = attestation.response as AuthenticatorAttestationResponse;
-// 3. Send the attestation to your backend, which calls POST /auth/credentials.
+// 3. Send the attestation to your backend, which starts POST /auth/credentials.
const registerRes = await fetch("/my-backend/passkey/register", {
method: "POST",
credentials: "include",
@@ -109,9 +114,24 @@ const registerRes = await fetch("/my-backend/passkey/register", {
transports: att.getTransports?.() ?? [],
}),
});
-const { credentialId } = await registerRes.json();
+const { payloadToSign, requestId: registrationRequestId } = await registerRes.json();
+
+// 4. Stamp the registration payload with an existing session signing key, then
+// complete the signed retry. The backend retries the same Grid request with
+// Grid-Wallet-Signature and Request-Id.
+const gridWalletSignature = await buildGridWalletSignature(
+ existingSessionPrivateKeyBytes,
+ payloadToSign,
+);
+const registerCompleteRes = await fetch("/my-backend/passkey/register/complete", {
+ method: "POST",
+ credentials: "include",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ requestId: registrationRequestId, gridWalletSignature }),
+});
+const { credentialId } = await registerCompleteRes.json();
-// 4. Generate the client key pair and ask Grid for an authentication challenge
+// 5. Generate the client key pair and ask Grid for an authentication challenge
// sealed to its public key.
const { keyPair, publicKeyHex } = await generateClientKeyPair();
const challengeRes = await fetch("/my-backend/passkey/challenge", {
@@ -120,9 +140,9 @@ const challengeRes = await fetch("/my-backend/passkey/challenge", {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ credentialId, clientPublicKey: publicKeyHex }),
});
-const { challenge: gridChallenge, requestId } = await challengeRes.json();
+const { challenge: gridChallenge, requestId: authRequestId } = await challengeRes.json();
-// 5. Run the WebAuthn assertion against the Grid-issued challenge.
+// 6. Run the WebAuthn assertion against the Grid-issued challenge.
const assertion = (await navigator.credentials.get({
publicKey: {
challenge: base64urlToBytes(gridChallenge),
@@ -133,14 +153,14 @@ const assertion = (await navigator.credentials.get({
})) as PublicKeyCredential;
const asr = assertion.response as AuthenticatorAssertionResponse;
-// 6. Send the assertion to your backend; it relays to POST /verify with Request-Id.
+// 7. Send the assertion to your backend; it relays to POST /verify with Request-Id.
const verifyRes = await fetch("/my-backend/passkey/verify", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
credentialId,
- requestId,
+ requestId: authRequestId,
assertion: {
credentialId: bytesToBase64url(new Uint8Array(assertion.rawId)),
clientDataJson: bytesToBase64url(new Uint8Array(asr.clientDataJSON)),
@@ -179,10 +199,20 @@ suspend fun registerPasskey(context: Context, api: MyBackendApi): EmbeddedWallet
) as androidx.credentials.CreatePublicKeyCredentialResponse
val attestationJson = createResp.registrationResponseJson
- // 3. Backend → POST /auth/credentials. Returns AuthMethod { credentialId, rpId, ... }.
- val registerResp = api.passkeyRegister(attestationJson, nickname = "This device")
+ // 3. Backend -> POST /auth/credentials. Returns a signed-retry challenge.
+ val registerChallenge = api.passkeyRegister(attestationJson, nickname = "This device")
- // 4. Generate client key pair, then ask Grid for an authentication challenge
+ // 4. Client stamps the registration payload with an existing session key.
+ val gridWalletSignature = buildGridWalletSignature(
+ existingSessionPrivateKeyBytes,
+ registerChallenge.payloadToSign,
+ )
+ val registerResp = api.passkeyRegisterComplete(
+ requestId = registerChallenge.requestId,
+ gridWalletSignature = gridWalletSignature,
+ )
+
+ // 5. Generate client key pair, then ask Grid for an authentication challenge
// sealed to that public key via POST /auth/credentials/{id}/challenge.
val clientKeys = generateClientKeyPair(alias = "embedded-wallet-${registerResp.credentialId}")
val challengeResp = api.passkeyChallenge(
@@ -190,7 +220,7 @@ suspend fun registerPasskey(context: Context, api: MyBackendApi): EmbeddedWallet
clientPublicKeyHex = clientKeys.publicKeyHex,
)
- // 5. Run WebAuthn assertion against Grid-issued challenge.
+ // 6. Run WebAuthn assertion against Grid-issued challenge.
val getOptionsJson = buildWebAuthnGetOptionsJson(
challenge = challengeResp.challenge,
rpId = registerResp.rpId,
@@ -202,7 +232,7 @@ suspend fun registerPasskey(context: Context, api: MyBackendApi): EmbeddedWallet
).credential as PublicKeyCredential
val assertionJson = getResp.authenticationResponseJson
- // 6. Backend → POST /verify with Request-Id. Returns encryptedSessionSigningKey.
+ // 7. Backend -> POST /verify with Request-Id. Returns encryptedSessionSigningKey.
return api.passkeyVerify(
credentialId = registerResp.credentialId,
requestId = challengeResp.requestId,
@@ -245,15 +275,25 @@ final class PasskeyCoordinator: NSObject, ASAuthorizationControllerDelegate {
controller.performRequests()
}
- // 3. Backend → POST /auth/credentials. Returns AuthMethod.
- let registered = try await api.passkeyRegister(
+ // 3. Backend -> POST /auth/credentials. Returns a signed-retry challenge.
+ let registerChallenge = try await api.passkeyRegister(
credentialId: attestation.credentialID.base64URLEncoded,
clientDataJson: attestation.rawClientDataJSON.base64URLEncoded,
attestationObject: attestation.rawAttestationObject!.base64URLEncoded,
nickname: "This device",
)
- // 4. Generate client key pair, ask Grid for an authentication challenge
+ // 4. Client stamps the registration payload with an existing session key.
+ let gridWalletSignature = try buildGridWalletSignature(
+ sessionPrivateScalar: existingSessionPrivateScalar,
+ payloadToSign: registerChallenge.payloadToSign,
+ )
+ let registered = try await api.passkeyRegisterComplete(
+ requestId: registerChallenge.requestId,
+ gridWalletSignature: gridWalletSignature,
+ )
+
+ // 5. Generate client key pair, ask Grid for an authentication challenge
// sealed to that public key via POST /auth/credentials/{id}/challenge.
let keys = generateClientKeyPair()
let challengeResp = try await api.passkeyChallenge(
@@ -261,7 +301,7 @@ final class PasskeyCoordinator: NSObject, ASAuthorizationControllerDelegate {
clientPublicKeyHex: keys.publicKeyHex,
)
- // 5. Run WebAuthn assertion against Grid-issued challenge.
+ // 6. Run WebAuthn assertion against Grid-issued challenge.
let assertRequest = provider.createCredentialAssertionRequest(
challenge: challengeResp.challenge,
)
@@ -279,7 +319,7 @@ final class PasskeyCoordinator: NSObject, ASAuthorizationControllerDelegate {
controller.performRequests()
}
- // 6. Backend → POST /verify with Request-Id. Returns encryptedSessionSigningKey.
+ // 7. Backend -> POST /verify with Request-Id. Returns encryptedSessionSigningKey.
return try await api.passkeyVerify(
credentialId: registered.credentialId,
requestId: challengeResp.requestId,
@@ -357,7 +397,7 @@ sequenceDiagram
## 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.
+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. Adding OAuth to an initialized Global Account requires an active session from an existing credential and uses the same signed-retry pattern as passkey registration.
### OAuth registration
@@ -372,6 +412,11 @@ sequenceDiagram
OP-->>C: id_token (OIDC)
C->>IB: POST /my-backend/oauth/register { oidcToken }
IB->>G: POST /auth/credentials { type: OAUTH, oidcToken, accountId }
+ G-->>IB: 202 { payloadToSign, requestId, expiresAt }
+ IB-->>C: { payloadToSign, requestId }
+ C->>C: stamp(payloadToSign, existingSessionPrivateKey)
+ C->>IB: { stamp, requestId }
+ IB->>G: Same POST
Grid-Wallet-Signature: stamp
Request-Id: requestId
G->>OP: fetch .well-known/openid-configuration + jwks
G-->>IB: 201 AuthMethod
IB-->>C: { credentialId }
@@ -397,7 +442,7 @@ curl -X POST "$GRID_BASE_URL/auth/credentials" \
}'
```
-**Response:** `201 AuthMethod` with `nickname` populated from the OIDC token's `email` claim.
+**Response on the signed retry:** `201 AuthMethod` with `nickname` populated from the OIDC token's `email` claim.
### OAuth verify / reauthentication
@@ -416,11 +461,11 @@ curl -X POST "$GRID_BASE_URL/auth/credentials/AuthMethod:019542f5-b3e7-1d02-0000
## 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.
+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. Global Accounts are initialized with an `EMAIL_OTP` credential; call `POST /auth/credentials` for `EMAIL_OTP` only if the credential was removed and you need to add it back.
-### Email OTP registration
+### Default Email OTP credential
-Creating the credential triggers an OTP email to the customer email on file for the internal account. Do not include an `email` field in the request body; Grid resolves the address from the account's customer record.
+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.
```mermaid
sequenceDiagram
@@ -429,11 +474,11 @@ sequenceDiagram
participant G as Grid
participant E as Email
- C->>IB: POST /my-backend/otp/register { accountId }
- IB->>G: POST /auth/credentials { type: EMAIL_OTP, accountId }
+ C->>IB: POST /my-backend/otp/challenge { credentialId }
+ IB->>G: POST /auth/credentials/{id}/challenge
G->>E: deliver OTP email
- G-->>IB: 201 AuthMethod
- IB-->>C: { credentialId }
+ G-->>IB: 200 AuthMethod
+ IB-->>C: ok
E-->>C: OTP code
C->>C: generateClientKeyPair()
C->>IB: POST /my-backend/otp/verify { otp, clientPublicKey }
@@ -443,16 +488,11 @@ sequenceDiagram
```
```bash
-curl -X POST "$GRID_BASE_URL/auth/credentials" \
- -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
- -H "Content-Type: application/json" \
- -d '{
- "type": "EMAIL_OTP",
- "accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002"
- }'
+curl -X POST "$GRID_BASE_URL/auth/credentials/AuthMethod:019542f5-b3e7-1d02-0000-000000000004/challenge" \
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
-**Response (201):**
+**Response (200):**
```json
{
@@ -465,7 +505,7 @@ curl -X POST "$GRID_BASE_URL/auth/credentials" \
}
```
-Then complete activation with the OTP value:
+Then verify with the OTP value:
```bash
curl -X POST "$GRID_BASE_URL/auth/credentials/AuthMethod:019542f5-b3e7-1d02-0000-000000000004/verify" \
@@ -499,9 +539,13 @@ curl -X POST "$GRID_BASE_URL/auth/credentials/AuthMethod:019542f5-b3e7-1d02-0000
Same pattern as the first activation: call `/challenge` to send a new OTP, then `/verify` with the new code and a fresh `clientPublicKey`.
+### Changing the email OTP address
+
+The `EMAIL_OTP` address comes from the customer email on file. To change it, update the customer with `PATCH /customers/{customerId}`. If the customer has tied Embedded Wallet `EMAIL_OTP` credentials, Grid returns a signed-retry challenge; stamp the returned `payloadToSign` with an active session signing key, then retry the same customer update with `Grid-Wallet-Signature` and `Request-Id`. Grid syncs the customer email and tied `EMAIL_OTP` credential email together.
+
## Managing credentials
-Every Global Account starts with a single credential — the one used in the quickstart. In production, encourage customers to register a backup credential, such as another passkey or an email OTP, 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.
+Every Global Account starts with a single `EMAIL_OTP` credential — the one used in the quickstart. In production, encourage customers to register a backup credential, such as a passkey or OAuth credential, 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
@@ -539,7 +583,7 @@ The response is not paginated — each account holds a small, bounded number of
### The signed-retry pattern
-Adding an additional credential, revoking a credential, revoking a session, exporting a wallet, and updating wallet privacy all share the same shape:
+Adding an additional credential, revoking a credential, refreshing or revoking a session, exporting a wallet, updating wallet privacy, and updating a customer email tied to `EMAIL_OTP` all share the same shape:
```mermaid
sequenceDiagram
@@ -550,23 +594,23 @@ sequenceDiagram
IB->>G: Request (no headers)
G-->>IB: 202 { payloadToSign, requestId, expiresAt }
IB-->>C: { payloadToSign, requestId }
- C->>C: sign(payloadToSign, sessionPrivateKey)
- C->>IB: { signature }
- IB->>G: Same request
Grid-Wallet-Signature: signature
Request-Id: requestId
+ C->>C: stamp(payloadToSign, sessionPrivateKey)
+ C->>IB: { stamp }
+ IB->>G: Same request
Grid-Wallet-Signature: stamp
Request-Id: requestId
G-->>IB: 2xx (terminal success)
IB-->>C: done
```
Key rules:
-- Always sign the `payloadToSign` **byte-for-byte as Grid returned it**. Do not re-parse, re-serialize, or modify whitespace.
+- Always stamp the `payloadToSign` **byte-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 `requestId` is returned as `Request:` and is single-use; reusing one yields `401`.
### 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 responds `202` instead of `201`. For `EMAIL_OTP`, Grid uses the customer email on file for the internal account.
+Requires an active session on an *existing* credential on the same account. The first call uses the normal credential-create body; Grid detects the pre-existing credential and responds `202` instead of `201`. `OAUTH` and `PASSKEY` are the typical additional credential types. `EMAIL_OTP` can be added back only after the existing email OTP credential has been removed, because each account supports one.
@@ -575,8 +619,9 @@ Requires an active session on an *existing* credential on the same account. The
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
- "type": "EMAIL_OTP",
- "accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002"
+ "type": "OAUTH",
+ "accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002",
+ "oidcToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9..."
}'
```
@@ -584,32 +629,33 @@ Requires an active session on an *existing* credential on the same account. The
```json
{
- "type": "EMAIL_OTP",
- "payloadToSign": "{\"organizationId\":\"org_2m9F...\",\"parameters\":{\"userEmail\":\"jane@example.com\",\"userId\":\"user_2m9F...\"},\"timestampMs\":\"1775681700000\",\"type\":\"ACTIVITY_TYPE_UPDATE_USER_EMAIL\"}",
+ "type": "OAUTH",
+ "payloadToSign": "{\"organizationId\":\"org_2m9F...\",\"parameters\":{\"oauthProviders\":[{\"oidcToken\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9...\",\"providerName\":\"Google\"}],\"userId\":\"user_2m9F...\"},\"timestampMs\":\"1775681700000\",\"type\":\"ACTIVITY_TYPE_CREATE_OAUTH_PROVIDERS\"}",
"requestId": "Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21",
"expiresAt": "2026-04-08T15:35:00Z"
}
```
-
- Send `payloadToSign` to the client. The client signs with the session signing key from the existing credential's active session — see signing payloads.
+
+ Send `payloadToSign` to the client. The client builds a Turnkey API-key stamp with the session signing key from the existing credential's active session — see signing payloads.
- Re-run the same request with the signature and request id in headers:
+ Re-run the same request with the stamp and request id in headers:
```bash
curl -X POST "$GRID_BASE_URL/auth/credentials" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
- -H "Grid-Wallet-Signature: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE=" \
+ -H "Grid-Wallet-Signature: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9" \
-H "Request-Id: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21" \
-d '{
- "type": "EMAIL_OTP",
- "accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002"
+ "type": "OAUTH",
+ "accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002",
+ "oidcToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9..."
}'
```
- **Response (201):** a plain `AuthMethod`. For `EMAIL_OTP`, Grid delivers the OTP email on this signed retry, not on the first call.
+ **Response (201):** a plain `AuthMethod`.
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.
@@ -642,14 +688,14 @@ A credential is revoked by signing with a session from **a different credential
}
```
-
- The client signs `payloadToSign` with the session signing key of an active session on any *other* credential (not the one being revoked).
+
+ The client stamps `payloadToSign` with the session signing key of an active session on any *other* credential (not the one being revoked).
```bash
curl -X DELETE "$GRID_BASE_URL/auth/credentials/AuthMethod:019542f5-b3e7-1d02-0000-000000000001" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
- -H "Grid-Wallet-Signature: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE=" \
+ -H "Grid-Wallet-Signature: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9" \
-H "Request-Id: Request:9f7a2c10-5e88-4fb1-bd0e-1c3a8e7b2d45"
```
diff --git a/mintlify/snippets/global-accounts/client-keys.mdx b/mintlify/snippets/global-accounts/client-keys.mdx
index 8104fa41..a40b5f77 100644
--- a/mintlify/snippets/global-accounts/client-keys.mdx
+++ b/mintlify/snippets/global-accounts/client-keys.mdx
@@ -2,14 +2,14 @@ Every signed Global Account action uses two key pairs:
| Key pair | Where it lives | What it does |
|---|---|---|
-| **Client key pair** (P-256) | On the customer's device, generated fresh per verification request | Used as the HPKE recipient key so Grid can encrypt the session signing key to the client. Ephemeral — one pair per `POST /auth/credentials/{id}/verify` call. |
+| **Client key pair** (P-256) | On the customer's device, generated fresh per session-issuing or export request | Used as the HPKE recipient key so Grid can encrypt session keys or wallet export credentials to the client. Ephemeral — one pair per authentication, session refresh, or wallet export. |
| **Session signing key** (P-256) | Issued by Grid, encrypted to the client public key, decrypted and held on the device | Signs every account action for the lifetime of the session (default 15 minutes). |
This page covers generating the client key pair, sending the public key to your backend, decrypting 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
-Generate a fresh P-256 key pair for every authentication and for every wallet export. The public key is sent to Grid as `clientPublicKey` — 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 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 — a 130-character string starting with `04` — through your integrator backend. The Web Crypto, iOS, and Android APIs shown below all produce this format natively.
+Generate a fresh P-256 key pair for every authentication, session refresh, and wallet export. The public key is sent to Grid as `clientPublicKey` — 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 — a 130-character string starting with `04` — through your integrator backend. The Web Crypto, iOS, and Android APIs shown below all produce this format natively.
For local development, you can generate a P-256 key pair from the command line:
@@ -224,9 +224,19 @@ 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` (adding an additional credential) — 202 response body.
-- `DELETE /auth/credentials/{id}`, `DELETE /auth/sessions/{id}`, `POST /internal-accounts/{id}/export`, `PATCH /internal-accounts/{id}` — all 202 response bodies.
+- `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.
-Sign the payload **byte-for-byte as returned** (do not re-parse, re-serialize, or trim whitespace). The signature is ECDSA over SHA-256 using the session signing key, DER-encoded, then base64-encoded. Pass it as the `Grid-Wallet-Signature` header on the retry (and, for endpoints that use it, the `Request-Id` header echoed back from the 202).
+Stamp the payload **byte-for-byte as returned** (do not re-parse, re-serialize, or trim whitespace). The session signing key is a Turnkey API key: derive its compressed P-256 public key, sign the payload with the private scalar, then base64url-encode the Turnkey stamp JSON:
+
+```json
+{
+ "publicKey": "",
+ "scheme": "SIGNATURE_SCHEME_TK_API_P256",
+ "signature": ""
+}
+```
+
+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`).
**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.
@@ -234,36 +244,69 @@ Sign the payload **byte-for-byte as returned** (do not re-parse, re-serialize, o
```typescript Web (TypeScript)
-// npm i @noble/curves @noble/hashes
-import { p256 } from "@noble/curves/p256";
-import { sha256 } from "@noble/hashes/sha256";
+// npm i @turnkey/api-key-stamper @turnkey/crypto
+import { signWithApiKey } from "@turnkey/api-key-stamper";
+import { getPublicKey } from "@turnkey/crypto";
+
+const TURNKEY_STAMP_SCHEME = "SIGNATURE_SCHEME_TK_API_P256";
-function bytesToBase64(bytes: Uint8Array): string {
+function bytesToHex(bytes: Uint8Array): string {
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
+}
+
+function base64url(bytes: Uint8Array): string {
let binary = "";
for (const byte of bytes) {
binary += String.fromCharCode(byte);
}
- return btoa(binary);
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
-function signPayload(
+async function buildGridWalletSignature(
sessionPrivateKeyBytes: Uint8Array, // 32 bytes, from decryptSessionSigningKey
payloadToSign: string,
-): string {
- const digest = sha256(new TextEncoder().encode(payloadToSign));
- const signature = p256.sign(digest, sessionPrivateKeyBytes);
- return bytesToBase64(signature.toDERRawBytes());
+): Promise {
+ const apiPrivateKey = bytesToHex(sessionPrivateKeyBytes);
+ const apiPublicKey = bytesToHex(getPublicKey(apiPrivateKey, true));
+ const signature = await signWithApiKey({
+ content: payloadToSign,
+ publicKey: apiPublicKey,
+ privateKey: apiPrivateKey,
+ });
+ const stamp = JSON.stringify({
+ publicKey: apiPublicKey,
+ scheme: TURNKEY_STAMP_SCHEME,
+ signature,
+ });
+ return base64url(new TextEncoder().encode(stamp));
}
```
```kotlin Android (Kotlin)
+import android.util.Base64
import java.security.KeyFactory
import java.security.Signature
+import java.security.interfaces.ECPrivateKey
import java.security.spec.ECPrivateKeySpec
import java.security.spec.ECParameterSpec
-import android.util.Base64
+import org.bouncycastle.jce.ECNamedCurveTable
+
+private const val TURNKEY_STAMP_SCHEME = "SIGNATURE_SCHEME_TK_API_P256"
+
+private fun ByteArray.hex(): String = joinToString("") { "%02x".format(it) }
+
+private fun base64url(value: String): String =
+ Base64.encodeToString(
+ value.toByteArray(Charsets.UTF_8),
+ Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP,
+ )
+
+private fun compressedPublicKeyHex(privateKey: ECPrivateKey): String {
+ val curve = ECNamedCurveTable.getParameterSpec("secp256r1")
+ return curve.g.multiply(privateKey.s).normalize().getEncoded(true).hex()
+}
-fun signPayload(
+fun buildGridWalletSignature(
sessionPrivateScalar: ByteArray, // 32 bytes
payloadToSign: String,
p256Params: ECParameterSpec,
@@ -275,8 +318,10 @@ fun signPayload(
initSign(privateKey)
update(payloadToSign.toByteArray(Charsets.UTF_8))
}
- val der = signer.sign() // JCE returns DER-encoded ECDSA
- return Base64.encodeToString(der, Base64.NO_WRAP)
+ val derSignatureHex = signer.sign().hex() // JCE returns DER-encoded ECDSA.
+ val publicKeyHex = compressedPublicKeyHex(privateKey as ECPrivateKey) // 33-byte SEC1 form.
+ val stampJson = """{"publicKey":"$publicKeyHex","scheme":"$TURNKEY_STAMP_SCHEME","signature":"$derSignatureHex"}"""
+ return base64url(stampJson)
}
```
@@ -284,24 +329,33 @@ fun signPayload(
import CryptoKit
import Foundation
-func signPayload(
+func buildGridWalletSignature(
sessionPrivateScalar: Data, // 32 bytes
payloadToSign: String,
) throws -> String {
let signingKey = try P256.Signing.PrivateKey(rawRepresentation: sessionPrivateScalar)
let payload = Data(payloadToSign.utf8)
let signature = try signingKey.signature(for: payload)
- return signature.derRepresentation.base64EncodedString()
+ let signatureHex = signature.derRepresentation.map { String(format: "%02x", $0) }.joined()
+ let publicKeyHex = signingKey.publicKey.compressedRepresentation
+ .map { String(format: "%02x", $0) }
+ .joined()
+ let stampJson = #"{"publicKey":"\#(publicKeyHex)","scheme":"SIGNATURE_SCHEME_TK_API_P256","signature":"\#(signatureHex)"}"#
+ return Data(stampJson.utf8)
+ .base64EncodedString()
+ .replacingOccurrences(of: "+", with: "-")
+ .replacingOccurrences(of: "/", with: "_")
+ .replacingOccurrences(of: "=", with: "")
}
```
-Your backend adds the signature to the retry request:
+Your backend adds the stamp to the retry request:
```bash
curl -X POST "https://api.lightspark.com/grid/2025-10-13/quotes/Quote:019542f5-b3e7-1d02-0000-000000000006/execute" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
- -H "Grid-Wallet-Signature: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE="
+ -H "Grid-Wallet-Signature: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9"
```
## Session lifetime
diff --git a/mintlify/snippets/global-accounts/concepts.mdx b/mintlify/snippets/global-accounts/concepts.mdx
index 77c8ccab..e07b8464 100644
--- a/mintlify/snippets/global-accounts/concepts.mdx
+++ b/mintlify/snippets/global-accounts/concepts.mdx
@@ -35,7 +35,7 @@ Three distinct pieces of crypto collaborate to authorize actions on the Global A
| Piece | Where it lives | How long it lives | What it proves |
|---|---|---|---|
| **Auth credential** — passkey, OIDC token, or email OTP | Registered on the account; the passkey itself lives on the authenticator, OIDC on your IdP, OTP in the user's inbox | Until the customer revokes it | *"I am the human who owns this account."* Used to authenticate the user at the start of each session. |
-| **Client key pair** (P-256) | Generated on the client device for each verification request; private key stays in device-local secure storage | One verification request | Binds a given session signing key delivery to the exact device that asked for it — Grid encrypts the session to this public key, so only this device can decrypt. |
-| **Session signing key** (P-256) | Issued by Grid, sealed to the client public key, decrypted and held on the device for the session's lifetime | 15 minutes (default) | *"This specific account action was approved on an authenticated device."* Signs the `payloadToSign` Grid returns on quotes, credential changes, session revocations, wallet exports, and wallet privacy updates. |
+| **Client key pair** (P-256) | Generated on the client device for each session-issuing or export request; private key stays in device-local secure storage | One authentication, session refresh, or wallet export | Binds a given session signing key or wallet export delivery to the exact device that asked for it — Grid encrypts the response to this public key, so only this device can decrypt. |
+| **Session signing key** (P-256) | Issued by Grid, sealed to the client public key, decrypted and held on the device for the session's lifetime | 15 minutes (default) | *"This specific account action was approved on an authenticated device."* Builds Turnkey API-key stamps over the `payloadToSign` Grid returns on quotes, credential changes, session refresh/revocation, wallet exports, customer email updates, and wallet privacy updates. |
-The flow is always the same: verify an auth credential → receive a short-lived session signing key → sign `payloadToSign` bytes on the client → pass the signature as the `Grid-Wallet-Signature` header on the request that actually moves funds or changes account state. This applies to withdrawals, adding or removing credentials, revoking sessions, exporting the wallet seed, and updating wallet privacy.
+The flow is always the same: verify an auth credential → receive a short-lived session signing key → build a Turnkey API-key stamp over the `payloadToSign` bytes on the client → pass that stamp as the `Grid-Wallet-Signature` header on the request that actually moves funds or changes account state. This applies to withdrawals, adding or removing credentials, refreshing or revoking sessions, exporting the wallet seed, updating customer email for tied email OTP credentials, and updating wallet privacy.
diff --git a/mintlify/snippets/global-accounts/exporting-wallet.mdx b/mintlify/snippets/global-accounts/exporting-wallet.mdx
index 7d2ec7a2..58389c14 100644
--- a/mintlify/snippets/global-accounts/exporting-wallet.mdx
+++ b/mintlify/snippets/global-accounts/exporting-wallet.mdx
@@ -13,8 +13,8 @@ sequenceDiagram
IB->>G: POST /internal-accounts/{id}/export { clientPublicKey }
G-->>IB: 202 { payloadToSign, requestId, expiresAt }
IB-->>C: { payloadToSign, requestId }
- C->>C: sign(payloadToSign, sessionPrivateKey)
- C->>IB: { signature }
+ C->>C: stamp(payloadToSign, sessionPrivateKey)
+ C->>IB: { stamp }
IB->>G: POST /internal-accounts/{id}/export { same clientPublicKey }
Grid-Wallet-Signature
Request-Id
G-->>IB: 200 { id, encryptedWalletCredentials }
IB-->>C: { encryptedWalletCredentials }
@@ -42,15 +42,15 @@ sequenceDiagram
}
```
-
- Sign `payloadToSign` with an active session signing key on the account. Keep the export private key on the client; Grid will use the matching `clientPublicKey` from step 1 to seal the wallet credentials.
+
+ Build a Turnkey API-key stamp over `payloadToSign` with an active session signing key on the account. Keep the export private key on the client; Grid will use the matching `clientPublicKey` from step 1 to seal the wallet credentials.
```bash
curl -X POST "$GRID_BASE_URL/internal-accounts/InternalAccount:019542f5-b3e7-1d02-0000-000000000002/export" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
- -H "Grid-Wallet-Signature: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE=" \
+ -H "Grid-Wallet-Signature: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9" \
-H "Request-Id: Request:c3f8a614-47e2-4a19-9f5d-2b0a91d47e08" \
-d '{
"clientPublicKey": "04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"
diff --git a/mintlify/snippets/global-accounts/managing-sessions.mdx b/mintlify/snippets/global-accounts/managing-sessions.mdx
index 81f16c1f..1d3eca0a 100644
--- a/mintlify/snippets/global-accounts/managing-sessions.mdx
+++ b/mintlify/snippets/global-accounts/managing-sessions.mdx
@@ -1,4 +1,4 @@
-Every call to `POST /auth/credentials/{id}/verify` creates a new **session** — an authenticated signing context with a 15-minute lifetime by default. Sessions accumulate: a customer signed in on a laptop and a phone has two active sessions, each with its own session signing key held on that device. Use the session endpoints to show the customer their active sign-ins and to sign out of a specific device.
+Every call to `POST /auth/credentials/{id}/verify` creates a new **session** — an authenticated signing context with a 15-minute lifetime by default. Sessions accumulate: a customer signed in on a laptop and a phone has two active sessions, each with its own session signing key held on that device. Use the session endpoints to show active sign-ins, refresh an active session before it expires, and sign out of a specific device.
## List active sessions
@@ -36,6 +36,50 @@ curl -X GET "$GRID_BASE_URL/auth/sessions?accountId=InternalAccount:019542f5-b3e
The list endpoint returns all **active** sessions; expired sessions are not included. `encryptedSessionSigningKey` is never returned here — it is delivered exactly once on the verify response and never again.
+## Refresh a session
+
+Session refresh creates a new session signing key from an existing active session. Use this when the customer is still present and the current session is close to expiration. If the session has already expired, reauthenticate with the original credential instead.
+
+
+
+ ```bash
+ curl -X POST "$GRID_BASE_URL/auth/sessions/Session:019542f5-b3e7-1d02-0000-000000000003/refresh" \
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "clientPublicKey": "04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"
+ }'
+ ```
+
+ **Response (202):**
+
+ ```json
+ {
+ "payloadToSign": "{\"organizationId\":\"org_2m9F...\",\"parameters\":{\"targetPublicKey\":\"04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2\"},\"timestampMs\":\"1775681700000\",\"type\":\"ACTIVITY_TYPE_CREATE_READ_WRITE_SESSION_V2\"}",
+ "requestId": "Request:8c1e7f55-7b9c-4383-86c7-0cbde77c7328",
+ "expiresAt": "2026-04-19T12:10:00Z"
+ }
+ ```
+
+
+ Build a Turnkey API-key stamp over `payloadToSign` with the current session signing key.
+
+
+ ```bash
+ curl -X POST "$GRID_BASE_URL/auth/sessions/Session:019542f5-b3e7-1d02-0000-000000000003/refresh" \
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
+ -H "Content-Type: application/json" \
+ -H "Grid-Wallet-Signature: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9" \
+ -H "Request-Id: Request:8c1e7f55-7b9c-4383-86c7-0cbde77c7328" \
+ -d '{
+ "clientPublicKey": "04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"
+ }'
+ ```
+
+ **Response (201):** `AuthSession` with a new `encryptedSessionSigningKey`. Decrypt it with the private key matching the `clientPublicKey` above and replace the old session signing key on the client.
+
+
+
## Revoke a session
Session revocation uses the same signed-retry pattern as credential management. Unlike credential revocation, a session **can revoke itself** — this is how self-logout works: sign with the session key you are about to invalidate.
@@ -58,14 +102,14 @@ Session revocation uses the same
- Sign `payloadToSign` with any active session signing key on the same account — either the session being revoked (self-logout) or another session (admin-style sign-out of a different device).
+
+ Build a Turnkey API-key stamp over `payloadToSign` with any active session signing key on the same account — either the session being revoked (self-logout) or another session (admin-style sign-out of a different device).
```bash
curl -X DELETE "$GRID_BASE_URL/auth/sessions/Session:019542f5-b3e7-1d02-0000-000000000003" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
- -H "Grid-Wallet-Signature: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE=" \
+ -H "Grid-Wallet-Signature: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9" \
-H "Request-Id: Request:2b1e5a08-9c44-4e91-ae7f-6d0b3f8c1e22"
```
diff --git a/mintlify/snippets/global-accounts/walkthrough.mdx b/mintlify/snippets/global-accounts/walkthrough.mdx
index 7e21cd8c..04450541 100644
--- a/mintlify/snippets/global-accounts/walkthrough.mdx
+++ b/mintlify/snippets/global-accounts/walkthrough.mdx
@@ -19,7 +19,7 @@ export GRID_CLIENT_SECRET="YOUR_SANDBOX_CLIENT_SECRET"
## Walkthrough
-The walkthrough below is the happy path: create a customer, find the auto-provisioned account, register a passkey, fund it, and withdraw to a bank account. Each step shows the HTTP request your integrator backend makes on behalf of the client.
+The walkthrough below is the happy path: create a customer, find the auto-provisioned account and its default email OTP credential, fund it, and withdraw to a bank account. Each step shows the HTTP request your integrator backend makes on behalf of the client.
### 1. Create a customer
@@ -80,43 +80,33 @@ curl -X GET "$GRID_BASE_URL/internal-accounts?customerId=Customer:019542f5-b3e7-
Hold onto the `InternalAccount:...` id — every auth credential is scoped to it.
-### 3. Register a passkey credential
+### 3. Find the default email OTP credential
-Global Accounts support three authentication credential types: **passkey**, **OAuth (OIDC)**, and **email OTP**. A passkey is a user-friendly default: biometric, phishing-resistant, and usable across the user's devices.
+Global Accounts are initialized with an `EMAIL_OTP` credential tied to the customer email on file. Fetch the auth methods for the account and keep the `AuthMethod:...` id for the signing step later in this walkthrough.
-Registration only binds the passkey to the account — it doesn't issue a session. Sessions are created on-demand, when the customer initiates an action that needs a signature (step 7). The full flow with sequence diagram is documented in Authentication; the condensed version:
+```bash
+curl -X GET "$GRID_BASE_URL/auth/credentials?accountId=InternalAccount:019542f5-b3e7-1d02-0000-000000000002" \
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
+```
-
-
- Generate a random base64url `challenge`, store it short-lived in your session store, and return it to the client.
-
-
- The browser or OS prompts the user for a biometric, returns an `attestation`. The client posts the attestation back to your backend.
-
-
- ```bash
- curl -X POST "$GRID_BASE_URL/auth/credentials" \
- -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
- -H "Content-Type: application/json" \
- -d '{
- "type": "PASSKEY",
- "accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002",
- "nickname": "iPhone Face-ID",
- "challenge": "ArkQi2yAYHPlgnJNFBlneIwchQdWXBOTrdB-AmMUB21Lx",
- "attestation": {
- "credentialId": "AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY",
- "clientDataJson": "eyJjaGFsbGVuZ2UiOiJBcktRaTJ5...",
- "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10...",
- "transports": ["internal", "hybrid"]
- }
- }'
- ```
+**Response:**
- Grid verifies the attestation and replies `201` with the new `AuthMethod:...` id plus the first-authentication `challenge`, `requestId`, and `expiresAt`. Persist the auth method id against the customer — you'll pass it to `/challenge` and `/verify` whenever the customer needs to sign.
-
-
+```json
+{
+ "data": [
+ {
+ "id": "AuthMethod:019542f5-b3e7-1d02-0000-000000000001",
+ "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"
+ }
+ ]
+}
+```
-The passkey is now bound to the account. You'll use it to authorize the withdrawal in step 7.
+You can add passkeys or OAuth credentials later, but adding credentials is itself a signed action. Start with the default email OTP credential to mint the first session signing key.
### 4. Fund the Account
@@ -228,7 +218,7 @@ curl -X POST "$GRID_BASE_URL/quotes" \
"accountType": "EMBEDDED_WALLET",
"payloadToSign": "{\"type\":\"ACTIVITY_TYPE_SIGN_TRANSACTION_V2\",\"timestampMs\":\"1746736509954\",\"organizationId\":\"org_abc123\",\"parameters\":{\"signWith\":\"wallet_abc123def456\",\"unsignedTransaction\":\"ea69b4bf05f775209f26ff0a34a05569180f7936579d5c4af9377ae550194f72\",\"type\":\"TRANSACTION_TYPE_ETHEREUM\"},\"generateAppProofs\":true}"
},
- "instructionsNotes": "Sign the payloadToSign byte-for-byte and pass the signature as the Grid-Wallet-Signature header on execute"
+ "instructionsNotes": "Stamp the payloadToSign byte-for-byte and pass the stamp as the Grid-Wallet-Signature header on execute"
}
]
}
@@ -236,19 +226,15 @@ 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 — this is when the passkey actually gets used. The flow is keypair → challenge → assertion → verify → decrypt → 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 client generates a fresh P-256 client key pair and posts the public key (uncompressed hex) to your backend, which forwards it to Grid. Grid bakes the public key into the session-creation payload so the resulting session signing key is sealed to that device.
+
+ Ask Grid to send a fresh OTP email for the default `EMAIL_OTP` credential.
```bash
curl -X POST "$GRID_BASE_URL/auth/credentials/AuthMethod:019542f5-b3e7-1d02-0000-000000000001/challenge" \
- -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
- -H "Content-Type: application/json" \
- -d '{
- "clientPublicKey": "04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"
- }'
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
**Response (200):**
@@ -257,66 +243,36 @@ The customer has an outstanding quote with a `payloadToSign`. Now we need a sess
{
"id": "AuthMethod:019542f5-b3e7-1d02-0000-000000000001",
"accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002",
- "type": "PASSKEY",
- "nickname": "iPhone Face-ID",
+ "type": "EMAIL_OTP",
+ "nickname": "jane@example.com",
"createdAt": "2026-04-19T12:00:01Z",
- "updatedAt": "2026-04-19T12:05:00Z",
- "challenge": "VjZ6o8KfE9V3q3LkR2nH5eZ6dM8yA1xW",
- "requestId": "7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21",
- "expiresAt": "2026-04-19T12:10:00Z"
+ "updatedAt": "2026-04-19T12:05:00Z"
}
```
-
- Return `challenge` and `requestId` to the client.
-
- Prompt the authenticator with the `challenge` returned in the previous step:
-
- ```js
- const base64urlToBytes = (value) => {
- const padded = value.replace(/-/g, "+").replace(/_/g, "/")
- .padEnd(Math.ceil(value.length / 4) * 4, "=");
- return Uint8Array.from(atob(padded), (char) => char.charCodeAt(0));
- };
-
- const assertion = await navigator.credentials.get({
- publicKey: {
- challenge: base64urlToBytes(gridChallenge),
- rpId: "yourapp.com",
- userVerification: "required",
- },
- });
- ```
-
- Post the assertion (and the `requestId` from the previous step) back to your backend.
+
+ The client generates a fresh P-256 client key pair 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.
-
+
```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 "Request-Id: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21" \
-d '{
- "type": "PASSKEY",
- "assertion": {
- "credentialId": "KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew",
- "clientDataJson": "eyJjaGFsbGVuZ2UiOiJWalo2bzhLZkU5VjNxM0xrUjJuSDVlWjZkTTh5QTF4VyIsIm9yaWdpbiI6Imh0dHBzOi8veW91cmFwcC5jb20iLCJ0eXBlIjoid2ViYXV0aG4uZ2V0In0",
- "authenticatorData": "PdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KABAAAAkA",
- "signature": "MEUCIQDYXBOpCWSWq2Ll4558GJKD2RoWg958lvJSB_GdeokxogIgWuEVQ7ee6AswQY0OsuQ6y8Ks6jhd45bDx92wjXKs900"
- }
+ "type": "EMAIL_OTP",
+ "otp": "123456",
+ "clientPublicKey": "04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"
}'
```
- `clientPublicKey` is no longer required here — Grid already has it from the `/challenge` call. The `Request-Id` header ties this verify to that earlier challenge.
-
**Response (200):**
```json
{
"id": "Session:019542f5-b3e7-1d02-0000-000000000003",
"accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002",
- "type": "PASSKEY",
- "nickname": "iPhone Face-ID",
+ "type": "EMAIL_OTP",
+ "nickname": "jane@example.com",
"encryptedSessionSigningKey": "w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf",
"createdAt": "2026-04-19T12:05:01Z",
"updatedAt": "2026-04-19T12:05:01Z",
@@ -326,27 +282,27 @@ The customer has an outstanding quote with a `payloadToSign`. Now we need a sess
Return `encryptedSessionSigningKey` and `expiresAt` to the client.
-
- The client decrypts `encryptedSessionSigningKey` with the matching client private key, then signs the quote's `payloadToSign` with the resulting session signing key. Return the base64 signature to your backend.
+
+ The client decrypts `encryptedSessionSigningKey` with the matching client private key, then stamps the quote's `payloadToSign` with the resulting session signing key. Return the full Turnkey API-key stamp to your backend.
- Sign the `payloadToSign` bytes exactly as Grid returned them. Do not parse, re-serialize, trim, or normalize the JSON — the signature must cover the same bytes Grid's verifier hashes.
+ Stamp the `payloadToSign` bytes exactly as Grid returned them. Do not parse, re-serialize, trim, or normalize the JSON — the stamp must cover the same bytes Grid's verifier hashes.
The session signing key is now valid for 15 minutes, so subsequent account actions within that window (for example, a second withdrawal) can reuse it without another `/challenge` + `/verify` round-trip.
### 8. Execute the quote
-Call `/execute` with the signature in the `Grid-Wallet-Signature` header.
+Call `/execute` with the stamp in the `Grid-Wallet-Signature` header.
```bash
curl -X POST "$GRID_BASE_URL/quotes/Quote:019542f5-b3e7-1d02-0000-000000000006/execute" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21" \
- -H "Grid-Wallet-Signature: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE="
+ -H "Grid-Wallet-Signature: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9"
```
**Response:**
@@ -374,7 +330,7 @@ The transaction is on its way. You'll receive standard transaction webhooks (`OU
OAuth and Email OTP flows, passkey reauthentication, and the full WebAuthn parameter mapping.
- List active sessions and revoke a session (sign-out).
+ List, refresh, and revoke active sessions.
Let a customer take their wallet seed off Grid.
diff --git a/openapi.yaml b/openapi.yaml
index 70e482bc..b8830b17 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -2482,10 +2482,11 @@ paths:
or has direct pull functionality (e.g. ACH pull with an external account).
When the quote's `source` is an internal account of type `EMBEDDED_WALLET`,
- the request must include a `Grid-Wallet-Signature` header. The signature is
- produced by signing the `payloadToSign` value from the quote's
- `paymentInstructions[].accountOrWalletInfo` entry with the session private
- key of a verified authentication credential on the source Embedded Wallet.
+ the request must include a `Grid-Wallet-Signature` header. The header value
+ is the full Turnkey API-key stamp built over the `payloadToSign` value from
+ the quote's `paymentInstructions[].accountOrWalletInfo` entry with the
+ session private key of a verified authentication credential on the source
+ Embedded Wallet.
Once executed, the quote cannot be cancelled and the transfer will be processed.
operationId: executeQuote
@@ -2512,10 +2513,10 @@ paths:
- name: Grid-Wallet-Signature
in: header
required: false
- description: Signature over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet and base64-encoded. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types.
+ description: Full Turnkey API-key stamp over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types.
schema:
type: string
- example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE=
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
responses:
'200':
description: |
@@ -4606,7 +4607,7 @@ paths:
challenge:
summary: Session refresh challenge
value:
- payloadToSign: '{"type":"ACTIVITY_TYPE_CREATE_READ_WRITE_SESSION_V2","timestampMs":"1746736509954","organizationId":"org_abc123","parameters":{"targetPublicKey":"04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"}}'
+ payloadToSign: '{"organizationId":"org_abc123","parameters":{"targetPublicKey":"04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"},"timestampMs":"1746736509954","type":"ACTIVITY_TYPE_CREATE_READ_WRITE_SESSION_V2"}'
requestId: Request:019542f5-b3e7-1d02-0000-000000000010
expiresAt: '2026-04-08T15:35:00Z'
'400':
@@ -5617,10 +5618,10 @@ paths:
- name: Grid-Wallet-Signature
in: header
required: false
- description: Signature over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet and base64-encoded. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types.
+ description: Full Turnkey API-key stamp over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types.
schema:
type: string
- example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE=
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
responses:
'200':
description: 'Action submitted successfully. If the agent''s policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated. Note: if approval is required, the underlying quote may expire before the platform approves — in that case the action will transition to `FAILED`.'
@@ -10765,7 +10766,7 @@ components:
description: Discriminator value identifying this as Embedded Wallet payment instructions.
payloadToSign:
type: string
- description: JSON-encoded transaction signing payload that must be signed, as-is (byte-for-byte, without re-serialization), with the session private key of a verified authentication credential on the source Embedded Wallet. The resulting signature is base64-encoded and passed as the `Grid-Wallet-Signature` header on `POST /quotes/{quoteId}/execute` to authorize the outbound transfer from the wallet.
+ description: JSON-encoded transaction signing payload that must be stamped, as-is (byte-for-byte, without re-serialization), with the session private key of a verified authentication credential on the source Embedded Wallet. The resulting Turnkey API-key stamp is passed as the `Grid-Wallet-Signature` header on `POST /quotes/{quoteId}/execute` to authorize the outbound transfer from the wallet.
example: '{"type":"ACTIVITY_TYPE_SIGN_TRANSACTION_V2","timestampMs":"1746736509954","organizationId":"org_abc123","parameters":{"signWith":"wallet_abc123def456","unsignedTransaction":"ea69b4bf05f775209f26ff0a34a05569180f7936579d5c4af9377ae550194f72","type":"TRANSACTION_TYPE_ETHEREUM"},"generateAppProofs":true}'
PaymentInstructions:
type: object
@@ -15588,7 +15589,7 @@ components:
description: |-
Whether to immediately execute the quote after creation. If true, the quote will be executed and the transaction will be created at the current exchange rate. It should only be used if you don't want to lock and view rate details before executing the quote. If you are executing a pre-existing quote, use the `/quotes/{quoteId}/execute` endpoint instead. This is false by default.
This can only be used for quotes with a `source` which is either an internal account, or has direct pull functionality (e.g. ACH pull with an external account).
- Not supported when the `source` is an internal account of type `EMBEDDED_WALLET`: those transfers require a `Grid-Wallet-Signature` over the `payloadToSign` returned in the quote response, which is not available in a combined create-and-execute call. Create the quote first with `immediatelyExecute: false` and then call `POST /quotes/{quoteId}/execute` with the signature header.
+ Not supported when the `source` is an internal account of type `EMBEDDED_WALLET`: those transfers require a `Grid-Wallet-Signature` over the `payloadToSign` returned in the quote response, which is not available in a combined create-and-execute call. Create the quote first with `immediatelyExecute: false` and then call `POST /quotes/{quoteId}/execute` with the `Grid-Wallet-Signature` stamp header.
example: false
description:
type: string
diff --git a/openapi/components/schemas/common/PaymentEmbeddedWalletInfo.yaml b/openapi/components/schemas/common/PaymentEmbeddedWalletInfo.yaml
index d91fd070..2261af99 100644
--- a/openapi/components/schemas/common/PaymentEmbeddedWalletInfo.yaml
+++ b/openapi/components/schemas/common/PaymentEmbeddedWalletInfo.yaml
@@ -14,10 +14,10 @@ allOf:
payloadToSign:
type: string
description: >-
- JSON-encoded transaction signing payload that must be signed, as-is
+ JSON-encoded transaction signing payload that must be stamped, as-is
(byte-for-byte, without re-serialization), with the session private
key of a verified authentication credential on the source Embedded
- Wallet. The resulting signature is base64-encoded and passed as the
+ Wallet. The resulting Turnkey API-key stamp is passed as the
`Grid-Wallet-Signature` header on `POST /quotes/{quoteId}/execute`
to authorize the outbound transfer from the wallet.
example: '{"type":"ACTIVITY_TYPE_SIGN_TRANSACTION_V2","timestampMs":"1746736509954","organizationId":"org_abc123","parameters":{"signWith":"wallet_abc123def456","unsignedTransaction":"ea69b4bf05f775209f26ff0a34a05569180f7936579d5c4af9377ae550194f72","type":"TRANSACTION_TYPE_ETHEREUM"},"generateAppProofs":true}'
diff --git a/openapi/components/schemas/quotes/QuoteRequest.yaml b/openapi/components/schemas/quotes/QuoteRequest.yaml
index 5b5cb20a..f8a98c92 100644
--- a/openapi/components/schemas/quotes/QuoteRequest.yaml
+++ b/openapi/components/schemas/quotes/QuoteRequest.yaml
@@ -48,7 +48,8 @@ properties:
over the `payloadToSign` returned in the quote response, which is not
available in a combined create-and-execute call. Create the quote
first with `immediatelyExecute: false` and then call
- `POST /quotes/{quoteId}/execute` with the signature header.
+ `POST /quotes/{quoteId}/execute` with the `Grid-Wallet-Signature` stamp
+ header.
example: false
description:
type: string
diff --git a/openapi/paths/agents/agents_me_quotes_{quoteId}_execute.yaml b/openapi/paths/agents/agents_me_quotes_{quoteId}_execute.yaml
index 7754dbe8..6913027a 100644
--- a/openapi/paths/agents/agents_me_quotes_{quoteId}_execute.yaml
+++ b/openapi/paths/agents/agents_me_quotes_{quoteId}_execute.yaml
@@ -36,15 +36,15 @@ post:
in: header
required: false
description: >-
- Signature over the `payloadToSign` returned in the quote's
- `paymentInstructions[].accountOrWalletInfo` entry, produced with the
- session private key of a verified authentication credential on the
- source Embedded Wallet and base64-encoded. Required when the quote's
- source is an internal account of type `EMBEDDED_WALLET`; ignored for
- other source types.
+ Full Turnkey API-key stamp over the `payloadToSign` returned in the
+ quote's `paymentInstructions[].accountOrWalletInfo` entry, produced
+ with the session private key of a verified authentication credential on
+ the source Embedded Wallet. Required when the quote's source is an
+ internal account of type `EMBEDDED_WALLET`; ignored for other source
+ types.
schema:
type: string
- example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE=
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
responses:
'200':
description: >-
diff --git a/openapi/paths/auth/auth_sessions_{id}_refresh.yaml b/openapi/paths/auth/auth_sessions_{id}_refresh.yaml
index cf4f4d44..25a4b81f 100644
--- a/openapi/paths/auth/auth_sessions_{id}_refresh.yaml
+++ b/openapi/paths/auth/auth_sessions_{id}_refresh.yaml
@@ -99,7 +99,7 @@ post:
challenge:
summary: Session refresh challenge
value:
- payloadToSign: '{"type":"ACTIVITY_TYPE_CREATE_READ_WRITE_SESSION_V2","timestampMs":"1746736509954","organizationId":"org_abc123","parameters":{"targetPublicKey":"04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"}}'
+ payloadToSign: '{"organizationId":"org_abc123","parameters":{"targetPublicKey":"04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"},"timestampMs":"1746736509954","type":"ACTIVITY_TYPE_CREATE_READ_WRITE_SESSION_V2"}'
requestId: Request:019542f5-b3e7-1d02-0000-000000000010
expiresAt: '2026-04-08T15:35:00Z'
'400':
diff --git a/openapi/paths/quotes/quotes_{quoteId}_execute.yaml b/openapi/paths/quotes/quotes_{quoteId}_execute.yaml
index f7020d0a..2ff83ef8 100644
--- a/openapi/paths/quotes/quotes_{quoteId}_execute.yaml
+++ b/openapi/paths/quotes/quotes_{quoteId}_execute.yaml
@@ -8,10 +8,11 @@ post:
or has direct pull functionality (e.g. ACH pull with an external account).
When the quote's `source` is an internal account of type `EMBEDDED_WALLET`,
- the request must include a `Grid-Wallet-Signature` header. The signature is
- produced by signing the `payloadToSign` value from the quote's
- `paymentInstructions[].accountOrWalletInfo` entry with the session private
- key of a verified authentication credential on the source Embedded Wallet.
+ the request must include a `Grid-Wallet-Signature` header. The header value
+ is the full Turnkey API-key stamp built over the `payloadToSign` value from
+ the quote's `paymentInstructions[].accountOrWalletInfo` entry with the
+ session private key of a verified authentication credential on the source
+ Embedded Wallet.
Once executed, the quote cannot be cancelled and the transfer will be processed.
operationId: executeQuote
@@ -40,15 +41,15 @@ post:
in: header
required: false
description: >-
- Signature over the `payloadToSign` returned in the quote's
- `paymentInstructions[].accountOrWalletInfo` entry, produced with the
- session private key of a verified authentication credential on the
- source Embedded Wallet and base64-encoded. Required when the quote's
- source is an internal account of type `EMBEDDED_WALLET`; ignored for
- other source types.
+ Full Turnkey API-key stamp over the `payloadToSign` returned in the
+ quote's `paymentInstructions[].accountOrWalletInfo` entry, produced
+ with the session private key of a verified authentication credential on
+ the source Embedded Wallet. Required when the quote's source is an
+ internal account of type `EMBEDDED_WALLET`; ignored for other source
+ types.
schema:
type: string
- example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE=
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
responses:
'200':
description: >