diff --git a/mintlify/api-reference/sandbox-testing.mdx b/mintlify/api-reference/sandbox-testing.mdx index 9313ecd8..d73d723b 100644 --- a/mintlify/api-reference/sandbox-testing.mdx +++ b/mintlify/api-reference/sandbox-testing.mdx @@ -11,6 +11,7 @@ import SandboxTransferPatterns from '/snippets/sandbox-transfer-patterns.mdx'; import SandboxQuotePatterns from '/snippets/sandbox-quote-patterns.mdx'; import SandboxUmaAddresses from '/snippets/sandbox-uma-addresses.mdx'; import SandboxKybVerification from '/snippets/sandbox-kyb-verification.mdx'; +import SandboxGlobalAccountMagic from '/snippets/sandbox-global-account-magic.mdx'; The Grid sandbox environment simulates real payment flows without moving real money. You can control test outcomes using special account number patterns and test addresses. @@ -97,3 +98,7 @@ curl -X POST https://api.lightspark.com/grid/2025-10-13/sandbox/uma/receive \ "receivingCurrencyAmount": 5000 }' ``` + +## Global Account magic values + + diff --git a/mintlify/docs.json b/mintlify/docs.json index bb43852f..c3f6cfce 100644 --- a/mintlify/docs.json +++ b/mintlify/docs.json @@ -72,6 +72,61 @@ } ] }, + { + "tab": "Global Accounts", + "groups": [ + { + "group": "Overview", + "pages": [ + "global-accounts/index", + "global-accounts/core-concepts", + "global-accounts/implementation-overview", + "global-accounts/quickstart" + ] + }, + { + "group": "Onboarding", + "pages": [ + "global-accounts/platform-configuration", + "global-accounts/customers" + ] + }, + { + "group": "Managing Accounts", + "pages": [ + "global-accounts/internal-accounts", + "global-accounts/external-accounts" + ] + }, + { + "group": "Account security", + "pages": [ + "global-accounts/authentication", + "global-accounts/client-keys", + "global-accounts/managing-sessions", + "global-accounts/exporting-wallet" + ] + }, + { + "group": "Moving Money", + "pages": [ + "global-accounts/funding", + "global-accounts/withdrawals", + "global-accounts/list-transactions", + "global-accounts/reconciliation", + "global-accounts/error-handling" + ] + }, + { + "group": "Platform Tools", + "pages": [ + "global-accounts/webhooks", + "global-accounts/sandbox-testing", + "global-accounts/postman-collection" + ] + } + ] + }, { "tab": "Payouts & B2B", "groups": [ @@ -162,16 +217,6 @@ "ramps/conversion-flows/self-custody-wallets" ] }, - { - "group": "Embedded Wallets", - "pages": [ - "ramps/embedded-wallets/overview", - "ramps/embedded-wallets/client-keys", - "ramps/embedded-wallets/authentication", - "ramps/embedded-wallets/managing-sessions", - "ramps/embedded-wallets/exporting-wallet" - ] - }, { "group": "Platform Tools", "pages": [ @@ -214,16 +259,6 @@ "rewards/developer-guides/listing-transactions" ] }, - { - "group": "Embedded Wallets", - "pages": [ - "rewards/embedded-wallets/overview", - "rewards/embedded-wallets/client-keys", - "rewards/embedded-wallets/authentication", - "rewards/embedded-wallets/managing-sessions", - "rewards/embedded-wallets/exporting-wallet" - ] - }, { "group": "Platform Tools", "pages": [ @@ -271,16 +306,6 @@ "global-p2p/sending-receiving-payments/error-handling" ] }, - { - "group": "Embedded Wallets", - "pages": [ - "global-p2p/embedded-wallets/overview", - "global-p2p/embedded-wallets/client-keys", - "global-p2p/embedded-wallets/authentication", - "global-p2p/embedded-wallets/managing-sessions", - "global-p2p/embedded-wallets/exporting-wallet" - ] - }, { "group": "Platform Tools", "pages": [ diff --git a/mintlify/global-accounts/authentication.mdx b/mintlify/global-accounts/authentication.mdx new file mode 100644 index 00000000..1e57f1a4 --- /dev/null +++ b/mintlify/global-accounts/authentication.mdx @@ -0,0 +1,14 @@ +--- +title: "Authentication" +description: "Authenticate customers before signed Global Account actions." +icon: "/images/icons/shield.svg" +"og:image": "/images/og/og-global-accounts.webp" +--- + +import Authentication from '/snippets/embedded-wallets/authentication.mdx'; + +Global Accounts require customer authentication before outbound actions, credential changes, session revocation, and wallet export. + +Use this guide to register customer credentials, issue sessions, and authorize signed account actions. The endpoint group is named `Embedded Wallet Auth` in the API reference. + + diff --git a/mintlify/global-accounts/client-keys.mdx b/mintlify/global-accounts/client-keys.mdx new file mode 100644 index 00000000..24af867a --- /dev/null +++ b/mintlify/global-accounts/client-keys.mdx @@ -0,0 +1,32 @@ +--- +title: "Client keys" +description: "Understand the client keys used to sign Global Account actions." +icon: "/images/icons/key2.svg" +"og:image": "/images/og/og-global-accounts.webp" +--- + +import ClientKeys from '/snippets/embedded-wallets/client-keys.mdx'; + +Client keys bind a signed Global Account action to the customer's current device. + +When a customer authorizes an outbound action, the client generates a key pair and sends the public key to Grid during credential verification. Grid encrypts the short-lived session signing key to that public key. Only the device with the matching private key can decrypt it and sign the action. + +## What client keys protect + +Client keys help ensure that: + +- The session signing key is delivered only to the device that requested it +- A stolen encrypted session key cannot be used by another device +- A signed withdrawal or account action is tied to an authenticated customer session + +## Signing responsibilities + +Your frontend or mobile app handles device-local key generation, decryption, and signing. Your backend holds Grid API credentials and brokers requests to Grid. + +The client should never receive your Grid client secret. + +## Full signing flow + +Use the reference below to generate client keys, decrypt session material, and authorize `payloadToSign` values. + + diff --git a/mintlify/global-accounts/core-concepts.mdx b/mintlify/global-accounts/core-concepts.mdx new file mode 100644 index 00000000..eb774850 --- /dev/null +++ b/mintlify/global-accounts/core-concepts.mdx @@ -0,0 +1,48 @@ +--- +title: "Core concepts" +description: "Learn the key Global Accounts concepts and API object names." +icon: "/images/icons/file-text.svg" +"og:image": "/images/og/og-global-accounts.webp" +--- + +This page focuses on the concepts specific to Global Accounts. For the full Grid entity model, see [Entities](/platform-overview/core-concepts/entities) and [Account model](/platform-overview/core-concepts/account-model). + +## How Global Accounts fit into Grid + +A platform creates customers. Each eligible customer gets a Global Account. In the API, that account is an internal account with `type: "EMBEDDED_WALLET"`. Funding and withdrawals use quotes, while outbound movement requires customer authorization. + +For a step-by-step implementation path, see [Implementation overview](/global-accounts/implementation-overview). + +## Key objects + +| Object | Meaning | +| --- | --- | +| Global Account | A branded account experience that lets a customer hold a stable dollar balance and move funds through supported Grid rails. | +| Customer | The person or business that owns the Global Account. Customers are represented by `Customer:...` IDs. | +| Internal account | The Grid account object that holds the balance. A Global Account is an internal account with `type: "EMBEDDED_WALLET"`. | +| External account | A bank account, wallet, or other destination outside Grid used for withdrawals and payouts. External accounts use `ExternalAccount:...` IDs. | +| Quote | A priced payment plan that locks amounts, currencies, fees, and payment instructions before execution. Quotes use `Quote:...` IDs. | +| Session | A short-lived signing context issued after the customer verifies a credential. Sessions use `Session:...` IDs. | +| Auth method | A registered customer credential such as passkey, OAuth, or email OTP. Auth methods use `AuthMethod:...` IDs. | + +## API naming + +In these docs, **Global Account** refers to the customer-facing account product. In the API, that account is represented as an internal account with `type: "EMBEDDED_WALLET"`. + +You may also see endpoint groups such as `Embedded Wallet Auth`; those are the authentication and signing APIs used by Global Accounts. The most important mapping is: + +```json +{ + "id": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002", + "customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001", + "type": "EMBEDDED_WALLET" +} +``` + +## Global Accounts vs Payouts + +Choose **Global Accounts** when you are building a customer-owned account experience: issuing an account, showing a balance, funding it, requiring customer authorization, and moving money out of that account. + +Choose **Payouts & B2B** when you are building a payouts-only flow: sending money from platform-managed or customer-managed balances to bank accounts and other payout destinations, without a customer-owned account experience. + +The APIs overlap because both products use customers, accounts, quotes, transactions, and webhooks. The difference is the account model and authorization requirement: outbound movement from a Global Account requires a customer signature with `Grid-Wallet-Signature`. diff --git a/mintlify/global-accounts/customers.mdx b/mintlify/global-accounts/customers.mdx new file mode 100644 index 00000000..ad4df852 --- /dev/null +++ b/mintlify/global-accounts/customers.mdx @@ -0,0 +1,126 @@ +--- +title: "Customers" +description: "Configure customers before issuing and using Global Accounts." +icon: "/images/icons/people.svg" +"og:image": "/images/og/og-global-accounts.webp" +--- + +A Global Account belongs to a Grid customer. Before the account can move funds to or from fiat rails, the customer must satisfy the required KYC or KYB checks for your platform and use case. + +## Customer types + +Global Accounts can be used with individual or business customers, depending on your platform configuration. + +- Use an **individual customer** when the account belongs to a person. +- Use a **business customer** when the account belongs to a company or organization. + +Your compliance model determines whether Grid hosts the KYC/KYB flow or your platform provides verified customer information. + +## Create a customer + +Create a customer with `POST /customers`. Use `platformCustomerId` to connect the Grid customer to the user or business in your system. + +```bash +curl -X POST "$GRID_BASE_URL/customers" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \ + -H "Content-Type: application/json" \ + -d '{ + "customerType": "INDIVIDUAL", + "platformCustomerId": "customer-123", + "region": "US", + "email": "jane@example.com", + "fullName": "Jane Doe", + "birthDate": "1990-01-15", + "nationality": "US", + "address": { + "line1": "123 Main Street", + "city": "San Francisco", + "state": "CA", + "postalCode": "94105", + "country": "US" + } + }' +``` + +The response includes the Grid-assigned `Customer:...` ID. Store both IDs: + +- `Customer:...` for Grid API calls +- `platformCustomerId` for your internal records and hosted KYC links + +## KYC/KYB options + +Global Accounts can only move funds to or from fiat rails after the required compliance checks are complete. + +| Platform model | Typical flow | +| --- | --- | +| Regulated platform | Your platform performs KYC/KYB and creates or updates customers through the API. | +| Non-regulated platform | Grid can host the KYC/KYB flow and notify you when verification state changes. | + +For hosted verification, generate a KYC link for the customer: + +```bash +curl -X GET "$GRID_BASE_URL/customers/kyc-link?platformCustomerId=customer-123&redirectUri=https://yourapp.com/onboarding-complete" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" +``` + +After the customer completes hosted verification, use customer or verification webhooks to update your product state. + +## Account provisioning + +When a customer is created on a platform enabled for Global Accounts, Grid provisions a Global Account as an internal account with `type: "EMBEDDED_WALLET"`. + +Use the customer's `Customer:...` ID to list internal accounts: + +```bash +curl -X GET "$GRID_BASE_URL/customers/internal-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001&type=EMBEDDED_WALLET" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" +``` + +If no `EMBEDDED_WALLET` account appears, confirm that: + +- The platform is enabled for Global Accounts. +- The customer is eligible for the configured Global Accounts currencies. +- The customer has completed the required KYC/KYB checks before attempting fiat movement. + +## Compliance before movement + + + Do not let customers move funds to or from fiat rails until the required KYC or KYB checks are complete. Sandbox can approve customers automatically for testing, but production behavior depends on your platform setup. + + +## Updating and finding customers + +Retrieve a customer by ID: + +```bash +curl -X GET "$GRID_BASE_URL/customers/Customer:019542f5-b3e7-1d02-0000-000000000001" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" +``` + +Or find customers by your own platform ID: + +```bash +curl -X GET "$GRID_BASE_URL/customers?platformCustomerId=customer-123" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" +``` + +If customer information changes, update it before starting payment flows that rely on that information: + +```bash +curl -X PATCH "$GRID_BASE_URL/customers/Customer:019542f5-b3e7-1d02-0000-000000000001" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \ + -H "Content-Type: application/json" \ + -d '{ + "customerType": "INDIVIDUAL", + "fullName": "Jane Doe", + "birthDate": "1990-01-15", + "nationality": "US" + }' +``` + +## Related docs + +- [Internal accounts](/global-accounts/internal-accounts) +- [Account model](/platform-overview/core-concepts/account-model) +- [Implementation overview](/global-accounts/implementation-overview) +- [Webhooks](/global-accounts/webhooks) diff --git a/mintlify/global-accounts/error-handling.mdx b/mintlify/global-accounts/error-handling.mdx new file mode 100644 index 00000000..c73c0447 --- /dev/null +++ b/mintlify/global-accounts/error-handling.mdx @@ -0,0 +1,72 @@ +--- +title: "Error handling" +description: "Handle Global Account API errors, quote failures, and signed-action retries." +icon: "/images/icons/shield.svg" +"og:image": "/images/og/og-global-accounts.webp" +--- + +Global Accounts use the same Grid error model as other payment flows, with additional cases for customer authorization and signed account actions. + +## HTTP status codes + +| Status | Meaning | Common Global Accounts case | +| --- | --- | --- | +| `400` | Bad request | Invalid account ID, amount, destination, or request body. | +| `401` | Unauthorized | Invalid API credentials or missing/invalid `Grid-Wallet-Signature`. | +| `403` | Forbidden | Platform or customer is not enabled for the requested action. | +| `404` | Not found | Customer, account, quote, credential, or session does not exist. | +| `409` | Conflict | Quote already executed, expired, or in an invalid state. | +| `500` | Server error | Unexpected Grid service error. Retry safely where appropriate. | + +## Quote execution errors + +When executing a withdrawal quote from a Global Account, handle: + +- Expired quotes: create a new quote and ask the customer to authorize the new `payloadToSign`. +- Insufficient balance: prompt the customer to fund the account or lower the amount. +- Missing signature: authenticate the customer, sign the payload, and retry execution with `Grid-Wallet-Signature`. +- Invalid signature: discard the signature, fetch or create the current action payload, and sign the exact bytes returned by Grid. + + + Never reuse a signature across different quotes or edited payloads. Sign the exact `payloadToSign` returned for the action you are executing. + + +## Signed-retry errors + +Some sensitive account actions return `202 Accepted` with a `payloadToSign` and `requestId`. Your client builds an API-key stamp over the payload, then your backend retries the same request with: + +```bash +-H "Grid-Wallet-Signature: " \ +-H "Request-Id: " +``` + +If the retry fails: + +| Failure | Recovery | +| --- | --- | +| Missing `Request-Id` | Retry with the `requestId` from the challenge response. | +| Expired challenge | Start the action again and sign the new payload. | +| Signature mismatch | Verify the client stamped the exact `payloadToSign` bytes and sent the complete API-key stamp. | +| Session expired | Reauthenticate the customer and issue a new session signing key. | + +## Webhook processing errors + +Your webhook endpoint should verify signatures, process events idempotently, and return a `2xx` response after successful handling. + +If your handler fails, Grid retries delivery. Use dashboard logs plus transaction queries to identify missed events and reconcile state. + +## Customer-facing recovery + +For user-facing Global Account flows: + +- Show clear retry paths for expired quotes and sessions. +- Ask the customer to reauthenticate when a session is no longer valid. +- Do not show final success until Grid confirms the action or a webhook reaches the expected state. +- Store enough IDs to support customer support and operational debugging. + +## Related docs + +- [Authentication](/global-accounts/authentication) +- [Client keys](/global-accounts/client-keys) +- [Withdrawals](/global-accounts/withdrawals) +- [Reconciliation](/global-accounts/reconciliation) diff --git a/mintlify/global-accounts/exporting-wallet.mdx b/mintlify/global-accounts/exporting-wallet.mdx new file mode 100644 index 00000000..9a665483 --- /dev/null +++ b/mintlify/global-accounts/exporting-wallet.mdx @@ -0,0 +1,31 @@ +--- +title: "Exporting wallet credentials" +description: "Let a customer export the wallet credentials behind their Global Account." +icon: "/images/icons/arrow-path-right.svg" +"og:image": "/images/og/og-global-accounts.webp" +--- + +import ExportingWallet from '/snippets/embedded-wallets/exporting-wallet.mdx'; + +Export lets a customer take the wallet credentials behind their Global Account off Grid when your product supports that flow. + +Export is a sensitive action. It requires customer authentication and a signed-retry request. + +## Export flow + +At a high level: + +1. The customer authenticates with a registered credential. +2. The client generates or selects a key for encrypted delivery. +3. Grid returns a challenge for the export action. +4. The client builds a retry stamp over the export payload. +5. Grid returns encrypted wallet credentials. +6. The client verifies, decrypts, and handles the credentials according to your product's security requirements. + + + Wallet export gives the customer control of credentials that can move funds. Design this flow carefully and avoid exposing secrets to your backend or logs. + + +## Export endpoint flow + + diff --git a/mintlify/global-accounts/external-accounts.mdx b/mintlify/global-accounts/external-accounts.mdx new file mode 100644 index 00000000..97502d58 --- /dev/null +++ b/mintlify/global-accounts/external-accounts.mdx @@ -0,0 +1,97 @@ +--- +title: "External accounts" +description: "Create and manage withdrawal destinations for Global Accounts." +icon: "/images/icons/bank.svg" +"og:image": "/images/og/og-global-accounts.webp" +--- + +External accounts are destinations outside Grid, such as bank accounts or wallets. Global Accounts use external accounts when a customer withdraws funds to a supported rail. + +## Create a destination + +Create external accounts with `POST /customers/external-accounts`. Include the customer who owns the destination and the account information required for the rail. + +```bash +curl -X POST "$GRID_BASE_URL/customers/external-accounts" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \ + -H "Content-Type: application/json" \ + -d '{ + "customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001", + "currency": "USD", + "platformAccountId": "jane-primary-checking", + "accountInfo": { + "accountType": "USD_ACCOUNT", + "accountNumber": "1234567890", + "routingNumber": "021000021", + "beneficiary": { + "beneficiaryType": "INDIVIDUAL", + "fullName": "Jane Doe", + "birthDate": "1990-01-15", + "nationality": "US", + "address": { + "line1": "123 Main Street", + "city": "San Francisco", + "state": "CA", + "postalCode": "94105", + "country": "US" + } + } + } + }' +``` + +Save the returned `ExternalAccount:...` ID. You can use it as the destination account when creating a withdrawal quote. + +## List destinations for a customer + +```bash +curl -X GET "$GRID_BASE_URL/customers/external-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" +``` + +You can also filter by currency when your product lets a customer choose among multiple withdrawal destinations. + +## Supported destination account types + +The required `accountInfo` fields depend on the destination country, currency, and rail. Common examples include: + +| Destination | Typical account type | Common rail | +| --- | --- | --- | +| United States bank account | `USD_ACCOUNT` | ACH, wire, RTP, FedNow | +| Brazil bank account | `BRL_ACCOUNT` | PIX | +| Mexico bank account | `MXN_ACCOUNT` | SPEI / CLABE | +| Europe bank account | `EUR_ACCOUNT` | SEPA / IBAN | +| United Kingdom bank account | `GBP_ACCOUNT` | Faster Payments | +| India bank account | `INR_ACCOUNT` | UPI | +| Spark wallet | `SPARK_WALLET` | Spark | + +The `accountType` value identifies the country or wallet account shape. Rails such as PIX, CLABE, IBAN, and UPI are represented by the account details and rail support for that destination, not by separate `accountType` values. + +Some corridors require bank names or beneficiary fields to match Grid's expected values. Use the Discoveries API when the receiving institution must come from a supported list. + +## Use a destination in a withdrawal quote + +Once the destination exists, create a quote from the Global Account to the external account: + +```json +{ + "source": { + "sourceType": "ACCOUNT", + "accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002" + }, + "destination": { + "destinationType": "ACCOUNT", + "accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123" + }, + "lockedCurrencySide": "SENDING", + "lockedCurrencyAmount": 10000 +} +``` + +Because the source is a Global Account, quote execution requires a customer signature. + +## Related docs + +- [Withdrawals](/global-accounts/withdrawals) +- [Discoveries API](/api-reference/discoveries/list-available-receiving-institution-names) +- [Currencies and rails](/platform-overview/core-concepts/currencies-and-rails) diff --git a/mintlify/global-accounts/funding.mdx b/mintlify/global-accounts/funding.mdx new file mode 100644 index 00000000..f139bbcf --- /dev/null +++ b/mintlify/global-accounts/funding.mdx @@ -0,0 +1,95 @@ +--- +title: "Funding" +description: "Fund a Global Account with supported Grid payment and account flows." +icon: "/images/icons/arrow-inbox.svg" +"og:image": "/images/og/og-global-accounts.webp" +--- + +Funding adds value to a customer's Global Account. Incoming funds behave like other Grid account deposits: the customer does not need to sign an incoming transfer. + +## Funding model + +A Global Account can receive funds through supported Grid flows for your platform, such as direct account funding instructions, just-in-time funding, or quote-based movement from another supported source. + +The account appears as an `InternalAccount:...` with `type: "EMBEDDED_WALLET"`, so you can use it as the destination in quote-based flows. + +## Find the account to fund + +```bash +curl -X GET "$GRID_BASE_URL/customers/internal-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001&type=EMBEDDED_WALLET" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" +``` + +Use the returned `InternalAccount:...` ID as the destination account. + +## Funding with payment instructions + +If the account response includes `fundingPaymentInstructions`, display those instructions in your app so the customer can push funds to the account. + +```json +{ + "fundingPaymentInstructions": [ + { + "instructionsNotes": "Include the reference code in your transfer memo", + "accountOrWalletInfo": { + "accountType": "USD_ACCOUNT", + "reference": "FUND-ABC123", + "accountNumber": "9876543210", + "routingNumber": "021000021", + "accountHolderName": "Lightspark Payments FBO Jane Doe", + "bankName": "JP Morgan Chase" + } + } + ] +} +``` + +Show reference codes, memo instructions, and notes exactly as returned. They help Grid match incoming funds to the right account. + +## Just-in-time funding + +In some flows, Grid returns payment instructions as part of a quote. Once the required funds arrive, Grid initiates the payment described by the quote. + +Use just-in-time funding when your product does not keep a prefunded balance in the Global Account before the payment starts. + +## Sandbox funding + +In sandbox, fund the account directly: + +```bash +curl -X POST "$GRID_BASE_URL/sandbox/internal-accounts/InternalAccount:019542f5-b3e7-1d02-0000-000000000002/fund" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \ + -H "Content-Type: application/json" \ + -d '{ + "amount": 100000 + }' +``` + +Grid returns the updated internal account. If webhooks are configured, Grid sends an `INTERNAL_ACCOUNT.BALANCE_UPDATED` event when the balance changes. + +## Quote-based funding + +To fund from another supported source, create a quote with the Global Account as the destination. The source depends on your enabled rails and platform configuration. + +```json +{ + "destination": { + "destinationType": "ACCOUNT", + "accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002" + } +} +``` + +The available source currencies, funding rails, and settlement behavior depend on your platform configuration. + +## Reconciliation + +Use account and transaction webhooks to update your product state when funds arrive. Do not rely only on client-side state or a successful request response. + +See [Webhooks](/global-accounts/webhooks) for details. + +## Related docs + +- [Internal accounts](/global-accounts/internal-accounts) +- [Quickstart](/global-accounts/quickstart) +- [Reconciliation](/global-accounts/reconciliation) diff --git a/mintlify/global-accounts/implementation-overview.mdx b/mintlify/global-accounts/implementation-overview.mdx new file mode 100644 index 00000000..f50fc65d --- /dev/null +++ b/mintlify/global-accounts/implementation-overview.mdx @@ -0,0 +1,42 @@ +--- +title: "Implementation overview" +description: "Follow the end-to-end Global Accounts implementation flow." +icon: "/images/icons/code.svg" +"og:image": "/images/og/og-global-accounts.webp" +--- + +Global Accounts are implemented with the same Grid objects you use for other payment flows. This page shows the build sequence from customer creation through signed withdrawal and reconciliation. + +## Object model + +| Concept | API object | What it does | +| --- | --- | --- | +| Platform | Platform configuration | Controls supported currencies, rails, environments, and permissions. | +| Customer | `Customer` | Represents the user or business receiving a Global Account. | +| Global Account | `InternalAccount` with `type: "EMBEDDED_WALLET"` | Holds the customer's balance and participates in Grid quotes. | +| Destination account | `ExternalAccount` or supported destination | Represents where funds are withdrawn or sent. | +| Quote | `Quote` | Prices and prepares movement between the account and a destination. | +| Credential | `AuthMethod` | Lets the customer authenticate with passkey, OAuth, or email OTP. | +| Session | `Session` (`AuthSession` response) | Issues a short-lived signing key for signed account actions. | +| Webhook | Account or transaction event | Tells your backend when balances change, funds move, or payments settle. | + +## Core flow + +1. Create or configure the customer. +2. Complete required KYC or KYB before moving funds to or from fiat rails. +3. List the customer's internal accounts and find the Global Account by filtering for `type=EMBEDDED_WALLET`. +4. Register at least one customer credential. +5. Fund the account through a supported Grid funding flow. +6. Create a quote from the Global Account to a destination. +7. Authenticate the customer, sign the quote's `payloadToSign`, and execute the quote with `Grid-Wallet-Signature`. +8. Listen for account and transaction webhooks to reconcile the movement. + + + Incoming funds do not require a customer signature. Outbound movement from the Global Account does. + + +## Where to go next + +- Use [Quickstart](/global-accounts/quickstart) for an end-to-end sandbox flow. +- Use [Funding](/global-accounts/funding) and [Withdrawals](/global-accounts/withdrawals) for the money movement paths. +- Use [Authentication](/global-accounts/authentication) and [Client keys](/global-accounts/client-keys) for signed action details. diff --git a/mintlify/global-accounts/index.mdx b/mintlify/global-accounts/index.mdx new file mode 100644 index 00000000..0dea1ab2 --- /dev/null +++ b/mintlify/global-accounts/index.mdx @@ -0,0 +1,88 @@ +--- +title: "Global Accounts" +sidebarTitle: "Introduction" +description: "Give your customers a branded global dollar account powered by Grid." +icon: "/images/icons/wallet1.svg" +mode: "wide" +"og:image": "/images/og/og-global-accounts.webp" +--- + +import { FeatureCard, FeatureCardGrid } from '/snippets/feature-card.mdx'; + +Grid Global Accounts hero + +Grid Global Accounts let platforms give customers a branded, self-custody dollar account without building banking, wallet, FX, and payout infrastructure from scratch. Your users can hold a stable dollar balance, receive funds, and move money out through supported Grid rails. + +Under the hood, a Global Account uses core Grid APIs: customers, internal accounts, quotes, payment instructions, signed withdrawals, sessions, and webhooks. + +## What you can build today + +Use the current Grid API to: + +- Create and verify customers before funds move to or from fiat rails +- Find the customer's Global Account by listing internal accounts +- Fund the account through supported Grid funding flows +- Create quotes to withdraw or transfer funds out of the account +- Require customer authorization for outbound movement with signed requests +- Register credentials, issue sessions, revoke sessions, and export wallet credentials +- Reconcile account activity with account and transaction webhooks +- Test the flow in sandbox before production launch + +## Core capabilities + +Global Accounts package account, funding, movement, and authorization flows into one embedded account experience. + + + + Give each customer a branded account experience backed by Grid account infrastructure. + + + Hold dollar-denominated value and use Grid quotes to move between account balances and supported rails. + + + Let customers move value to supported local bank rails, including corridors such as PIX, UPI, SEPA, FPS, and more. + + + Require customer approval for outbound account actions. Grid and your platform cannot unilaterally move customer funds. + + + Use Spark, a Lightning-compatible Bitcoin L2, to support Bitcoin and stablecoin flows where enabled for your platform. + + + Track funding, withdrawals, and settlement status with standard Grid account and transaction webhooks. + + + +## Start building + +If you are evaluating the product, start with [Core concepts](/global-accounts/core-concepts) and [Implementation overview](/global-accounts/implementation-overview). + +If you are integrating today, continue to the [Quickstart](/global-accounts/quickstart), then set up [Authentication](/global-accounts/authentication) for customer-authorized actions. + +## Global Accounts or Payouts? + +Use Global Accounts when you want each customer to have a branded account, hold a balance, and authorize outbound movement from their own account. + +Use [Payouts & B2B](/payouts-and-b2b) when you only need to send payments to bank accounts or other destinations and do not need a customer-owned account experience. + +## Additional capabilities + +Some Global Accounts capabilities require platform enablement before you can build with them. + + + + Issue cards tied to Global Account balances where enabled for your platform. + + + Support bounded account access for AI agents with policy-controlled movement. + + + Configure limits, permissions, and controls for more complex account programs. + + + +[Book a demo](https://www.lightspark.com/contact) to see how Global Accounts can support your platform. diff --git a/mintlify/global-accounts/internal-accounts.mdx b/mintlify/global-accounts/internal-accounts.mdx new file mode 100644 index 00000000..4b8e90cb --- /dev/null +++ b/mintlify/global-accounts/internal-accounts.mdx @@ -0,0 +1,77 @@ +--- +title: "Internal accounts" +description: "Find and use the internal account that represents a customer’s Global Account." +icon: "/images/icons/wallet1.svg" +"og:image": "/images/og/og-global-accounts.webp" +--- + +A Global Account appears in the API as a customer internal account with `type: "EMBEDDED_WALLET"`. + +You use this `InternalAccount:...` ID when you fund the account, show its balance, create withdrawal quotes, register customer credentials, manage sessions, or export wallet credentials. + +## Find a customer's Global Account + +List the customer's internal accounts and filter to `type=EMBEDDED_WALLET`: + +```bash +curl -X GET "$GRID_BASE_URL/customers/internal-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001&type=EMBEDDED_WALLET" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" +``` + +```json +{ + "data": [ + { + "id": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002", + "customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001", + "type": "EMBEDDED_WALLET", + "balance": { + "amount": 100000, + "currency": { + "code": "USDB", + "name": "USDB", + "symbol": "$", + "decimals": 2 + } + }, + "fundingPaymentInstructions": [], + "createdAt": "2026-04-19T12:00:00Z", + "updatedAt": "2026-04-19T12:00:00Z" + } + ], + "hasMore": false, + "totalCount": 1 +} +``` + +Save the `InternalAccount:...` ID. It is the account identifier your backend uses for Global Account operations. + +## How Global Accounts differ from other internal accounts + +| Internal account type | Common owner | Outbound authorization | +| --- | --- | --- | +| `EMBEDDED_WALLET` | Customer | Customer signs outbound actions with a session signing key. | +| `INTERNAL_FIAT` | Platform or customer, depending on configuration | Standard API authorization. | +| `INTERNAL_CRYPTO` | Platform or customer, depending on configuration | Standard API authorization. | + +## Funding instructions + +Some internal accounts include `fundingPaymentInstructions` that describe how to push funds into the account. The available instructions depend on the currency and your platform configuration. + +Use these instructions only as returned by the API. They can include bank details, payment URLs, references, or wallet information. + + + If you present funding instructions to customers, show reference codes and notes exactly as returned so deposits can be matched to the right account. + + +## Balance updates + +Global Account balances change when funding arrives, withdrawals execute, refunds post, or other account movements settle. + +Use `INTERNAL_ACCOUNT.BALANCE_UPDATED` webhooks and transaction queries to reconcile balances on your backend. Do not rely only on client-side state. + +## Related docs + +- [Funding](/global-accounts/funding) +- [Withdrawals](/global-accounts/withdrawals) +- [Authentication](/global-accounts/authentication) diff --git a/mintlify/global-accounts/list-transactions.mdx b/mintlify/global-accounts/list-transactions.mdx new file mode 100644 index 00000000..dabec0e3 --- /dev/null +++ b/mintlify/global-accounts/list-transactions.mdx @@ -0,0 +1,93 @@ +--- +title: "List transactions" +description: "Query Global Account funding, withdrawal, and settlement history." +icon: "/images/icons/file-text.svg" +"og:image": "/images/og/og-global-accounts.webp" +--- + +Use the Transactions API to reconcile Global Account activity, build account history, and investigate payment state. + +Transactions are returned in descending order by default and can be filtered by customer, account, status, type, and date range. + +## List recent transactions + +```bash +curl -X GET "$GRID_BASE_URL/transactions?limit=20" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" +``` + +## Filter by customer + +Use the Grid customer ID or your platform customer ID to show a customer's Global Account activity. + +```bash +curl -X GET "$GRID_BASE_URL/transactions?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" +``` + +```bash +curl -X GET "$GRID_BASE_URL/transactions?platformCustomerId=customer-123" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" +``` + +## Filter by Global Account + +Use account filters when you need the ledger for a specific Global Account. + +```bash +# Any transaction involving the Global Account +curl -X GET "$GRID_BASE_URL/transactions?accountIdentifier=InternalAccount:019542f5-b3e7-1d02-0000-000000000002" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" + +# Only transactions sent from the Global Account +curl -X GET "$GRID_BASE_URL/transactions?senderAccountIdentifier=InternalAccount:019542f5-b3e7-1d02-0000-000000000002" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" + +# Only transactions received by the Global Account +curl -X GET "$GRID_BASE_URL/transactions?receiverAccountIdentifier=InternalAccount:019542f5-b3e7-1d02-0000-000000000002" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" +``` + +## Filter by status and type + +```bash +curl -X GET "$GRID_BASE_URL/transactions?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001&type=OUTGOING&status=COMPLETED" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" +``` + +Common transaction statuses include: + +| Status | Meaning | +| --- | --- | +| `CREATED` | Initial transaction record or lookup has been created. | +| `PENDING` | Transaction created and waiting for processing. | +| `PROCESSING` | Transaction is moving through the payment rail. | +| `COMPLETED` | Transaction reached a successful terminal state. | +| `REJECTED` | Receiving institution, wallet, or approval flow rejected the payment. | +| `FAILED` | Transaction failed. Inspect `failureReason` when present. | +| `EXPIRED` | Quote expired before execution. | +| `REFUNDED` | Funds were returned after a failed or reversed movement. | + +## Paginate results + +Follow `nextCursor` while `hasMore` is `true`. + +```bash +curl -X GET "$GRID_BASE_URL/transactions?accountIdentifier=InternalAccount:019542f5-b3e7-1d02-0000-000000000002&limit=100&cursor=eyJpZCI6IlRyYW5z..." \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" +``` + +## Recommended pattern + +Use webhooks for real-time product updates, then use transaction queries as a backstop: + +1. Store `Customer:...`, `InternalAccount:...`, `Quote:...`, and `Transaction:...` IDs from API responses. +2. Process webhooks idempotently. +3. Query transactions by customer, account, and date range during daily reconciliation. +4. Follow pagination until `hasMore` is `false`. + +## Related docs + +- [Webhooks](/global-accounts/webhooks) +- [Reconciliation](/global-accounts/reconciliation) +- [Transaction lifecycle](/platform-overview/core-concepts/transaction-lifecycle) diff --git a/mintlify/global-accounts/managing-sessions.mdx b/mintlify/global-accounts/managing-sessions.mdx new file mode 100644 index 00000000..a8d34a76 --- /dev/null +++ b/mintlify/global-accounts/managing-sessions.mdx @@ -0,0 +1,29 @@ +--- +title: "Sessions" +description: "List and revoke customer sessions for Global Account actions." +icon: "/images/icons/arrows-repeat-circle.svg" +"og:image": "/images/og/og-global-accounts.webp" +--- + +import Sessions from '/snippets/embedded-wallets/managing-sessions.mdx'; + +A session represents a short-lived authorization window for signed Global Account actions. + +Sessions are created when a customer verifies a registered credential. The session signing key is encrypted to the client device and used to sign action payloads, such as quote execution from a Global Account. + +## When to manage sessions + +List or revoke sessions when you need to: + +- Show active devices or signed-in sessions to a customer +- Sign a customer out of a device +- Revoke access after account recovery +- Reduce risk after suspicious activity + +## Revocation requires a signed action + +Session revocation can require the same signed-retry pattern as other sensitive account actions. The customer authenticates, authorizes the revocation payload, and your backend retries the request with `Grid-Wallet-Signature`. + +## Session endpoints + + diff --git a/mintlify/global-accounts/platform-configuration.mdx b/mintlify/global-accounts/platform-configuration.mdx new file mode 100644 index 00000000..0ff8e1b5 --- /dev/null +++ b/mintlify/global-accounts/platform-configuration.mdx @@ -0,0 +1,65 @@ +--- +title: "Platform configuration" +description: "Configure currencies, credentials, and webhooks before issuing Global Accounts." +icon: "/images/icons/settings-gear2.svg" +"og:image": "/images/og/og-global-accounts.webp" +--- + +Before you issue Global Accounts, your platform must be configured for the currencies, environments, credentials, and webhooks your integration uses. + +## Supported currencies + +Global Accounts are provisioned from your platform configuration. When your platform is enabled for Global Accounts, Grid provisions an internal account with `type: "EMBEDDED_WALLET"` for each eligible customer. + +In sandbox, Global Accounts are available for testing on enabled platforms. In production, supported currencies and funding rails depend on your platform setup. + + + If you expect a customer to receive a Global Account but do not see an `EMBEDDED_WALLET` account when listing internal accounts, confirm that the platform and customer are enabled for Global Accounts. + + +## API credentials + +Create API credentials in the Grid dashboard. Credentials are scoped to an environment and cannot be used across Sandbox and Production. + +Use HTTP Basic Auth for server-to-server calls: + +```bash +export GRID_BASE_URL="https://api.lightspark.com/grid/2025-10-13" +export GRID_CLIENT_ID="YOUR_CLIENT_ID" +export GRID_CLIENT_SECRET="YOUR_CLIENT_SECRET" + +curl -X GET "$GRID_BASE_URL/config" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" +``` + +Never expose your Grid client secret to browsers or mobile apps. Your client talks to your backend, and your backend calls Grid. + +## Required API areas + +Most Global Accounts integrations use: + +| Area | Why it matters | +| --- | --- | +| Customers | Create and verify the people or businesses that own accounts. | +| Internal Accounts | Find the customer’s Global Account and read account state. | +| External Accounts | Store withdrawal destinations such as bank accounts or wallets. | +| Quotes | Fund accounts, withdraw funds, and lock exchange rates where applicable. | +| Embedded Wallet Auth | Register credentials, issue sessions, and sign account actions. | +| Transactions | Reconcile funding, withdrawals, refunds, and settlement status. | +| Webhooks | Keep your product state in sync with account and payment events. | + +## Webhooks + +Configure a public HTTPS webhook endpoint in the Grid dashboard and verify every webhook using the `X-Grid-Signature` header. + +For Global Accounts, listen for: + +- `INTERNAL_ACCOUNT.BALANCE_UPDATED` when a Global Account balance changes +- `OUTGOING_PAYMENT.*` events for withdrawals and payment settlement +- Customer and verification events if your onboarding flow depends on Grid-hosted KYC/KYB status + +## Environments + +Use Sandbox to test account creation, sandbox funding, signed quote execution, webhook handling, and transaction reconciliation without moving real funds. + +Move to Production only after your platform configuration, credential handling, webhook verification, and customer-facing signing flows are ready. diff --git a/mintlify/global-accounts/postman-collection.mdx b/mintlify/global-accounts/postman-collection.mdx new file mode 100644 index 00000000..6843fc30 --- /dev/null +++ b/mintlify/global-accounts/postman-collection.mdx @@ -0,0 +1,29 @@ +--- +title: "Postman collection" +description: "Use the Grid Postman collection to test Global Accounts API calls." +icon: "/images/icons/IconPostman.svg" +"og:image": "/images/og/og-global-accounts.webp" +--- + +import PostmanCollection from '/snippets/postman-collection.mdx'; + +Use the Grid Postman collection to test the API calls behind Global Accounts. + +The same collection covers customers, internal accounts, external accounts, quotes, auth credentials, sessions, and webhooks. + + + +## Recommended setup + +1. Open the Grid Postman collection above. +2. Set your sandbox `GRID_CLIENT_ID` and `GRID_CLIENT_SECRET`. +3. Set `GRID_BASE_URL` to the current Grid API base URL. +4. Run the requests from the [Quickstart](/global-accounts/quickstart) in order. +5. Save returned IDs, such as `Customer:...`, `InternalAccount:...`, `ExternalAccount:...`, and `Quote:...`, as collection variables. + +## Related docs + +- [API environments](/api-reference/environments) +- [API authentication](/api-reference/authentication) +- [Quickstart](/global-accounts/quickstart) +- [Sandbox testing](/global-accounts/sandbox-testing) diff --git a/mintlify/global-accounts/quickstart.mdx b/mintlify/global-accounts/quickstart.mdx new file mode 100644 index 00000000..deaef231 --- /dev/null +++ b/mintlify/global-accounts/quickstart.mdx @@ -0,0 +1,215 @@ +--- +title: "Quickstart" +description: "Create a customer, find their Global Account, fund it, and execute a signed withdrawal." +icon: "/images/icons/rocket.svg" +"og:image": "/images/og/og-global-accounts.webp" +--- + +This quickstart walks through the core Global Accounts flow in sandbox. + +You will create a customer, find the customer's Global Account, fund it, create a withdrawal quote, sign the quote, and execute it. + +## Prerequisites + +You need: + +- Sandbox API credentials +- A platform enabled for Global Accounts +- Access to `Internal Accounts`, `Quotes`, and `Embedded Wallet Auth` + +```bash +export GRID_BASE_URL="https://api.lightspark.com/grid/2025-10-13" +export GRID_CLIENT_ID="YOUR_SANDBOX_CLIENT_ID" +export GRID_CLIENT_SECRET="YOUR_SANDBOX_CLIENT_SECRET" +``` + + + In sandbox, customers are automatically approved for testing. Production customers must complete the required KYC or KYB checks before funds can move to or from fiat rails. + + +## 1. Create a customer + +Create the customer who will receive a Global Account. + +```bash +curl -X POST "$GRID_BASE_URL/customers" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \ + -H "Content-Type: application/json" \ + -d '{ + "customerType": "INDIVIDUAL", + "platformCustomerId": "customer-123", + "region": "US", + "email": "jane@example.com", + "fullName": "Jane Doe", + "birthDate": "1990-01-15", + "nationality": "US" + }' +``` + +Save the returned `Customer:...` ID for later commands: + +```bash +export CUSTOMER_ID="Customer:019542f5-b3e7-1d02-0000-000000000001" +``` + +## 2. Find the Global Account + +Global Accounts appear in the API as internal accounts with `type=EMBEDDED_WALLET`. + +```bash +curl -X GET "$GRID_BASE_URL/customers/internal-accounts?customerId=$CUSTOMER_ID&type=EMBEDDED_WALLET" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" +``` + +Save the returned `InternalAccount:...` ID. You will use this account as the source or destination in quote requests. + +```bash +export GLOBAL_ACCOUNT_ID="InternalAccount:019542f5-b3e7-1d02-0000-000000000002" +``` + +## 3. Register a credential + +Outbound movement from a Global Account requires customer authorization. For this sandbox quickstart, register an email OTP credential against the Global Account. + +```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": "'"$GLOBAL_ACCOUNT_ID"'" + }' +``` + +Save the returned `AuthMethod:...` ID, then verify it with the sandbox OTP code. The `clientPublicKey` is required by the API; in sandbox, the returned `encryptedSessionSigningKey` is a stub because you will use `sandbox-valid-signature` later. + +```bash +export AUTH_METHOD_ID="AuthMethod:019542f5-b3e7-1d02-0000-000000000001" +``` + +```bash +curl -X POST "$GRID_BASE_URL/auth/credentials/$AUTH_METHOD_ID/verify" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \ + -H "Content-Type: application/json" \ + -d '{ + "type": "EMAIL_OTP", + "otp": "000000", + "clientPublicKey": "04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2" + }' +``` + +For production passkey, OAuth, email OTP, and signing flows, see [Authentication](/global-accounts/authentication) and [Client keys](/global-accounts/client-keys). + +## 4. Fund the account + +In sandbox, use the sandbox funding endpoint to add test funds to the account. + +```bash +curl -X POST "$GRID_BASE_URL/sandbox/internal-accounts/$GLOBAL_ACCOUNT_ID/fund" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \ + -H "Content-Type: application/json" \ + -d '{ + "amount": 100000 + }' +``` + +Grid returns the updated internal account. If webhooks are configured, Grid sends an `INTERNAL_ACCOUNT.BALANCE_UPDATED` event when the balance changes. + +## 5. Create a withdrawal destination + +Create or reuse an external account for the destination. + +```bash +curl -X POST "$GRID_BASE_URL/customers/external-accounts" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \ + -H "Content-Type: application/json" \ + -d '{ + "customerId": "'"$CUSTOMER_ID"'", + "currency": "USD", + "platformAccountId": "jane-checking", + "accountInfo": { + "accountType": "USD_ACCOUNT", + "accountNumber": "1234567890", + "routingNumber": "021000021", + "beneficiary": { + "beneficiaryType": "INDIVIDUAL", + "fullName": "Jane Doe", + "birthDate": "1990-01-15", + "nationality": "US", + "address": { + "line1": "123 Main Street", + "city": "San Francisco", + "state": "CA", + "postalCode": "94105", + "country": "US" + } + } + } + }' +``` + +Save the returned `ExternalAccount:...` ID. + +```bash +export EXTERNAL_ACCOUNT_ID="ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123" +``` + +## 6. Create a withdrawal quote + +Create a quote with the Global Account as the source and the external account as the destination. + +```bash +curl -X POST "$GRID_BASE_URL/quotes" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \ + -H "Content-Type: application/json" \ + -d '{ + "source": { + "sourceType": "ACCOUNT", + "accountId": "'"$GLOBAL_ACCOUNT_ID"'" + }, + "destination": { + "destinationType": "ACCOUNT", + "accountId": "'"$EXTERNAL_ACCOUNT_ID"'" + }, + "lockedCurrencySide": "SENDING", + "lockedCurrencyAmount": 10000, + "description": "Withdrawal to checking" + }' +``` + +Because the quote source is a Global Account, Grid returns a `payloadToSign` in `paymentInstructions[].accountOrWalletInfo`. + +Save the returned `Quote:...` ID. + +```bash +export QUOTE_ID="Quote:019542f5-b3e7-1d02-0000-000000000006" +``` + +## 7. Sign and execute + +Authenticate the customer, sign the `payloadToSign`, and execute the quote with the `Grid-Wallet-Signature` header. + +```bash +curl -X POST "$GRID_BASE_URL/quotes/$QUOTE_ID/execute" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \ + -H "Idempotency-Key: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21" \ + -H "Grid-Wallet-Signature: sandbox-valid-signature" +``` + + + Sandbox accepts `sandbox-valid-signature` so you can test the flow without implementing client-side signing first. Production requires a real signature from a verified session signing key. + + +## 8. Reconcile with webhooks + +Listen for account and transaction webhooks to track balance changes, settlement, and refunds. See [Webhooks](/global-accounts/webhooks) for the Global Accounts webhook model. + +## Production checklist + +Before going live: + +- Complete the required KYC/KYB flow before moving funds to or from fiat rails. +- Replace `sandbox-valid-signature` with a real `Grid-Wallet-Signature` generated from the returned `payloadToSign`. +- Verify webhook signatures with `X-Grid-Signature`. +- Use idempotency keys for quote execution retries. +- Store `Customer:...`, `InternalAccount:...`, `ExternalAccount:...`, `Quote:...`, and `Transaction:...` IDs for reconciliation and support. diff --git a/mintlify/global-accounts/reconciliation.mdx b/mintlify/global-accounts/reconciliation.mdx new file mode 100644 index 00000000..595c8b88 --- /dev/null +++ b/mintlify/global-accounts/reconciliation.mdx @@ -0,0 +1,71 @@ +--- +title: "Reconciliation" +description: "Reconcile Global Account balances, withdrawals, and transaction state." +icon: "/images/icons/checkmark1.svg" +"og:image": "/images/og/og-global-accounts.webp" +--- + +Reconciliation keeps your product ledger aligned with Grid. For Global Accounts, use both real-time webhooks and periodic API reads. + + + Use webhooks for immediate customer-visible updates and scheduled transaction queries as a backstop for missed or delayed events. + + +## What to reconcile + +Track these IDs together in your system: + +| ID | Why it matters | +| --- | --- | +| `Customer:...` | Owner of the Global Account. | +| `InternalAccount:...` | The Global Account balance source of truth in Grid. | +| `ExternalAccount:...` | Withdrawal destination. | +| `Quote:...` | The priced funding or withdrawal plan. | +| `Transaction:...` | The settled or in-progress movement created by execution. | +| Webhook ID | Idempotency key for webhook processing. | + +## Webhook-first updates + +Listen for account and transaction events: + +- `INTERNAL_ACCOUNT.BALANCE_UPDATED` when the Global Account balance changes +- `OUTGOING_PAYMENT.*` for withdrawal lifecycle events +- `INCOMING_PAYMENT.*` when your Global Account flow receives payments through UMA-address-based incoming payment flows +- Customer and verification events if customer approval gates account use + +Process webhook deliveries idempotently. Store the event ID, event type, related resource ID, and the raw payload for auditability. + +## Query backstop + +Periodically query transactions for the customer or account and compare the results with your ledger. + +```bash +curl -X GET "$GRID_BASE_URL/transactions?accountIdentifier=InternalAccount:019542f5-b3e7-1d02-0000-000000000002&startDate=2026-04-01T00:00:00Z&endDate=2026-04-01T23:59:59Z&limit=100" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" +``` + +Always follow pagination until `hasMore` is `false`. + +## Balance handling + +Treat Grid as the source of truth for the Global Account balance. + +- Update customer-visible balances from `INTERNAL_ACCOUNT.BALANCE_UPDATED` events or fresh internal account reads. +- Use transaction status to explain why a balance changed. +- Account for refunds or reversals when an outgoing payment fails after funds have been debited. + +## Failure recovery + +If state does not match: + +1. Re-query the Global Account with `GET /customers/internal-accounts`. +2. Re-query transactions for the affected customer or account. +3. Check webhook delivery logs in the dashboard. +4. Reprocess missed webhook payloads idempotently. +5. Escalate with the relevant `Customer:...`, `InternalAccount:...`, `Quote:...`, and `Transaction:...` IDs. + +## Related docs + +- [List transactions](/global-accounts/list-transactions) +- [Webhooks](/global-accounts/webhooks) +- [Error handling](/global-accounts/error-handling) diff --git a/mintlify/global-accounts/sandbox-testing.mdx b/mintlify/global-accounts/sandbox-testing.mdx new file mode 100644 index 00000000..fd447966 --- /dev/null +++ b/mintlify/global-accounts/sandbox-testing.mdx @@ -0,0 +1,56 @@ +--- +title: "Sandbox testing" +description: "Test Global Accounts flows in sandbox without moving real funds." +icon: "/images/icons/sandbox.svg" +"og:image": "/images/og/og-global-accounts.webp" +--- + +import SandboxGlobalAccountMagic from '/snippets/sandbox-global-account-magic.mdx'; + +Sandbox lets you test the Global Accounts integration before moving real funds. + +## Sandbox behavior + +In sandbox: + +- Customers can be automatically approved for test flows +- You can fund internal accounts with sandbox endpoints +- Email OTP verification uses `000000` +- Signed account actions can use `sandbox-valid-signature` +- Webhooks mirror the production event model + +## Magic values + + + +## Sandbox funding + +Use the sandbox funding endpoint to add test funds to a Global Account: + +```bash +curl -X POST "$GRID_BASE_URL/sandbox/internal-accounts/InternalAccount:019542f5-b3e7-1d02-0000-000000000002/fund" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \ + -H "Content-Type: application/json" \ + -d '{ + "amount": 100000 + }' +``` + +The response is the updated internal account. Use `INTERNAL_ACCOUNT.BALANCE_UPDATED` webhooks to test balance reconciliation. + +## Suggested test path + +1. Create a sandbox customer. +2. Find the customer's Global Account by filtering internal accounts with `type=EMBEDDED_WALLET`. +3. Fund the account with the sandbox funding endpoint. +4. Create a test external account. +5. Create a withdrawal quote. +6. Execute with `Grid-Wallet-Signature: sandbox-valid-signature`. +7. Confirm webhooks arrive and update your internal state. + +## Related docs + +- [Quickstart](/global-accounts/quickstart) +- [Funding](/global-accounts/funding) +- [Authentication](/global-accounts/authentication) +- [Sandbox testing](/api-reference/sandbox-testing) diff --git a/mintlify/global-accounts/webhooks.mdx b/mintlify/global-accounts/webhooks.mdx new file mode 100644 index 00000000..b0adcf80 --- /dev/null +++ b/mintlify/global-accounts/webhooks.mdx @@ -0,0 +1,73 @@ +--- +title: "Webhooks" +description: "Use webhooks to reconcile Global Account funding, withdrawals, and account activity." +icon: "/images/icons/bell.svg" +"og:image": "/images/og/og-global-accounts.webp" +--- + +Use webhooks to keep your product state in sync with Global Account activity. + +Global Accounts use Grid account and transaction events. Your backend should listen for balance updates when funds arrive or leave the account, and transaction events when withdrawals execute, settle, fail, or refund. + +## What to track + +For Global Accounts, webhooks are especially important for: + +- `INTERNAL_ACCOUNT.BALANCE_UPDATED` events for customer-facing balance changes +- `OUTGOING_PAYMENT.*` events for withdrawals and payment settlement +- `INCOMING_PAYMENT.*` events when your Global Account flow receives payments through UMA-address-based incoming payment flows +- Withdrawal settlement, failures, refunds, and reversals +- Transaction state that results from executed quotes +- Customer or verification state changes when KYC/KYB gates account use + +## Recommended pattern + +1. Store the customer, account, quote, and transaction IDs from API responses. +2. Verify the `X-Grid-Signature` header before processing. +3. Process account and transaction webhooks idempotently. +4. Update customer-visible state only after confirmed webhook events when settlement status matters. +5. Reconcile webhook state against API reads for operational workflows. + +## Balance updates + +When a Global Account balance changes, Grid sends an internal account status webhook: + +```json +{ + "id": "Webhook:019542f5-b3e7-1d02-0000-000000000007", + "type": "INTERNAL_ACCOUNT.BALANCE_UPDATED", + "timestamp": "2026-04-19T12:30:00Z", + "data": { + "id": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002", + "customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001", + "type": "EMBEDDED_WALLET", + "balance": { + "amount": 100000, + "currency": { + "code": "USDB", + "decimals": 2 + } + } + } +} +``` + +Use this event to refresh customer-visible balances. + +## Withdrawal events + +Withdrawals from a Global Account create outgoing payment activity after quote execution. Track the resulting transaction through terminal states such as `COMPLETED`, `FAILED`, or `EXPIRED`, and handle refund events when applicable. + +Grid does not send quote-specific webhook events. If your webhook handler misses an event, use [List transactions](/global-accounts/list-transactions) as a backstop. + +## Incoming payment events + +If your platform receives funds through UMA-address-based incoming payment flows, Grid can send `INCOMING_PAYMENT.PENDING`, `INCOMING_PAYMENT.COMPLETED`, and `INCOMING_PAYMENT.FAILED` webhooks. Pending incoming payments can require your platform to approve or reject the payment synchronously in the webhook response, or asynchronously with `POST /transactions/{transactionId}/approve` or `POST /transactions/{transactionId}/reject`. + +Not every Global Account funding path uses incoming payment webhooks. For direct account balance updates, use `INTERNAL_ACCOUNT.BALANCE_UPDATED` as the account balance signal. + +## Related docs + +- [Transaction lifecycle](/platform-overview/core-concepts/transaction-lifecycle) +- [API reference webhooks](/api-reference/webhooks) +- [Reconciliation](/global-accounts/reconciliation) diff --git a/mintlify/global-accounts/withdrawals.mdx b/mintlify/global-accounts/withdrawals.mdx new file mode 100644 index 00000000..96fe7d54 --- /dev/null +++ b/mintlify/global-accounts/withdrawals.mdx @@ -0,0 +1,85 @@ +--- +title: "Withdrawals" +description: "Move funds out of a Global Account with quotes and customer signatures." +icon: "/images/icons/paper-plane-top-right.svg" +"og:image": "/images/og/og-global-accounts.webp" +--- + +Withdrawals move funds out of a Global Account to a supported destination, such as a bank account or another supported Grid destination. + +Unlike incoming funds, outbound movement requires customer authorization. + + + Global Account withdrawals use the quotes flow. Create a quote, have the customer authorize the returned `payloadToSign`, then execute the quote with `Grid-Wallet-Signature`. + + +## Withdrawal flow + +1. Create or select a destination account. +2. Create a quote with the Global Account as the source. +3. Read the `payloadToSign` returned in the quote's payment instructions. +4. Authenticate the customer and create a session. +5. Sign the payload with the session signing key. +6. Execute the quote with the `Grid-Wallet-Signature` header. +7. Reconcile status with account and transaction webhooks. + +## Create a quote + +```bash +curl -X POST "$GRID_BASE_URL/quotes" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \ + -H "Content-Type: application/json" \ + -d '{ + "source": { + "sourceType": "ACCOUNT", + "accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002" + }, + "destination": { + "destinationType": "ACCOUNT", + "accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123" + }, + "lockedCurrencySide": "SENDING", + "lockedCurrencyAmount": 10000, + "description": "Withdrawal to checking" + }' +``` + +The quote response includes the payment instructions for the withdrawal. When the source account is `EMBEDDED_WALLET`, the response includes a `payloadToSign` value under `paymentInstructions[].accountOrWalletInfo`. + +## Execute with a signature + +The signature covers the exact `paymentInstructions[].accountOrWalletInfo.payloadToSign` value returned by the quote. + +```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: sandbox-valid-signature" +``` + + + In production, sign the exact `payloadToSign` bytes returned by Grid. Do not parse, reserialize, trim, or normalize the payload before signing. + + +## Same-currency and cross-currency withdrawals + +If the source and destination use the same currency, the quote can represent a same-currency withdrawal. If the destination uses a different currency, the quote locks the conversion details before execution. + +In both cases, the customer signature requirement comes from the source account type, not from the currency pair. If the source is an `EMBEDDED_WALLET`, execute the quote with `Grid-Wallet-Signature`. + +## Reconciliation + +After execution, track the withdrawal through transaction and account webhooks: + +- `OUTGOING_PAYMENT.*` events track the payment lifecycle. +- `INTERNAL_ACCOUNT.BALANCE_UPDATED` events track balance changes on the Global Account. +- Transaction reads provide a backstop for webhook delivery gaps. + +## Related docs + +- [Authentication](/global-accounts/authentication) +- [Client keys](/global-accounts/client-keys) +- [External accounts](/global-accounts/external-accounts) +- [Reconciliation](/global-accounts/reconciliation) +- [Transaction lifecycle](/platform-overview/core-concepts/transaction-lifecycle) diff --git a/mintlify/images/heroes/hero-gga.png b/mintlify/images/heroes/hero-gga.png new file mode 100644 index 00000000..aed04a2e Binary files /dev/null and b/mintlify/images/heroes/hero-gga.png differ diff --git a/mintlify/images/heroes/hero-gga.webp b/mintlify/images/heroes/hero-gga.webp new file mode 100644 index 00000000..57d034e8 Binary files /dev/null and b/mintlify/images/heroes/hero-gga.webp differ diff --git a/mintlify/images/og/og-global-accounts.png b/mintlify/images/og/og-global-accounts.png new file mode 100644 index 00000000..ee5880fd Binary files /dev/null and b/mintlify/images/og/og-global-accounts.png differ diff --git a/mintlify/images/og/og-global-accounts.webp b/mintlify/images/og/og-global-accounts.webp new file mode 100644 index 00000000..4eef2633 Binary files /dev/null and b/mintlify/images/og/og-global-accounts.webp differ diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index b524fd03..9e4313fd 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -3635,9 +3635,9 @@ paths: Export is a two-step signed-retry flow (same pattern as add-additional credential, revoke credential, and revoke session): - 1. Call `POST /internal-accounts/{id}/export` with the request body `{ "clientPublicKey": "..." }` and no signature headers. Grid binds the `clientPublicKey` into the `payloadToSign` it returns, so the subsequent `Grid-Wallet-Signature` commits to the target encryption key. The response is `202` with `payloadToSign`, `requestId`, and `expiresAt`. + 1. Call `POST /internal-accounts/{id}/export` with the request body `{ "clientPublicKey": "..." }` and no signature headers. Grid binds the `clientPublicKey` into the `payloadToSign` it returns, so the subsequent stamp in `Grid-Wallet-Signature` commits to the target encryption key. The response is `202` with `payloadToSign`, `requestId`, and `expiresAt`. - 2. Sign the `payloadToSign` with the session private key of a verified authentication credential on the same internal account and retry with the signature as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The retry body must carry the **same** `clientPublicKey` submitted in step 1 — Grid rejects the retry with `401` if it disagrees with what was bound into `payloadToSign`. The signed retry returns `200` with `encryptedWalletCredentials`, which the client decrypts with the matching private key. + 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** `clientPublicKey` submitted in step 1 — Grid rejects the retry with `401` if it disagrees with what was bound into `payloadToSign`. The signed retry returns `200` with `encryptedWalletCredentials`, which the client decrypts with the matching private key. The `clientPublicKey` is ephemeral: generate a fresh P-256 keypair for this export and discard the private key after decrypting. Do not reuse the keypair from any prior verify call — that private key was already discarded after decrypting the session signing key it was issued against. operationId: exportInternalAccount @@ -3655,10 +3655,10 @@ paths: - name: Grid-Wallet-Signature in: header required: false - description: Signature over the `payloadToSign` returned in a prior `202` response, produced with the session private key of a verified authentication credential on the target internal account and base64-encoded. Required on the signed retry; ignored on the initial call. + 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: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE= + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzaWduYXR1cmUiOiIzMDQ1MDIyMTAwLi4uIiwic2NoZW1lIjoiUDI1Nl9FQ0RTQV9TSEEyNTYifQ - name: Request-Id in: header required: false @@ -3685,7 +3685,7 @@ paths: schema: $ref: '#/components/schemas/InternalAccountExportResponse' '202': - description: Challenge issued. The response contains a `payloadToSign` (which binds the submitted `clientPublicKey`) that must be signed with the session private key of a verified authentication credential on the target internal account, along with a `requestId` that must be echoed back on the retry. + description: Challenge issued. The response contains `payloadToSign` (which binds the submitted `clientPublicKey`) 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: @@ -3726,7 +3726,7 @@ paths: **Adding an additional credential** - Registering an additional credential against an internal account that already has one requires a signature from an existing verified credential. Call this endpoint with the new credential's details; if an existing credential is already registered on the internal account the response is `202` with a `payloadToSign` and a `requestId`. Sign the payload with the session private key of an existing verified credential on the same internal account (decrypted client-side from its `encryptedSessionSigningKey`) and retry the same request with the signature supplied as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `201` with the created `AuthMethod`. For `EMAIL_OTP`, the OTP email is triggered on the signed retry, and the credential must then be activated via `POST /auth/credentials/{id}/verify`. + Registering an additional credential against an internal account that already has one requires a signature from an existing verified credential. Call this endpoint with the new credential's details; if an existing credential is already registered on the internal account the response is `202` with a `payloadToSign` and a `requestId`. Use the session API keypair of an existing verified credential on the same internal account (decrypted client-side from its `encryptedSessionSigningKey`) to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `201` with the created `AuthMethod`. For `EMAIL_OTP`, the OTP email is triggered on the signed retry, and the credential must then be activated via `POST /auth/credentials/{id}/verify`. operationId: createAuthCredential tags: - Embedded Wallet Auth @@ -3736,10 +3736,10 @@ paths: - name: Grid-Wallet-Signature in: header required: false - description: Signature over the `payloadToSign` returned in a prior `202` response, produced with the session private key of an existing verified authentication credential on the target internal account and base64-encoded. Required when registering an additional credential on an internal account that already has one; ignored when the internal account has no existing credentials. + description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of an existing verified authentication credential on the target internal account. Required when registering an additional credential on an internal account that already has one; ignored when the internal account has no existing credentials. schema: type: string - example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE= + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzaWduYXR1cmUiOiIzMDQ1MDIyMTAwLi4uIiwic2NoZW1lIjoiUDI1Nl9FQ0RTQV9TSEEyNTYifQ - name: Request-Id in: header required: false @@ -3818,7 +3818,7 @@ paths: requestId: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 expiresAt: '2026-04-08T15:35:00Z' '202': - description: An existing authentication credential is already registered on the internal account. The response contains a `payloadToSign` that must be signed with the session private key of an existing verified credential on the same internal account, along with a `requestId` that must be echoed back on the retry. The signature is passed as the `Grid-Wallet-Signature` header and the `requestId` as the `Request-Id` header on a retry of this request to complete registration. + description: An existing authentication credential is already registered on the internal account. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair of an existing verified credential on the same internal account, then send that full stamp as `Grid-Wallet-Signature` and echo `requestId` as `Request-Id` on the retry. content: application/json: schema: @@ -3944,7 +3944,7 @@ paths: 1. Call `DELETE /auth/credentials/{id}` with no headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`. - 2. Sign the `payloadToSign` with the session private key of an existing verified credential on the same internal account — other than the one being revoked — and retry the same `DELETE` request with the signature supplied as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`. + 2. Use the session API keypair of an existing verified credential on the same internal account — other than the one being revoked — to build an API-key stamp over `payloadToSign`, then retry the same `DELETE` request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`. The account must retain at least one authentication credential; an account with only a single credential cannot use this endpoint to revoke it. operationId: revokeAuthCredential @@ -3962,10 +3962,10 @@ paths: - name: Grid-Wallet-Signature in: header required: false - description: Signature over the `payloadToSign` returned in a prior `202` response, produced with the session private key of an existing verified authentication credential on the same internal account (other than the one being revoked) and base64-encoded. Required on the signed retry; ignored on the initial call. + description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of an existing verified authentication credential on the same internal account (other than the one being revoked). Required on the signed retry; ignored on the initial call. schema: type: string - example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE= + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzaWduYXR1cmUiOiIzMDQ1MDIyMTAwLi4uIiwic2NoZW1lIjoiUDI1Nl9FQ0RTQV9TSEEyNTYifQ - name: Request-Id in: header required: false @@ -3975,7 +3975,7 @@ paths: example: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 responses: '202': - description: Challenge issued. The response contains a `payloadToSign` that must be signed with the session private key of an existing verified credential on the same internal account (other than the one being revoked), along with a `requestId` that must be echoed back on the retry. + description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair of an existing verified credential on the same internal account (other than the one being revoked), then echo `requestId` on the retry. content: application/json: schema: @@ -4232,7 +4232,7 @@ paths: 1. Call `DELETE /auth/sessions/{id}` with no headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`. - 2. Sign the `payloadToSign` with the session private key of a verified session on the same internal account (this can be the session being revoked, for self-logout) and retry the same `DELETE` request with the signature as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`. + 2. Use the session API keypair of a verified session on the same internal account (this can be the session being revoked, for self-logout) to build an API-key stamp over `payloadToSign`, then retry the same `DELETE` request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`. operationId: revokeAuthSession tags: - Embedded Wallet Auth @@ -4248,10 +4248,10 @@ paths: - name: Grid-Wallet-Signature in: header required: false - description: Signature over the `payloadToSign` returned in a prior `202` response, produced with the session private key of a verified session on the same internal account and base64-encoded. Required on the signed retry; ignored on the initial call. + description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified session on the same internal account. Required on the signed retry; ignored on the initial call. schema: type: string - example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE= + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzaWduYXR1cmUiOiIzMDQ1MDIyMTAwLi4uIiwic2NoZW1lIjoiUDI1Nl9FQ0RTQV9TSEEyNTYifQ - name: Request-Id in: header required: false @@ -4261,7 +4261,7 @@ paths: example: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 responses: '202': - description: Challenge issued. The response contains a `payloadToSign` that must be signed with the session private key of a verified session on the same internal account, along with a `requestId` that must be echoed back on the retry. + description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair of a verified session on the same internal account, then echo `requestId` on the retry. content: application/json: schema: @@ -13743,7 +13743,7 @@ components: $ref: '#/components/schemas/Permission' InternalAccountExportRequest: title: Internal Account Export Request - description: Request body for `POST /internal-accounts/{id}/export`. The `clientPublicKey` is required on both steps of the signed-retry flow. On step 1 Grid binds it into `payloadToSign` so the subsequent `Grid-Wallet-Signature` commits to the target pubkey; on step 2 the client echoes the same `clientPublicKey` back and Grid uses it to encrypt the wallet credentials returned in the `200` response. + description: Request body for `POST /internal-accounts/{id}/export`. The `clientPublicKey` is required on both steps of the signed-retry flow. On step 1 Grid binds it into `payloadToSign` so the subsequent stamp in `Grid-Wallet-Signature` commits to the target pubkey; on step 2 the client echoes the same `clientPublicKey` back and Grid uses it to encrypt the wallet credentials returned in the `200` response. type: object required: - clientPublicKey @@ -13765,8 +13765,11 @@ components: example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 encryptedWalletCredentials: type: string - description: Encrypted wallet mnemonic, sealed to the `clientPublicKey` supplied on the verify request. Decrypt with the matching private key, then manage the mnemonic securely — it is the master key of the self-custodial Embedded Wallet. Encoded as base58check (same format as `AuthSession.encryptedSessionSigningKey`). - example: 5KqM8nT3wJz2F9b6H1vRgLpXcA7eD4YuN0sBaE8kPyW5iVfG2xQoZ3MnK9LhU6jT1dS4rCyPbH7oVwX2AgE5uYsNq8fLzR3D7JeM1bVkWcHa9Tp + description: |- + Encrypted wallet mnemonic, sealed to the `clientPublicKey` from the request body using HPKE: DHKEM(P-256, HKDF-SHA256) + HKDF-SHA256 + AES-256-GCM. Decrypt with the matching private key, then manage the mnemonic securely because it is the master key of the self-custodial Embedded Wallet. + The value is a JSON string of the form `{"version": "v1.0.0", "data": "", "dataSignature": "", "enclaveQuorumPublic": ""}`. `data` hex-decodes to JSON `{"encappedPublic": "", "ciphertext": "", "organizationId": ""}`, where `encappedPublic` is the uncompressed SEC1 ephemeral public key. `dataSignature` is an ECDSA-P256-SHA256 signature over the `data` bytes produced by the issuer key in `enclaveQuorumPublic`; verify before decrypting. + In sandbox, `dataSignature` and `enclaveQuorumPublic` are empty strings. Clients should bypass attestation verification when calling against sandbox. + example: '{"version":"v1.0.0","data":"7b22656e6361707065645075626c6963223a22303433...","dataSignature":"3045022100c9...","enclaveQuorumPublic":"04a1b2c3..."}' SignedRequestChallenge: title: Signed Request Challenge type: object @@ -13778,7 +13781,7 @@ components: properties: payloadToSign: type: string - description: Payload that must be signed with the session private key of a verified authentication credential. The resulting signature is passed as the `Grid-Wallet-Signature` header on the retry of the originating request to complete the operation. + description: Canonical payload for the retry authorization stamp. Build an API-key stamp over this exact value with the session API keypair, then send the full base64url-encoded stamp in `Grid-Wallet-Signature` on the retry that completes the original request. example: Y2hhbGxlbmdlLXBheWxvYWQtdG8tc2lnbg== requestId: type: string diff --git a/mintlify/payouts-and-b2b/index.mdx b/mintlify/payouts-and-b2b/index.mdx index df66e0df..19f9877b 100644 --- a/mintlify/payouts-and-b2b/index.mdx +++ b/mintlify/payouts-and-b2b/index.mdx @@ -19,6 +19,10 @@ import { FeatureCard, FeatureCardGrid } from '/snippets/feature-card.mdx'; With {payToBankProductName}, you can send and receive low cost real-time payments to bank accounts worldwide through a single, simple API. {topLevelProductName} automatically routes each payment across its network of {topLevelProductName} switches, handling FX, blockchain settlement, and instant banking off-ramps for you. + + Looking to issue customer-owned accounts with balances and customer-signed withdrawals? Start with [Global Accounts](/global-accounts). Use this Payouts guide when your primary goal is sending payments to external destinations. + + Single API, global reach. {payToBankProductName} interacts with the Money Grid to route your payments globally. diff --git a/mintlify/snippets/embedded-wallets/authentication.mdx b/mintlify/snippets/embedded-wallets/authentication.mdx index 7dfdeb85..00cb3bde 100644 --- a/mintlify/snippets/embedded-wallets/authentication.mdx +++ b/mintlify/snippets/embedded-wallets/authentication.mdx @@ -357,7 +357,7 @@ curl -X POST "$GRID_BASE_URL/auth/credentials" \ -H "Content-Type: application/json" \ -d '{ "type": "OAUTH", - "accountId": "EmbeddedWallet:019542f5-b3e7-1d02-0000-000000000002", + "accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002", "oidcToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9..." }' ``` @@ -385,7 +385,7 @@ The lowest-friction credential type — works on any device with email access an ### Email OTP registration -Creating the credential triggers an OTP email to the address you pass. The user reads the code off the email and submits it through your UI. +Creating the credential triggers an OTP email to the email address on the customer record tied to the internal account. The user reads the code off the email and submits it through your UI. ```mermaid sequenceDiagram @@ -394,8 +394,8 @@ sequenceDiagram participant G as Grid participant E as Email - C->>IB: POST /my-backend/otp/register { email } - IB->>G: POST /auth/credentials { type: EMAIL_OTP, email, accountId } + C->>IB: POST /my-backend/otp/register + IB->>G: POST /auth/credentials { type: EMAIL_OTP, accountId } G->>E: deliver OTP email G-->>IB: 201 AuthMethod IB-->>C: { credentialId } @@ -413,8 +413,7 @@ curl -X POST "$GRID_BASE_URL/auth/credentials" \ -H "Content-Type: application/json" \ -d '{ "type": "EMAIL_OTP", - "accountId": "EmbeddedWallet:019542f5-b3e7-1d02-0000-000000000002", - "email": "jane@example.com" + "accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002" }' ``` @@ -423,7 +422,7 @@ curl -X POST "$GRID_BASE_URL/auth/credentials" \ ```json { "id": "AuthMethod:019542f5-b3e7-1d02-0000-000000000004", - "accountId": "EmbeddedWallet:019542f5-b3e7-1d02-0000-000000000002", + "accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002", "type": "EMAIL_OTP", "nickname": "jane@example.com", "createdAt": "2026-04-19T12:00:00Z", @@ -467,12 +466,12 @@ Same pattern as the first activation: call `/challenge` to send a new OTP, then ## Managing credentials -Every Embedded Wallet starts with a single credential — the one used in the quickstart. In production, encourage customers to register a second credential of a different type (e.g., an email OTP alongside a passkey) so the wallet 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 Embedded Wallet starts with a single credential. In production, encourage customers to register a second credential of a different type (e.g., an email OTP alongside a passkey) so the wallet 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 ```bash -curl -X GET "$GRID_BASE_URL/auth/credentials?accountId=EmbeddedWallet:019542f5-b3e7-1d02-0000-000000000002" \ +curl -X GET "$GRID_BASE_URL/auth/credentials?accountId=InternalAccount:019542f5-b3e7-1d02-0000-000000000002" \ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" ``` @@ -483,7 +482,7 @@ curl -X GET "$GRID_BASE_URL/auth/credentials?accountId=EmbeddedWallet:019542f5-b "data": [ { "id": "AuthMethod:019542f5-b3e7-1d02-0000-000000000001", - "accountId": "EmbeddedWallet:019542f5-b3e7-1d02-0000-000000000002", + "accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002", "type": "PASSKEY", "nickname": "iPhone Face-ID", "createdAt": "2026-04-08T15:30:01Z", @@ -491,7 +490,7 @@ curl -X GET "$GRID_BASE_URL/auth/credentials?accountId=EmbeddedWallet:019542f5-b }, { "id": "AuthMethod:019542f5-b3e7-1d02-0000-000000000004", - "accountId": "EmbeddedWallet:019542f5-b3e7-1d02-0000-000000000002", + "accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002", "type": "EMAIL_OTP", "nickname": "jane@example.com", "createdAt": "2026-04-09T10:15:00Z", @@ -505,7 +504,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, and exporting a wallet all share the same signed-retry shape: ```mermaid sequenceDiagram @@ -516,17 +515,17 @@ 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: build API-key stamp over payloadToSign + 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. -- Sign with the **session private key** held on the client — never ship it back to your backend. +- Build the API-key stamp over the `payloadToSign` **byte-for-byte as Grid returned it**. Do not re-parse, re-serialize, or modify whitespace. +- Build the stamp with the **session API keypair** held on the client — never ship the private key material back to your backend. - The retry must reach Grid before `expiresAt` (typically 5 minutes from issue). - The `requestId` is single-use; reusing one yields `401`. @@ -542,8 +541,7 @@ Requires an active session on an *existing* credential on the same account. The -H "Content-Type: application/json" \ -d '{ "type": "EMAIL_OTP", - "accountId": "EmbeddedWallet:019542f5-b3e7-1d02-0000-000000000002", - "email": "jane@example.com" + "accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002" }' ``` @@ -552,28 +550,27 @@ Requires an active session on an *existing* credential on the same account. The ```json { "type": "EMAIL_OTP", - "payloadToSign": "{\"requestId\":\"7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21\",\"type\":\"EMAIL_OTP\",\"accountId\":\"EmbeddedWallet:019542f5-b3e7-1d02-0000-000000000002\",\"expiresAt\":\"2026-04-08T15:35:00Z\"}", + "payloadToSign": "{\"requestId\":\"7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21\",\"type\":\"EMAIL_OTP\",\"accountId\":\"InternalAccount:019542f5-b3e7-1d02-0000-000000000002\",\"expiresAt\":\"2026-04-08T15:35:00Z\"}", "requestId": "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 an API-key stamp with the session API keypair from the existing credential's active session — see authorizing 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: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzaWduYXR1cmUiOiIzMDQ1MDIyMTAwLi4uIiwic2NoZW1lIjoiUDI1Nl9FQ0RTQV9TSEEyNTYifQ" \ -H "Request-Id: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21" \ -d '{ "type": "EMAIL_OTP", - "accountId": "EmbeddedWallet:019542f5-b3e7-1d02-0000-000000000002", - "email": "jane@example.com" + "accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002" }' ``` @@ -590,7 +587,7 @@ Requires an active session on an *existing* credential on the same account. The ### Revoke a credential -A credential is revoked by signing with a session from **a different credential on the same account**. This prevents a compromised credential from revoking itself to lock the legitimate owner out. An account must keep at least one credential — if only one exists, the revoke call returns `400`. +A credential is revoked by stamping the retry with a session from **a different credential on the same account**. This prevents a compromised credential from revoking itself to lock the legitimate owner out. An account must keep at least one credential — if only one exists, the revoke call returns `400`. @@ -610,14 +607,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 builds the API-key stamp with the session API keypair 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: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzaWduYXR1cmUiOiIzMDQ1MDIyMTAwLi4uIiwic2NoZW1lIjoiUDI1Nl9FQ0RTQV9TSEEyNTYifQ" \ -H "Request-Id: 9f7a2c10-5e88-4fb1-bd0e-1c3a8e7b2d45" ``` diff --git a/mintlify/snippets/embedded-wallets/client-keys.mdx b/mintlify/snippets/embedded-wallets/client-keys.mdx index 25b3f2f1..5c1f8a08 100644 --- a/mintlify/snippets/embedded-wallets/client-keys.mdx +++ b/mintlify/snippets/embedded-wallets/client-keys.mdx @@ -127,7 +127,7 @@ Grid encrypts the session signing key with **HPKE** (RFC 9180) using the suite: The wire format is a base58check string. Decoded, the payload is a 33-byte compressed P-256 encapsulated public key followed by AES-256-GCM ciphertext (ciphertext || 16-byte auth tag). - **In sandbox, `encryptedSessionSigningKey` is a stub** — random bytes shaped like a real HPKE payload but not encrypted to your `clientPublicKey`. Decrypt attempts will fail. Skip this step entirely on sandbox and use the literal `Grid-Wallet-Signature: sandbox-valid-signature` for any signed action (see Sign a `payloadToSign`). The decrypt path below applies only to production. + **In sandbox, `encryptedSessionSigningKey` is a stub** — random bytes shaped like a real HPKE payload but not encrypted to your `clientPublicKey`. Decrypt attempts will fail. Skip this step entirely on sandbox and use the literal `Grid-Wallet-Signature: sandbox-valid-signature` for any signed action (see Authorize a `payloadToSign`). The decrypt path below applies only to production. ## 3. Decrypt the session signing key @@ -218,7 +218,7 @@ func decryptSessionSigningKey( The plaintext is a **32-byte P-256 private scalar**. Treat it as the session signing key for the rest of the session. -## 4. Sign a `payloadToSign` +## 4. Authorize a `payloadToSign` Grid returns `payloadToSign` strings from several endpoints: @@ -226,12 +226,23 @@ Grid returns `payloadToSign` strings from several endpoints: - `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. -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). +Always authorize the payload **byte-for-byte as returned**. Do not re-parse, re-serialize, trim, or normalize whitespace. + +There are two authorization shapes: + +| Flow | What to send in `Grid-Wallet-Signature` | +|---|---| +| Quote execution (`POST /quotes/{quoteId}/execute`) | A base64 ECDSA signature over the quote `payloadToSign`. | +| Signed retries (`POST /auth/credentials`, `DELETE /auth/credentials/{id}`, `DELETE /auth/sessions/{id}`, `POST /internal-accounts/{id}/export`) | A full API-key stamp over the `payloadToSign`, built with the session API keypair. Also echo `Request-Id` from the `202` response. | - **In sandbox, send `Grid-Wallet-Signature: sandbox-valid-signature`** for any signed wallet 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. + **In sandbox, send `Grid-Wallet-Signature: sandbox-valid-signature`** for any signed account action. Sandbox skips signature and stamp verification, so you don't need a real session signing key or an extracted `payloadToSign`. The authorization patterns below apply only to production. +### Quote execution signature + +For quote execution, sign the quote `payloadToSign` with the session signing key. The signature is ECDSA over SHA-256, DER-encoded, then base64-encoded. + ```typescript Web (TypeScript) // npm i @noble/curves @noble/hashes @@ -304,6 +315,17 @@ curl -X POST "https://api.lightspark.com/grid/2025-10-13/quotes/Quote:019542f5-b -H "Grid-Wallet-Signature: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE=" ``` +### Signed-retry stamp + +For signed retries, use the session API keypair to build a full API-key stamp over the `payloadToSign`. Send that complete stamp as `Grid-Wallet-Signature` and include the `Request-Id` returned with the challenge. + +```bash +curl -X DELETE "https://api.lightspark.com/grid/2025-10-13/auth/sessions/Session:019542f5-b3e7-1d02-0000-000000000003" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \ + -H "Grid-Wallet-Signature: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzaWduYXR1cmUiOiIzMDQ1MDIyMTAwLi4uIiwic2NoZW1lIjoiUDI1Nl9FQ0RTQV9TSEEyNTYifQ" \ + -H "Request-Id: 2b1e5a08-9c44-4e91-ae7f-6d0b3f8c1e22" +``` + ## Session lifetime Sessions are valid for 15 minutes by default. The `AuthSession.expiresAt` field tells you exactly when the session signing key stops being accepted. After expiry, the client must re-verify the credential (see Authentication) to obtain a fresh session. diff --git a/mintlify/snippets/embedded-wallets/exporting-wallet.mdx b/mintlify/snippets/embedded-wallets/exporting-wallet.mdx index 9624c18a..29d67995 100644 --- a/mintlify/snippets/embedded-wallets/exporting-wallet.mdx +++ b/mintlify/snippets/embedded-wallets/exporting-wallet.mdx @@ -1,6 +1,6 @@ -Exporting a wallet returns the wallet's mnemonic seed, encrypted to the client's public key. The customer decrypts it on their device and can then import the wallet into any compatible self-custody client. Grid never sees the plaintext seed leaving the system. +Exporting a wallet returns the wallet's mnemonic seed in an encrypted credentials envelope sealed to the client's public key. The customer verifies and decrypts that envelope on their device and can then import the wallet into any compatible self-custody client. Grid never sees the plaintext seed leaving the system. -Export uses the same signed-retry pattern as credential and session revocation — the initial `POST` returns a `payloadToSign`, and the signed retry returns the encrypted seed. +Export uses the same signed-retry pattern as credential and session revocation — the initial `POST` returns a `payloadToSign`, and the stamped retry returns the encrypted seed. Generate a fresh P-256 client key pair specifically for the export. Send its `clientPublicKey` on both export requests, then decrypt `encryptedWalletCredentials` with the matching private key after the signed retry succeeds. @@ -13,19 +13,19 @@ 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: build API-key stamp over payloadToSign + C->>IB: { stamp } IB->>G: POST /internal-accounts/{id}/export { same clientPublicKey }
Grid-Wallet-Signature
Request-Id - G-->>IB: 200 { id, encryptedWalletCredentials } + G-->>IB: 200 { id, encryptedWalletCredentials envelope } IB-->>C: { encryptedWalletCredentials } - C->>C: decrypt with client private key
→ mnemonic + C->>C: verify envelope, decrypt with client private key
→ mnemonic ``` ```bash curl -X POST "$GRID_BASE_URL/internal-accounts/InternalAccount:019542f5-b3e7-1d02-0000-000000000002/export" \ - -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{ "clientPublicKey": "04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2" @@ -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 an API-key stamp over `payloadToSign` with an active session API keypair 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: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzaWduYXR1cmUiOiIzMDQ1MDIyMTAwLi4uIiwic2NoZW1lIjoiUDI1Nl9FQ0RTQV9TSEEyNTYifQ" \ -H "Request-Id: c3f8a614-47e2-4a19-9f5d-2b0a91d47e08" \ -d '{ "clientPublicKey": "04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2" @@ -62,12 +62,14 @@ sequenceDiagram ```json { "id": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002", - "encryptedWalletCredentials": "5KqM8nT3wJz2F9b6H1vRgLpXcA7eD4YuN0sBaE8kPyW5iVfG2xQoZ3MnK9LhU6jT1dS4rCyPbH7oVwX2AgE5uYsNq8fLzR3D7JeM1bVkWcHa9Tp" + "encryptedWalletCredentials": "{\"version\":\"v1.0.0\",\"data\":\"7b22656e6361707065645075626c6963223a22303433...\",\"dataSignature\":\"3045022100c9...\",\"enclaveQuorumPublic\":\"04a1b2c3...\"}" } ``` - - `encryptedWalletCredentials` uses the same base58check/HPKE format as `encryptedSessionSigningKey`. Decrypt with the export private key that matches the `clientPublicKey` you sent on both export requests — see decrypt the session signing key for code. + + `encryptedWalletCredentials` is a JSON string envelope. Parse the string, verify `dataSignature` against the `data` bytes using `enclaveQuorumPublic`, then hex-decode `data` to get the HPKE payload (`encappedPublic`, `ciphertext`, and `organizationId`). Decrypt the ciphertext with the export private key that matches the `clientPublicKey` you sent on both export requests. + + In sandbox, `dataSignature` and `enclaveQuorumPublic` are empty strings. Skip attestation verification in sandbox and decrypt the envelope payload directly. The plaintext is a BIP-39 mnemonic (the wallet's master seed). diff --git a/mintlify/snippets/embedded-wallets/managing-sessions.mdx b/mintlify/snippets/embedded-wallets/managing-sessions.mdx index 4966b698..c7a34d51 100644 --- a/mintlify/snippets/embedded-wallets/managing-sessions.mdx +++ b/mintlify/snippets/embedded-wallets/managing-sessions.mdx @@ -38,7 +38,7 @@ The list endpoint returns all **active** sessions; expired sessions are not incl ## 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. +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: build the retry stamp with the session key you are about to invalidate. @@ -58,14 +58,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 an API-key stamp over `payloadToSign` with any active session API keypair 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: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzaWduYXR1cmUiOiIzMDQ1MDIyMTAwLi4uIiwic2NoZW1lIjoiUDI1Nl9FQ0RTQV9TSEEyNTYifQ" \ -H "Request-Id: 2b1e5a08-9c44-4e91-ae7f-6d0b3f8c1e22" ``` diff --git a/mintlify/snippets/embedded-wallets/overview.mdx b/mintlify/snippets/embedded-wallets/overview.mdx index a5f4a4b0..bec0b4d0 100644 --- a/mintlify/snippets/embedded-wallets/overview.mdx +++ b/mintlify/snippets/embedded-wallets/overview.mdx @@ -100,7 +100,7 @@ curl -X POST "$GRID_BASE_URL/customers" \ When a customer is created on a USDB-enabled platform, Grid automatically provisions an Embedded Wallet alongside their other internal accounts. Fetch it by filtering the customer's internal accounts by `type=EMBEDDED_WALLET`. ```bash -curl -X GET "$GRID_BASE_URL/internal-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001&type=EMBEDDED_WALLET" \ +curl -X GET "$GRID_BASE_URL/customers/internal-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001&type=EMBEDDED_WALLET" \ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" ``` @@ -184,7 +184,7 @@ curl -X POST "$GRID_BASE_URL/sandbox/internal-accounts/InternalAccount:019542f5- }' ``` -You will receive an `INCOMING_PAYMENT` webhook when the balance updates. The wallet now holds 1,000.00 USDB. +You will receive an `INTERNAL_ACCOUNT.BALANCE_UPDATED` webhook when the balance updates. The wallet now holds 1,000.00 USDB. To fund from another currency (USD ACH, USDC on-chain, etc.), create a quote with `destination.destinationType: "ACCOUNT"` pointing at the Embedded Wallet's `InternalAccount` id. The quote's `sourceCurrency` can be any supported platform currency; Grid will convert into USDB on execute. @@ -257,12 +257,12 @@ curl -X POST "$GRID_BASE_URL/quotes" \ "createdAt": "2026-04-19T12:05:00Z", "expiresAt": "2026-04-19T12:10:00Z", "source": { - "accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002", - "currency": "USDB" + "sourceType": "ACCOUNT", + "accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002" }, "destination": { - "accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123", - "currency": "USD" + "destinationType": "ACCOUNT", + "accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123" }, "sendingCurrency": { "code": "USDB", "name": "USDB", "decimals": 2 }, "receivingCurrency": { "code": "USD", "name": "United States Dollar", "symbol": "$", "decimals": 2 }, @@ -369,7 +369,7 @@ 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 signs the quote's `payloadToSign` with the resulting session signing key. Return the base64 signature to your backend. diff --git a/mintlify/snippets/sandbox-global-account-magic.mdx b/mintlify/snippets/sandbox-global-account-magic.mdx new file mode 100644 index 00000000..3fd9d6e8 --- /dev/null +++ b/mintlify/snippets/sandbox-global-account-magic.mdx @@ -0,0 +1,86 @@ +The Grid sandbox accepts a small set of magic values that bypass real auth and credential checks for Global Account flows, so you can exercise the full request shape without standing up Turnkey, WebAuthn, or an OIDC provider. These values are sandbox-only — production enforces real signature verification, WebAuthn assertion, and OIDC nonce binding. + +A wrong magic value (or any other value) returns `401 UNAUTHORIZED` with a `reason` field that names the specific check that failed. + +### Email OTP code + +Pass `000000` as the body `otp` on `POST /auth/credentials/{id}/verify` when the credential type is `EMAIL_OTP`. The sandbox skips OTP delivery and accepts this value as a valid response to the issued challenge. + +```bash +curl -X POST "$GRID_BASE_URL/auth/credentials/AuthMethod:abc123/verify" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \ + -H "Content-Type: application/json" \ + -H "Request-Id: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21" \ + -d '{ + "type": "EMAIL_OTP", + "otp": "000000", + "clientPublicKey": "04f45f2a..." + }' +``` + +Any other code returns `401 UNAUTHORIZED` with `reason: "Invalid OTP code"`. + +### Passkey assertion signature + +Pass `sandbox-valid-passkey-signature` as `assertion.signature` on `POST /auth/credentials/{id}/verify` when the credential type is `PASSKEY`. The sandbox accepts the rest of the assertion as-is and skips the WebAuthn signature check. + +```bash +curl -X POST "$GRID_BASE_URL/auth/credentials/AuthMethod:abc123/verify" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \ + -H "Content-Type: application/json" \ + -H "Request-Id: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21" \ + -d '{ + "type": "PASSKEY", + "assertion": { + "credentialId": "...", + "clientDataJson": "...", + "authenticatorData": "...", + "signature": "sandbox-valid-passkey-signature" + }, + "clientPublicKey": "04f45f2a..." + }' +``` + +Any other signature returns `401 UNAUTHORIZED` with `reason: "Invalid passkey signature"`. `clientPublicKey` is still required — the magic value bypasses the credential check, not the HPKE plumbing that seals the session signing key to the public key you supply. + +### OAuth (OIDC) token + +Pass `sandbox-valid-oidc-token` as the body `oidcToken` on both `POST /auth/credentials` (OAUTH create) and `POST /auth/credentials/{id}/verify` (OAUTH). + +```bash +curl -X POST "$GRID_BASE_URL/auth/credentials/AuthMethod:abc123/verify" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \ + -H "Content-Type: application/json" \ + -H "Request-Id: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21" \ + -d '{ + "type": "OAUTH", + "oidcToken": "sandbox-valid-oidc-token", + "clientPublicKey": "04f45f2a..." + }' +``` + +Any other token returns `401 UNAUTHORIZED` with `reason: "Invalid OIDC token"`. + + + **OAUTH create still requires a JWT-shaped token.** On the initial `POST /auth/credentials` (OAUTH create), the `oidcToken` must be a structurally valid JWT (`header.payload.signature`) so Grid can decode the `iss` claim and resolve the provider name. The literal `sandbox-valid-oidc-token` works on `verify` but not on `create` — for `create`, sign your own dummy JWT with any payload that includes a recognized `iss` claim. The sandbox bypasses signature verification, not JWT structure parsing. + + +### Wallet signature header + +Pass `sandbox-valid-signature` as the `Grid-Wallet-Signature` HTTP header on any signed-retry flow: + +- `POST /auth/credentials` (add-additional-credential signed retry) +- `DELETE /auth/credentials/{id}` (revoke credential) +- `DELETE /auth/sessions/{id}` (revoke session) +- `POST /internal-accounts/{id}/export` (export wallet) +- `POST /quotes/{quoteId}/execute` (when source is an embedded wallet) + +```bash +curl -X POST "$GRID_BASE_URL/quotes/Quote:abc123/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: sandbox-valid-signature" +``` + +Any other header value returns `401 UNAUTHORIZED` with `reason: "Invalid Grid-Wallet-Signature"`. diff --git a/mintlify/snippets/terminology.mdx b/mintlify/snippets/terminology.mdx index 143cf08d..e3eb8475 100644 --- a/mintlify/snippets/terminology.mdx +++ b/mintlify/snippets/terminology.mdx @@ -49,7 +49,7 @@ Internal accounts: - Track available balance for sending payments or receiving funds - **Embedded Wallets** are a special kind of internal account: self-custodial [Spark](https://spark.money) wallets that Grid provisions for your customers to hold a stablecoin balance. They behave like any other internal account on the way in, but every outbound transfer must be authorized by a customer signature. See [Embedded Wallets](/payouts-and-b2b/embedded-wallets/overview) for the full overview. + **Global Accounts** are a special kind of internal account: self-custodial [Spark](https://spark.money) wallets that Grid provisions for your customers to hold a stablecoin balance. They behave like any other internal account on the way in, but every outbound transfer must be authorized by a customer signature. See [Global Accounts](/global-accounts) for the full overview. ### External Accounts diff --git a/openapi.yaml b/openapi.yaml index b524fd03..9e4313fd 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3635,9 +3635,9 @@ paths: Export is a two-step signed-retry flow (same pattern as add-additional credential, revoke credential, and revoke session): - 1. Call `POST /internal-accounts/{id}/export` with the request body `{ "clientPublicKey": "..." }` and no signature headers. Grid binds the `clientPublicKey` into the `payloadToSign` it returns, so the subsequent `Grid-Wallet-Signature` commits to the target encryption key. The response is `202` with `payloadToSign`, `requestId`, and `expiresAt`. + 1. Call `POST /internal-accounts/{id}/export` with the request body `{ "clientPublicKey": "..." }` and no signature headers. Grid binds the `clientPublicKey` into the `payloadToSign` it returns, so the subsequent stamp in `Grid-Wallet-Signature` commits to the target encryption key. The response is `202` with `payloadToSign`, `requestId`, and `expiresAt`. - 2. Sign the `payloadToSign` with the session private key of a verified authentication credential on the same internal account and retry with the signature as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The retry body must carry the **same** `clientPublicKey` submitted in step 1 — Grid rejects the retry with `401` if it disagrees with what was bound into `payloadToSign`. The signed retry returns `200` with `encryptedWalletCredentials`, which the client decrypts with the matching private key. + 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** `clientPublicKey` submitted in step 1 — Grid rejects the retry with `401` if it disagrees with what was bound into `payloadToSign`. The signed retry returns `200` with `encryptedWalletCredentials`, which the client decrypts with the matching private key. The `clientPublicKey` is ephemeral: generate a fresh P-256 keypair for this export and discard the private key after decrypting. Do not reuse the keypair from any prior verify call — that private key was already discarded after decrypting the session signing key it was issued against. operationId: exportInternalAccount @@ -3655,10 +3655,10 @@ paths: - name: Grid-Wallet-Signature in: header required: false - description: Signature over the `payloadToSign` returned in a prior `202` response, produced with the session private key of a verified authentication credential on the target internal account and base64-encoded. Required on the signed retry; ignored on the initial call. + 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: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE= + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzaWduYXR1cmUiOiIzMDQ1MDIyMTAwLi4uIiwic2NoZW1lIjoiUDI1Nl9FQ0RTQV9TSEEyNTYifQ - name: Request-Id in: header required: false @@ -3685,7 +3685,7 @@ paths: schema: $ref: '#/components/schemas/InternalAccountExportResponse' '202': - description: Challenge issued. The response contains a `payloadToSign` (which binds the submitted `clientPublicKey`) that must be signed with the session private key of a verified authentication credential on the target internal account, along with a `requestId` that must be echoed back on the retry. + description: Challenge issued. The response contains `payloadToSign` (which binds the submitted `clientPublicKey`) 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: @@ -3726,7 +3726,7 @@ paths: **Adding an additional credential** - Registering an additional credential against an internal account that already has one requires a signature from an existing verified credential. Call this endpoint with the new credential's details; if an existing credential is already registered on the internal account the response is `202` with a `payloadToSign` and a `requestId`. Sign the payload with the session private key of an existing verified credential on the same internal account (decrypted client-side from its `encryptedSessionSigningKey`) and retry the same request with the signature supplied as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `201` with the created `AuthMethod`. For `EMAIL_OTP`, the OTP email is triggered on the signed retry, and the credential must then be activated via `POST /auth/credentials/{id}/verify`. + Registering an additional credential against an internal account that already has one requires a signature from an existing verified credential. Call this endpoint with the new credential's details; if an existing credential is already registered on the internal account the response is `202` with a `payloadToSign` and a `requestId`. Use the session API keypair of an existing verified credential on the same internal account (decrypted client-side from its `encryptedSessionSigningKey`) to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `201` with the created `AuthMethod`. For `EMAIL_OTP`, the OTP email is triggered on the signed retry, and the credential must then be activated via `POST /auth/credentials/{id}/verify`. operationId: createAuthCredential tags: - Embedded Wallet Auth @@ -3736,10 +3736,10 @@ paths: - name: Grid-Wallet-Signature in: header required: false - description: Signature over the `payloadToSign` returned in a prior `202` response, produced with the session private key of an existing verified authentication credential on the target internal account and base64-encoded. Required when registering an additional credential on an internal account that already has one; ignored when the internal account has no existing credentials. + description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of an existing verified authentication credential on the target internal account. Required when registering an additional credential on an internal account that already has one; ignored when the internal account has no existing credentials. schema: type: string - example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE= + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzaWduYXR1cmUiOiIzMDQ1MDIyMTAwLi4uIiwic2NoZW1lIjoiUDI1Nl9FQ0RTQV9TSEEyNTYifQ - name: Request-Id in: header required: false @@ -3818,7 +3818,7 @@ paths: requestId: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 expiresAt: '2026-04-08T15:35:00Z' '202': - description: An existing authentication credential is already registered on the internal account. The response contains a `payloadToSign` that must be signed with the session private key of an existing verified credential on the same internal account, along with a `requestId` that must be echoed back on the retry. The signature is passed as the `Grid-Wallet-Signature` header and the `requestId` as the `Request-Id` header on a retry of this request to complete registration. + description: An existing authentication credential is already registered on the internal account. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair of an existing verified credential on the same internal account, then send that full stamp as `Grid-Wallet-Signature` and echo `requestId` as `Request-Id` on the retry. content: application/json: schema: @@ -3944,7 +3944,7 @@ paths: 1. Call `DELETE /auth/credentials/{id}` with no headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`. - 2. Sign the `payloadToSign` with the session private key of an existing verified credential on the same internal account — other than the one being revoked — and retry the same `DELETE` request with the signature supplied as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`. + 2. Use the session API keypair of an existing verified credential on the same internal account — other than the one being revoked — to build an API-key stamp over `payloadToSign`, then retry the same `DELETE` request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`. The account must retain at least one authentication credential; an account with only a single credential cannot use this endpoint to revoke it. operationId: revokeAuthCredential @@ -3962,10 +3962,10 @@ paths: - name: Grid-Wallet-Signature in: header required: false - description: Signature over the `payloadToSign` returned in a prior `202` response, produced with the session private key of an existing verified authentication credential on the same internal account (other than the one being revoked) and base64-encoded. Required on the signed retry; ignored on the initial call. + description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of an existing verified authentication credential on the same internal account (other than the one being revoked). Required on the signed retry; ignored on the initial call. schema: type: string - example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE= + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzaWduYXR1cmUiOiIzMDQ1MDIyMTAwLi4uIiwic2NoZW1lIjoiUDI1Nl9FQ0RTQV9TSEEyNTYifQ - name: Request-Id in: header required: false @@ -3975,7 +3975,7 @@ paths: example: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 responses: '202': - description: Challenge issued. The response contains a `payloadToSign` that must be signed with the session private key of an existing verified credential on the same internal account (other than the one being revoked), along with a `requestId` that must be echoed back on the retry. + description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair of an existing verified credential on the same internal account (other than the one being revoked), then echo `requestId` on the retry. content: application/json: schema: @@ -4232,7 +4232,7 @@ paths: 1. Call `DELETE /auth/sessions/{id}` with no headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`. - 2. Sign the `payloadToSign` with the session private key of a verified session on the same internal account (this can be the session being revoked, for self-logout) and retry the same `DELETE` request with the signature as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`. + 2. Use the session API keypair of a verified session on the same internal account (this can be the session being revoked, for self-logout) to build an API-key stamp over `payloadToSign`, then retry the same `DELETE` request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`. operationId: revokeAuthSession tags: - Embedded Wallet Auth @@ -4248,10 +4248,10 @@ paths: - name: Grid-Wallet-Signature in: header required: false - description: Signature over the `payloadToSign` returned in a prior `202` response, produced with the session private key of a verified session on the same internal account and base64-encoded. Required on the signed retry; ignored on the initial call. + description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified session on the same internal account. Required on the signed retry; ignored on the initial call. schema: type: string - example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE= + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzaWduYXR1cmUiOiIzMDQ1MDIyMTAwLi4uIiwic2NoZW1lIjoiUDI1Nl9FQ0RTQV9TSEEyNTYifQ - name: Request-Id in: header required: false @@ -4261,7 +4261,7 @@ paths: example: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 responses: '202': - description: Challenge issued. The response contains a `payloadToSign` that must be signed with the session private key of a verified session on the same internal account, along with a `requestId` that must be echoed back on the retry. + description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair of a verified session on the same internal account, then echo `requestId` on the retry. content: application/json: schema: @@ -13743,7 +13743,7 @@ components: $ref: '#/components/schemas/Permission' InternalAccountExportRequest: title: Internal Account Export Request - description: Request body for `POST /internal-accounts/{id}/export`. The `clientPublicKey` is required on both steps of the signed-retry flow. On step 1 Grid binds it into `payloadToSign` so the subsequent `Grid-Wallet-Signature` commits to the target pubkey; on step 2 the client echoes the same `clientPublicKey` back and Grid uses it to encrypt the wallet credentials returned in the `200` response. + description: Request body for `POST /internal-accounts/{id}/export`. The `clientPublicKey` is required on both steps of the signed-retry flow. On step 1 Grid binds it into `payloadToSign` so the subsequent stamp in `Grid-Wallet-Signature` commits to the target pubkey; on step 2 the client echoes the same `clientPublicKey` back and Grid uses it to encrypt the wallet credentials returned in the `200` response. type: object required: - clientPublicKey @@ -13765,8 +13765,11 @@ components: example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 encryptedWalletCredentials: type: string - description: Encrypted wallet mnemonic, sealed to the `clientPublicKey` supplied on the verify request. Decrypt with the matching private key, then manage the mnemonic securely — it is the master key of the self-custodial Embedded Wallet. Encoded as base58check (same format as `AuthSession.encryptedSessionSigningKey`). - example: 5KqM8nT3wJz2F9b6H1vRgLpXcA7eD4YuN0sBaE8kPyW5iVfG2xQoZ3MnK9LhU6jT1dS4rCyPbH7oVwX2AgE5uYsNq8fLzR3D7JeM1bVkWcHa9Tp + description: |- + Encrypted wallet mnemonic, sealed to the `clientPublicKey` from the request body using HPKE: DHKEM(P-256, HKDF-SHA256) + HKDF-SHA256 + AES-256-GCM. Decrypt with the matching private key, then manage the mnemonic securely because it is the master key of the self-custodial Embedded Wallet. + The value is a JSON string of the form `{"version": "v1.0.0", "data": "", "dataSignature": "", "enclaveQuorumPublic": ""}`. `data` hex-decodes to JSON `{"encappedPublic": "", "ciphertext": "", "organizationId": ""}`, where `encappedPublic` is the uncompressed SEC1 ephemeral public key. `dataSignature` is an ECDSA-P256-SHA256 signature over the `data` bytes produced by the issuer key in `enclaveQuorumPublic`; verify before decrypting. + In sandbox, `dataSignature` and `enclaveQuorumPublic` are empty strings. Clients should bypass attestation verification when calling against sandbox. + example: '{"version":"v1.0.0","data":"7b22656e6361707065645075626c6963223a22303433...","dataSignature":"3045022100c9...","enclaveQuorumPublic":"04a1b2c3..."}' SignedRequestChallenge: title: Signed Request Challenge type: object @@ -13778,7 +13781,7 @@ components: properties: payloadToSign: type: string - description: Payload that must be signed with the session private key of a verified authentication credential. The resulting signature is passed as the `Grid-Wallet-Signature` header on the retry of the originating request to complete the operation. + description: Canonical payload for the retry authorization stamp. Build an API-key stamp over this exact value with the session API keypair, then send the full base64url-encoded stamp in `Grid-Wallet-Signature` on the retry that completes the original request. example: Y2hhbGxlbmdlLXBheWxvYWQtdG8tc2lnbg== requestId: type: string diff --git a/openapi/components/schemas/common/SignedRequestChallenge.yaml b/openapi/components/schemas/common/SignedRequestChallenge.yaml index 56c0de54..d5b22268 100644 --- a/openapi/components/schemas/common/SignedRequestChallenge.yaml +++ b/openapi/components/schemas/common/SignedRequestChallenge.yaml @@ -15,10 +15,11 @@ properties: payloadToSign: type: string description: >- - Payload that must be signed with the session private key of a - verified authentication credential. The resulting signature is - passed as the `Grid-Wallet-Signature` header on the retry of the - originating request to complete the operation. + Canonical payload for the retry authorization stamp. Build an + API-key stamp over this exact value with the session API keypair, + then send the full base64url-encoded stamp in + `Grid-Wallet-Signature` on the retry that completes the original + request. example: Y2hhbGxlbmdlLXBheWxvYWQtdG8tc2lnbg== requestId: type: string diff --git a/openapi/components/schemas/internal_accounts/InternalAccountExportRequest.yaml b/openapi/components/schemas/internal_accounts/InternalAccountExportRequest.yaml index a0b48120..7d31e91b 100644 --- a/openapi/components/schemas/internal_accounts/InternalAccountExportRequest.yaml +++ b/openapi/components/schemas/internal_accounts/InternalAccountExportRequest.yaml @@ -3,7 +3,7 @@ description: >- Request body for `POST /internal-accounts/{id}/export`. The `clientPublicKey` is required on both steps of the signed-retry flow. On step 1 Grid binds it into `payloadToSign` so the - subsequent `Grid-Wallet-Signature` commits to the target pubkey; + subsequent stamp in `Grid-Wallet-Signature` commits to the target pubkey; on step 2 the client echoes the same `clientPublicKey` back and Grid uses it to encrypt the wallet credentials returned in the `200` response. diff --git a/openapi/components/schemas/internal_accounts/InternalAccountExportResponse.yaml b/openapi/components/schemas/internal_accounts/InternalAccountExportResponse.yaml index 7e61b98b..0f22875f 100644 --- a/openapi/components/schemas/internal_accounts/InternalAccountExportResponse.yaml +++ b/openapi/components/schemas/internal_accounts/InternalAccountExportResponse.yaml @@ -11,10 +11,22 @@ properties: encryptedWalletCredentials: type: string description: >- - Encrypted wallet mnemonic, sealed to the `clientPublicKey` - supplied on the verify request. Decrypt with the matching - private key, then manage the mnemonic securely — it is the - master key of the self-custodial Embedded Wallet. Encoded as - base58check (same format as - `AuthSession.encryptedSessionSigningKey`). - example: 5KqM8nT3wJz2F9b6H1vRgLpXcA7eD4YuN0sBaE8kPyW5iVfG2xQoZ3MnK9LhU6jT1dS4rCyPbH7oVwX2AgE5uYsNq8fLzR3D7JeM1bVkWcHa9Tp + Encrypted wallet mnemonic, sealed to the `clientPublicKey` from + the request body using HPKE: DHKEM(P-256, HKDF-SHA256) + + HKDF-SHA256 + AES-256-GCM. Decrypt with the matching private + key, then manage the mnemonic securely because it is the master + key of the self-custodial Embedded Wallet. + + The value is a JSON string of the form + `{"version": "v1.0.0", "data": "", "dataSignature": "", + "enclaveQuorumPublic": ""}`. `data` hex-decodes to JSON + `{"encappedPublic": "", "ciphertext": "", + "organizationId": ""}`, where `encappedPublic` is the + uncompressed SEC1 ephemeral public key. `dataSignature` is an + ECDSA-P256-SHA256 signature over the `data` bytes produced by the + issuer key in `enclaveQuorumPublic`; verify before decrypting. + + In sandbox, `dataSignature` and `enclaveQuorumPublic` are empty + strings. Clients should bypass attestation verification when + calling against sandbox. + example: '{"version":"v1.0.0","data":"7b22656e6361707065645075626c6963223a22303433...","dataSignature":"3045022100c9...","enclaveQuorumPublic":"04a1b2c3..."}' diff --git a/openapi/paths/auth/auth_credentials.yaml b/openapi/paths/auth/auth_credentials.yaml index 6536079d..7115240d 100644 --- a/openapi/paths/auth/auth_credentials.yaml +++ b/openapi/paths/auth/auth_credentials.yaml @@ -41,15 +41,16 @@ post: already has one requires a signature from an existing verified credential. Call this endpoint with the new credential's details; if an existing credential is already registered on the internal account - the response is `202` with a `payloadToSign` and a `requestId`. Sign - the payload with the session private key of an existing verified - credential on the same internal account (decrypted client-side from - its `encryptedSessionSigningKey`) and retry the same request with the - signature supplied as the `Grid-Wallet-Signature` header and the - `requestId` echoed back as the `Request-Id` header. The signed retry - returns `201` with the created `AuthMethod`. For `EMAIL_OTP`, the OTP - email is triggered on the signed retry, and the credential must then - be activated via `POST /auth/credentials/{id}/verify`. + the response is `202` with a `payloadToSign` and a `requestId`. + Use the session API keypair of an existing verified credential on + the same internal account (decrypted client-side from its + `encryptedSessionSigningKey`) to build an API-key stamp over + `payloadToSign`, then retry the same request with that full stamp + as the `Grid-Wallet-Signature` header and the `requestId` + echoed back as the `Request-Id` header. The signed retry returns + `201` with the created `AuthMethod`. For `EMAIL_OTP`, the OTP email + is triggered on the signed retry, and the credential must then be + activated via `POST /auth/credentials/{id}/verify`. operationId: createAuthCredential tags: - Embedded Wallet Auth @@ -60,15 +61,15 @@ post: in: header required: false description: >- - Signature over the `payloadToSign` returned in a prior `202` - response, produced with the session private key of an existing - verified authentication credential on the target internal account - and base64-encoded. Required when registering an additional - credential on an internal account that already has one; ignored - when the internal account has no existing credentials. + Full API-key stamp built over the prior `payloadToSign` with + the session API keypair of an existing verified authentication + credential on the target internal account. Required when + registering an additional credential on an internal account that + already has one; ignored when the internal account has no + existing credentials. schema: type: string - example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE= + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzaWduYXR1cmUiOiIzMDQ1MDIyMTAwLi4uIiwic2NoZW1lIjoiUDI1Nl9FQ0RTQV9TSEEyNTYifQ - name: Request-Id in: header required: false @@ -158,13 +159,12 @@ post: '202': description: >- An existing authentication credential is already registered on the - internal account. The response contains a `payloadToSign` that must - be signed with the session private key of an existing verified - credential on the same internal account, along with a `requestId` - that must be echoed back on the retry. The signature is passed as - the `Grid-Wallet-Signature` header and the `requestId` as the - `Request-Id` header on a retry of this request to complete - registration. + internal account. The response contains `payloadToSign` plus a + `requestId`. Build an API-key stamp over `payloadToSign` with + the session API keypair of an existing verified credential on + the same internal account, then send that full stamp as + `Grid-Wallet-Signature` and echo `requestId` as `Request-Id` + on the retry. content: application/json: schema: diff --git a/openapi/paths/auth/auth_credentials_{id}.yaml b/openapi/paths/auth/auth_credentials_{id}.yaml index 3305166e..a763eddc 100644 --- a/openapi/paths/auth/auth_credentials_{id}.yaml +++ b/openapi/paths/auth/auth_credentials_{id}.yaml @@ -13,12 +13,12 @@ delete: is `202` with a `payloadToSign`, `requestId`, and `expiresAt`. - 2. Sign the `payloadToSign` with the session private key of an - existing verified credential on the same internal account — other - than the one being revoked — and retry the same `DELETE` request - with the signature supplied as the `Grid-Wallet-Signature` header - and the `requestId` echoed back as the `Request-Id` header. The - signed retry returns `204`. + 2. Use the session API keypair of an existing verified credential + on the same internal account — other than the one being revoked — + to build an API-key stamp over `payloadToSign`, then retry the + same `DELETE` request with that full stamp as the + `Grid-Wallet-Signature` header and the `requestId` echoed back as + the `Request-Id` header. The signed retry returns `204`. The account must retain at least one authentication credential; an @@ -43,14 +43,14 @@ delete: in: header required: false description: >- - Signature over the `payloadToSign` returned in a prior `202` - response, produced with the session private key of an existing - verified authentication credential on the same internal account - (other than the one being revoked) and base64-encoded. Required - on the signed retry; ignored on the initial call. + Full API-key stamp built over the prior `payloadToSign` with + the session API keypair of an existing verified authentication + credential on the same internal account (other than the one + being revoked). Required on the signed retry; ignored on the + initial call. schema: type: string - example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE= + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzaWduYXR1cmUiOiIzMDQ1MDIyMTAwLi4uIiwic2NoZW1lIjoiUDI1Nl9FQ0RTQV9TSEEyNTYifQ - name: Request-Id in: header required: false @@ -65,11 +65,11 @@ delete: responses: '202': description: >- - Challenge issued. The response contains a `payloadToSign` that - must be signed with the session private key of an existing - verified credential on the same internal account (other than - the one being revoked), along with a `requestId` that must be - echoed back on the retry. + Challenge issued. The response contains `payloadToSign` plus a + `requestId`. Build an API-key stamp over `payloadToSign` with + the session API keypair of an existing verified credential on + the same internal account (other than the one being revoked), + then echo `requestId` on the retry. content: application/json: schema: diff --git a/openapi/paths/auth/auth_sessions_{id}.yaml b/openapi/paths/auth/auth_sessions_{id}.yaml index ab075266..6653b479 100644 --- a/openapi/paths/auth/auth_sessions_{id}.yaml +++ b/openapi/paths/auth/auth_sessions_{id}.yaml @@ -9,12 +9,12 @@ delete: is `202` with a `payloadToSign`, `requestId`, and `expiresAt`. - 2. Sign the `payloadToSign` with the session private key of a - verified session on the same internal account (this can be the - session being revoked, for self-logout) and retry the same `DELETE` - request with the signature as the `Grid-Wallet-Signature` header - and the `requestId` echoed back as the `Request-Id` header. The - signed retry returns `204`. + 2. Use the session API keypair of a verified session on the same + internal account (this can be the session being revoked, for + self-logout) to build an API-key stamp over `payloadToSign`, + then retry the same `DELETE` request with that full stamp as the + `Grid-Wallet-Signature` header and the `requestId` echoed back as + the `Request-Id` header. The signed retry returns `204`. operationId: revokeAuthSession tags: - Embedded Wallet Auth @@ -31,13 +31,13 @@ delete: in: header required: false description: >- - Signature over the `payloadToSign` returned in a prior `202` - response, produced with the session private key of a verified - session on the same internal account and base64-encoded. - Required on the signed retry; ignored on the initial call. + Full API-key stamp built over the prior `payloadToSign` with + the session API keypair of a verified session on the same + internal account. Required on the signed retry; ignored on the + initial call. schema: type: string - example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE= + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzaWduYXR1cmUiOiIzMDQ1MDIyMTAwLi4uIiwic2NoZW1lIjoiUDI1Nl9FQ0RTQV9TSEEyNTYifQ - name: Request-Id in: header required: false @@ -52,10 +52,10 @@ delete: responses: '202': description: >- - Challenge issued. The response contains a `payloadToSign` that - must be signed with the session private key of a verified - session on the same internal account, along with a `requestId` - that must be echoed back on the retry. + Challenge issued. The response contains `payloadToSign` plus a + `requestId`. Build an API-key stamp over `payloadToSign` with + the session API keypair of a verified session on the same + internal account, then echo `requestId` on the retry. content: application/json: schema: diff --git a/openapi/paths/internal_accounts/internal_accounts_{id}_export.yaml b/openapi/paths/internal_accounts/internal_accounts_{id}_export.yaml index a21bd84d..ff811935 100644 --- a/openapi/paths/internal_accounts/internal_accounts_{id}_export.yaml +++ b/openapi/paths/internal_accounts/internal_accounts_{id}_export.yaml @@ -13,16 +13,17 @@ post: 1. Call `POST /internal-accounts/{id}/export` with the request body `{ "clientPublicKey": "..." }` and no signature headers. Grid binds the `clientPublicKey` into the `payloadToSign` it returns, so the - subsequent `Grid-Wallet-Signature` commits to the target encryption - key. The response is `202` with `payloadToSign`, `requestId`, and - `expiresAt`. + subsequent stamp in `Grid-Wallet-Signature` commits to the target + encryption key. The response is `202` with `payloadToSign`, + `requestId`, and `expiresAt`. - 2. Sign the `payloadToSign` with the session private key of a - verified authentication credential on the same internal account - and retry with the signature as the `Grid-Wallet-Signature` header - and the `requestId` echoed back as the `Request-Id` header. The - retry body must carry the **same** `clientPublicKey` submitted in + 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** + `clientPublicKey` submitted in step 1 — Grid rejects the retry with `401` if it disagrees with what was bound into `payloadToSign`. The signed retry returns `200` with `encryptedWalletCredentials`, which the client decrypts @@ -50,14 +51,13 @@ post: in: header required: false description: >- - Signature over the `payloadToSign` returned in a prior `202` - response, produced with the session private key of a verified - authentication credential on the target internal account and - base64-encoded. Required on the signed retry; ignored on the - initial call. + 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: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE= + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzaWduYXR1cmUiOiIzMDQ1MDIyMTAwLi4uIiwic2NoZW1lIjoiUDI1Nl9FQ0RTQV9TSEEyNTYifQ - name: Request-Id in: header required: false @@ -89,11 +89,10 @@ post: $ref: ../../components/schemas/internal_accounts/InternalAccountExportResponse.yaml '202': description: >- - Challenge issued. The response contains a `payloadToSign` (which - binds the submitted `clientPublicKey`) that must be signed with - the session private key of a verified authentication credential - on the target internal account, along with a `requestId` that - must be echoed back on the retry. + Challenge issued. The response contains `payloadToSign` (which + binds the submitted `clientPublicKey`) 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: