diff --git a/.stainless/stainless.yml b/.stainless/stainless.yml index 95e3f866..c7c0f20e 100644 --- a/.stainless/stainless.yml +++ b/.stainless/stainless.yml @@ -91,6 +91,8 @@ resources: business_info: "#/components/schemas/BusinessInfo" internal_account_export_request: '#/components/schemas/InternalAccountExportRequest' internal_account_export_response: '#/components/schemas/InternalAccountExportResponse' + internal_account_wallet_privacy_update_request: '#/components/schemas/InternalAccountWalletPrivacyUpdateRequest' + internal_account_wallet_privacy_update_response: '#/components/schemas/InternalAccountWalletPrivacyUpdateResponse' methods: create: endpoint: post /customers @@ -104,6 +106,9 @@ resources: get_kyc_link: get /customers/kyc-link list_internal_accounts: get /customers/internal-accounts export: post /internal-accounts/{id}/export + update_wallet_privacy: + endpoint: patch /internal-accounts/{id}/wallet-privacy + body_param_name: InternalAccountWalletPrivacyUpdateRequest # Subresources define resources that are nested within another for more powerful # logical groupings, e.g. `cards.payments`. subresources: diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 20d7ff40..8c1d0abe 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -3825,6 +3825,106 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /internal-accounts/{id}/wallet-privacy: + patch: + summary: Update internal account wallet privacy + description: | + Update the wallet privacy setting for an Embedded Wallet internal account. + + This is a two-step signed-retry flow: + + 1. Call `PATCH /internal-accounts/{id}/wallet-privacy` with the request body `{ "privateEnabled": true }` and no signature headers. Grid returns `202` with `payloadToSign`, `requestId`, and `expiresAt`. + + 2. Use the session API keypair of a verified authentication credential on the same internal account to build an API-key stamp over `payloadToSign`, then retry with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The retry body must carry the same `privateEnabled` value submitted in step 1. The signed retry returns `200` with the updated wallet privacy state. + operationId: updateInternalAccountWalletPrivacy + tags: + - Internal Accounts + security: + - BasicAuth: [] + parameters: + - name: id + in: path + description: The id of the internal account whose wallet privacy setting should be updated. + required: true + schema: + type: string + example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + - name: Grid-Wallet-Signature + in: header + required: false + description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified authentication credential on the target internal account. Required on the signed retry; ignored on the initial call. + schema: + type: string + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzaWduYXR1cmUiOiIzMDQ1MDIyMTAwLi4uIiwic2NoZW1lIjoiUDI1Nl9FQ0RTQV9TSEEyNTYifQ + - name: Request-Id + in: header + required: false + description: The `requestId` returned in a prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. + schema: + type: string + example: Request:019542f5-b3e7-1d02-0000-000000000010 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/InternalAccountWalletPrivacyUpdateRequest' + examples: + updateWalletPrivacy: + summary: Update wallet privacy request (both steps) + value: + privateEnabled: true + responses: + '200': + description: Signed retry accepted. Returns the updated wallet privacy state. + content: + application/json: + schema: + $ref: '#/components/schemas/InternalAccountWalletPrivacyUpdateResponse' + examples: + enabled: + summary: Wallet privacy enabled + value: + id: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + privateEnabled: true + updatedAt: '2026-04-08T15:35:02Z' + '202': + description: Challenge issued. The response contains `payloadToSign` (which binds the submitted `privateEnabled` value) plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair and echo `requestId` on the retry. + content: + application/json: + schema: + $ref: '#/components/schemas/SignedRequestChallenge' + examples: + challenge: + summary: Wallet privacy update challenge + value: + payloadToSign: Y2hhbGxlbmdlLXBheWxvYWQtdG8tc2lnbg== + requestId: Request:019542f5-b3e7-1d02-0000-000000000010 + expiresAt: '2026-04-08T15:35:00Z' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending wallet privacy update challenge for this internal account, when the `Request-Id` does not match an unexpired pending challenge, or when the retry's `privateEnabled` value does not match the one bound into `payloadToSign` on the initial call. + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Internal account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' /auth/credentials: post: summary: Create an authentication credential @@ -15703,6 +15803,38 @@ components: format: date-time description: Timestamp when the email address was updated. example: '2026-04-08T15:35:02Z' + InternalAccountWalletPrivacyUpdateRequest: + title: Internal Account Wallet Privacy Update Request + description: Request body for `PATCH /internal-accounts/{id}/wallet-privacy`. The `privateEnabled` value is required on both steps of the signed-retry flow. On step 1 Grid binds it into `payloadToSign`; on step 2 the client echoes the same value back and Grid updates the wallet privacy setting for the internal account's Embedded Wallet. + type: object + required: + - privateEnabled + properties: + privateEnabled: + type: boolean + description: Whether wallet privacy should be enabled for the Embedded Wallet. + example: true + InternalAccountWalletPrivacyUpdateResponse: + title: Internal Account Wallet Privacy Update Response + type: object + required: + - id + - privateEnabled + - updatedAt + properties: + id: + type: string + description: The id of the internal account whose wallet privacy setting was updated. + example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + privateEnabled: + type: boolean + description: Whether wallet privacy is enabled for the Embedded Wallet. + example: true + updatedAt: + type: string + format: date-time + description: Timestamp when the wallet privacy setting was updated. + example: '2026-04-08T15:35:02Z' AuthMethodType: type: string enum: diff --git a/mintlify/snippets/global-accounts/authentication.mdx b/mintlify/snippets/global-accounts/authentication.mdx index 8d193043..0f2bd3ee 100644 --- a/mintlify/snippets/global-accounts/authentication.mdx +++ b/mintlify/snippets/global-accounts/authentication.mdx @@ -540,7 +540,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, and exporting a wallet all share the same shape: +Adding an additional credential, revoking a credential, revoking a session, exporting a wallet, and updating wallet privacy all share the same shape: ```mermaid sequenceDiagram diff --git a/mintlify/snippets/global-accounts/client-keys.mdx b/mintlify/snippets/global-accounts/client-keys.mdx index 95ea7f5e..1d781a21 100644 --- a/mintlify/snippets/global-accounts/client-keys.mdx +++ b/mintlify/snippets/global-accounts/client-keys.mdx @@ -224,7 +224,7 @@ 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` — all 202 response bodies. +- `DELETE /auth/credentials/{id}`, `DELETE /auth/sessions/{id}`, `POST /internal-accounts/{id}/export`, `PATCH /internal-accounts/{id}/wallet-privacy` — 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). diff --git a/mintlify/snippets/global-accounts/concepts.mdx b/mintlify/snippets/global-accounts/concepts.mdx index 14a489ba..77c8ccab 100644 --- a/mintlify/snippets/global-accounts/concepts.mdx +++ b/mintlify/snippets/global-accounts/concepts.mdx @@ -30,12 +30,12 @@ The client **never** talks to Grid directly. Every request flows client → inte ## Auth credentials, client keys, and session signing keys -Three distinct pieces of crypto collaborate to authorize actions on the Global Account (withdrawals, credential changes, session revocations, and wallet exports): +Three distinct pieces of crypto collaborate to authorize actions on the Global Account (withdrawals, credential changes, session revocations, wallet exports, and wallet privacy updates): | 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, and wallet exports. | +| **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. | -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, and exporting the wallet seed. +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. diff --git a/mintlify/snippets/sandbox-global-account-magic.mdx b/mintlify/snippets/sandbox-global-account-magic.mdx index 2fca679f..1b83c8a0 100644 --- a/mintlify/snippets/sandbox-global-account-magic.mdx +++ b/mintlify/snippets/sandbox-global-account-magic.mdx @@ -83,6 +83,7 @@ Pass `sandbox-valid-signature` as the `Grid-Wallet-Signature` HTTP header on any - `DELETE /auth/credentials/{id}` (revoke credential) - `DELETE /auth/sessions/{id}` (revoke session) - `POST /internal-accounts/{id}/export` (export wallet) +- `PATCH /internal-accounts/{id}/wallet-privacy` (update wallet privacy) - `POST /quotes/{quoteId}/execute` (when source is an embedded wallet) ```bash diff --git a/openapi.yaml b/openapi.yaml index 20d7ff40..8c1d0abe 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3825,6 +3825,106 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /internal-accounts/{id}/wallet-privacy: + patch: + summary: Update internal account wallet privacy + description: | + Update the wallet privacy setting for an Embedded Wallet internal account. + + This is a two-step signed-retry flow: + + 1. Call `PATCH /internal-accounts/{id}/wallet-privacy` with the request body `{ "privateEnabled": true }` and no signature headers. Grid returns `202` with `payloadToSign`, `requestId`, and `expiresAt`. + + 2. Use the session API keypair of a verified authentication credential on the same internal account to build an API-key stamp over `payloadToSign`, then retry with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The retry body must carry the same `privateEnabled` value submitted in step 1. The signed retry returns `200` with the updated wallet privacy state. + operationId: updateInternalAccountWalletPrivacy + tags: + - Internal Accounts + security: + - BasicAuth: [] + parameters: + - name: id + in: path + description: The id of the internal account whose wallet privacy setting should be updated. + required: true + schema: + type: string + example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + - name: Grid-Wallet-Signature + in: header + required: false + description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified authentication credential on the target internal account. Required on the signed retry; ignored on the initial call. + schema: + type: string + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzaWduYXR1cmUiOiIzMDQ1MDIyMTAwLi4uIiwic2NoZW1lIjoiUDI1Nl9FQ0RTQV9TSEEyNTYifQ + - name: Request-Id + in: header + required: false + description: The `requestId` returned in a prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. + schema: + type: string + example: Request:019542f5-b3e7-1d02-0000-000000000010 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/InternalAccountWalletPrivacyUpdateRequest' + examples: + updateWalletPrivacy: + summary: Update wallet privacy request (both steps) + value: + privateEnabled: true + responses: + '200': + description: Signed retry accepted. Returns the updated wallet privacy state. + content: + application/json: + schema: + $ref: '#/components/schemas/InternalAccountWalletPrivacyUpdateResponse' + examples: + enabled: + summary: Wallet privacy enabled + value: + id: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + privateEnabled: true + updatedAt: '2026-04-08T15:35:02Z' + '202': + description: Challenge issued. The response contains `payloadToSign` (which binds the submitted `privateEnabled` value) plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair and echo `requestId` on the retry. + content: + application/json: + schema: + $ref: '#/components/schemas/SignedRequestChallenge' + examples: + challenge: + summary: Wallet privacy update challenge + value: + payloadToSign: Y2hhbGxlbmdlLXBheWxvYWQtdG8tc2lnbg== + requestId: Request:019542f5-b3e7-1d02-0000-000000000010 + expiresAt: '2026-04-08T15:35:00Z' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending wallet privacy update challenge for this internal account, when the `Request-Id` does not match an unexpired pending challenge, or when the retry's `privateEnabled` value does not match the one bound into `payloadToSign` on the initial call. + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Internal account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' /auth/credentials: post: summary: Create an authentication credential @@ -15703,6 +15803,38 @@ components: format: date-time description: Timestamp when the email address was updated. example: '2026-04-08T15:35:02Z' + InternalAccountWalletPrivacyUpdateRequest: + title: Internal Account Wallet Privacy Update Request + description: Request body for `PATCH /internal-accounts/{id}/wallet-privacy`. The `privateEnabled` value is required on both steps of the signed-retry flow. On step 1 Grid binds it into `payloadToSign`; on step 2 the client echoes the same value back and Grid updates the wallet privacy setting for the internal account's Embedded Wallet. + type: object + required: + - privateEnabled + properties: + privateEnabled: + type: boolean + description: Whether wallet privacy should be enabled for the Embedded Wallet. + example: true + InternalAccountWalletPrivacyUpdateResponse: + title: Internal Account Wallet Privacy Update Response + type: object + required: + - id + - privateEnabled + - updatedAt + properties: + id: + type: string + description: The id of the internal account whose wallet privacy setting was updated. + example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + privateEnabled: + type: boolean + description: Whether wallet privacy is enabled for the Embedded Wallet. + example: true + updatedAt: + type: string + format: date-time + description: Timestamp when the wallet privacy setting was updated. + example: '2026-04-08T15:35:02Z' AuthMethodType: type: string enum: diff --git a/openapi/components/schemas/internal_accounts/InternalAccountWalletPrivacyUpdateRequest.yaml b/openapi/components/schemas/internal_accounts/InternalAccountWalletPrivacyUpdateRequest.yaml new file mode 100644 index 00000000..a9ac6d60 --- /dev/null +++ b/openapi/components/schemas/internal_accounts/InternalAccountWalletPrivacyUpdateRequest.yaml @@ -0,0 +1,15 @@ +title: Internal Account Wallet Privacy Update Request +description: >- + Request body for `PATCH /internal-accounts/{id}/wallet-privacy`. The + `privateEnabled` value is required on both steps of the signed-retry + flow. On step 1 Grid binds it into `payloadToSign`; on step 2 the + client echoes the same value back and Grid updates the wallet privacy + setting for the internal account's Embedded Wallet. +type: object +required: + - privateEnabled +properties: + privateEnabled: + type: boolean + description: Whether wallet privacy should be enabled for the Embedded Wallet. + example: true diff --git a/openapi/components/schemas/internal_accounts/InternalAccountWalletPrivacyUpdateResponse.yaml b/openapi/components/schemas/internal_accounts/InternalAccountWalletPrivacyUpdateResponse.yaml new file mode 100644 index 00000000..e148358f --- /dev/null +++ b/openapi/components/schemas/internal_accounts/InternalAccountWalletPrivacyUpdateResponse.yaml @@ -0,0 +1,20 @@ +title: Internal Account Wallet Privacy Update Response +type: object +required: + - id + - privateEnabled + - updatedAt +properties: + id: + type: string + description: The id of the internal account whose wallet privacy setting was updated. + example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + privateEnabled: + type: boolean + description: Whether wallet privacy is enabled for the Embedded Wallet. + example: true + updatedAt: + type: string + format: date-time + description: Timestamp when the wallet privacy setting was updated. + example: '2026-04-08T15:35:02Z' diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index cee1ebf0..af6b5eb5 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -203,6 +203,8 @@ paths: $ref: paths/internal_accounts/internal_accounts_{id}_export.yaml /internal-accounts/{id}/email: $ref: paths/internal_accounts/internal_accounts_{id}_email.yaml + /internal-accounts/{id}/wallet-privacy: + $ref: paths/internal_accounts/internal_accounts_{id}_wallet-privacy.yaml /auth/credentials: $ref: paths/auth/auth_credentials.yaml /auth/credentials/{id}: diff --git a/openapi/paths/internal_accounts/internal_accounts_{id}_wallet-privacy.yaml b/openapi/paths/internal_accounts/internal_accounts_{id}_wallet-privacy.yaml new file mode 100644 index 00000000..6f9be777 --- /dev/null +++ b/openapi/paths/internal_accounts/internal_accounts_{id}_wallet-privacy.yaml @@ -0,0 +1,129 @@ +patch: + summary: Update internal account wallet privacy + description: > + Update the wallet privacy setting for an Embedded Wallet internal + account. + + + This is a two-step signed-retry flow: + + + 1. Call `PATCH /internal-accounts/{id}/wallet-privacy` with the request + body `{ "privateEnabled": true }` and no signature headers. Grid returns + `202` with `payloadToSign`, `requestId`, and `expiresAt`. + + + 2. Use the session API keypair of a verified authentication credential + on the same internal account to build an API-key stamp over + `payloadToSign`, then retry with that full stamp as the + `Grid-Wallet-Signature` header and the `requestId` echoed back as + the `Request-Id` header. The retry body must carry the same + `privateEnabled` value submitted in step 1. The signed retry returns + `200` with the updated wallet privacy state. + operationId: updateInternalAccountWalletPrivacy + tags: + - Internal Accounts + security: + - BasicAuth: [] + parameters: + - name: id + in: path + description: The id of the internal account whose wallet privacy setting should be updated. + required: true + schema: + type: string + example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + - name: Grid-Wallet-Signature + in: header + required: false + description: >- + Full API-key stamp built over the prior `payloadToSign` with + the session API keypair of a verified authentication credential + on the target internal account. Required on the signed retry; + ignored on the initial call. + schema: + type: string + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzaWduYXR1cmUiOiIzMDQ1MDIyMTAwLi4uIiwic2NoZW1lIjoiUDI1Nl9FQ0RTQV9TSEEyNTYifQ + - name: Request-Id + in: header + required: false + description: >- + The `requestId` returned in a prior `202` response, echoed back + on the signed retry so the server can correlate it with the + issued challenge. Required on the signed retry; must be paired + with `Grid-Wallet-Signature`. + schema: + type: string + example: Request:019542f5-b3e7-1d02-0000-000000000010 + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/internal_accounts/InternalAccountWalletPrivacyUpdateRequest.yaml + examples: + updateWalletPrivacy: + summary: Update wallet privacy request (both steps) + value: + privateEnabled: true + responses: + '200': + description: Signed retry accepted. Returns the updated wallet privacy state. + content: + application/json: + schema: + $ref: ../../components/schemas/internal_accounts/InternalAccountWalletPrivacyUpdateResponse.yaml + examples: + enabled: + summary: Wallet privacy enabled + value: + id: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + privateEnabled: true + updatedAt: '2026-04-08T15:35:02Z' + '202': + description: >- + Challenge issued. The response contains `payloadToSign` (which + binds the submitted `privateEnabled` value) plus a `requestId`. + Build an API-key stamp over `payloadToSign` with the session + API keypair and echo `requestId` on the retry. + content: + application/json: + schema: + $ref: ../../components/schemas/common/SignedRequestChallenge.yaml + examples: + challenge: + summary: Wallet privacy update challenge + value: + payloadToSign: Y2hhbGxlbmdlLXBheWxvYWQtdG8tc2lnbg== + requestId: Request:019542f5-b3e7-1d02-0000-000000000010 + expiresAt: '2026-04-08T15:35:00Z' + '400': + description: Bad request + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: >- + Unauthorized. Returned when the provided `Grid-Wallet-Signature` + is missing, malformed, or does not match a pending wallet privacy + update challenge for this internal account, when the `Request-Id` + does not match an unexpired pending challenge, or when the retry's + `privateEnabled` value does not match the one bound into + `payloadToSign` on the initial call. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Internal account not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '500': + description: Internal service error + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error500.yaml