From 836281f9657348d302d9447cd5b96d022f7ca639 Mon Sep 17 00:00:00 2001 From: carsonp6 Date: Wed, 19 Aug 2026 23:35:14 -0700 Subject: [PATCH 1/3] feat: extend WalletOperationProcessing to verify, session-refresh, and contact-update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rounds out the 200/PROCESSING contract to the remaining Turnkey sync-SM endpoints (SP-3579), matching the reshape stack's actual return sites: - POST /auth/credentials/{id}/verify (VERIFY_OTP/OTP_LOGIN/OAUTH_LOGIN/ STAMP_LOGIN): 200 widened to a new AuthCredentialVerifyResponse wrapper — anyOf: [AuthSession, WalletOperationProcessing]. - POST /auth/sessions/{id}/refresh (CREATE_READ_WRITE_SESSION): new 200 sibling to the existing 201, single $ref to WalletOperationProcessing (no union needed — 201 is a distinct success code, same shape as the add-credential endpoint from the prior commit). - PATCH /customers/{customerId} (contact-update: email/phone + tied OTP credentials): 200 widened to a new CustomerUpdateResponse wrapper — anyOf: [CustomerOneOf, WalletOperationProcessing]. CustomerOneOf keeps its own discriminator untouched; get/delete on this resource still return CustomerOneOf directly, unaffected. Same anyOf-over-oneOf reasoning as the challenge endpoint: none of AuthSession or CustomerOneOf share a discriminator field with WalletOperationProcessing's `status`, so a discriminated oneOf doesn't fit and this repo's spectral rules require one on every oneOf. Existing 202 responses on these endpoints are untouched — they signal a different state (client must sign and resubmit a challenge), not "still settling." No inconsistency found across the reshape stack's five endpoint groups (this commit's three plus the prior commit's add-credential and challenge): all uniformly answer still-in-flight activity with 200/PROCESSING, never 202. Co-Authored-By: Claude Opus 4.8 --- mintlify/openapi.yaml | 50 +++++++++++-------- openapi.yaml | 50 +++++++++++-------- .../customers/CustomerUpdateResponse.yaml | 13 +++++ .../customers/customers_{customerId}.yaml | 8 ++- 4 files changed, 75 insertions(+), 46 deletions(-) create mode 100644 openapi/components/schemas/customers/CustomerUpdateResponse.yaml diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 378972751..981f90521 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -740,11 +740,11 @@ paths: phoneNumber: '+14155559876' responses: '200': - description: Customer updated successfully. For Embedded Wallet email or SMS auth phone updates, this is returned only on the signed retry after the customer contact field and all tied matching OTP credentials have been updated. + description: Customer updated successfully. For Embedded Wallet email or SMS auth phone updates, this is returned only on the signed retry after the customer contact field and all tied matching OTP credentials have been updated. When the contact update's underlying wallet-provider activity is still in flight, the body is instead a `WalletOperationProcessing` carrying a `PROCESSING` status — re-send the byte-identical signed retry until it settles; the backend also reconciles it to terminal on its own. content: application/json: schema: - $ref: '#/components/schemas/CustomerOneOf' + $ref: '#/components/schemas/CustomerUpdateResponse' '202': description: Challenge issued for an Embedded Wallet email or SMS auth phone update. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair from a verified authentication credential on one of the customer's tied Embedded Wallets, then retry the same request with `Grid-Wallet-Signature` and `Request-Id`. content: @@ -13840,6 +13840,32 @@ components: mapping: INDIVIDUAL: '#/components/schemas/IndividualCustomerUpdateRequest' BUSINESS: '#/components/schemas/BusinessCustomerUpdateRequest' + WalletOperationProcessing: + title: Wallet Operation Processing + description: '`200` response returned by an Embedded Wallet operation that the wallet provider has accepted but not yet settled — a consensus- or approval-gated activity that is still in flight. It is not an error and needs no client action beyond patience: the backend reconciles the operation to its terminal state on its own. The client MAY re-send the byte-identical request to converge sooner; the request is idempotent and returns the settled success response once the operation completes.' + type: object + required: + - status + properties: + status: + type: string + enum: + - PROCESSING + description: Always `PROCESSING`. Marks a still-in-flight operation whose terminal result is not yet available. + example: PROCESSING + message: + type: string + description: Human-readable explanation that the operation is still being processed and the same request may be retried. + example: This login is still being processed. Retry the same request in a moment. + example: + status: PROCESSING + message: This login is still being processed. Retry the same request in a moment. + CustomerUpdateResponse: + title: Customer Update Response + description: 'Response body for `PATCH /customers/{customerId}`. Normally a `CustomerOneOf` — the updated customer. When the update is an Embedded Wallet email or SMS auth phone change and the underlying wallet-provider activity is still in flight on the signed retry, this is instead a `WalletOperationProcessing` body with `status: "PROCESSING"` — the client re-sends the byte-identical signed retry until it settles; the backend also reconciles the activity to terminal on its own.' + anyOf: + - $ref: '#/components/schemas/CustomerOneOf' + - $ref: '#/components/schemas/WalletOperationProcessing' SignedRequestChallenge: title: Signed Request Challenge type: object @@ -23527,26 +23553,6 @@ components: SMS_OTP: '#/components/schemas/SmsOtpCredentialCreateRequest' OAUTH: '#/components/schemas/OauthCredentialCreateRequest' PASSKEY: '#/components/schemas/PasskeyCredentialCreateRequest' - WalletOperationProcessing: - title: Wallet Operation Processing - description: '`200` response returned by an Embedded Wallet operation that the wallet provider has accepted but not yet settled — a consensus- or approval-gated activity that is still in flight. It is not an error and needs no client action beyond patience: the backend reconciles the operation to its terminal state on its own. The client MAY re-send the byte-identical request to converge sooner; the request is idempotent and returns the settled success response once the operation completes.' - type: object - required: - - status - properties: - status: - type: string - enum: - - PROCESSING - description: Always `PROCESSING`. Marks a still-in-flight operation whose terminal result is not yet available. - example: PROCESSING - message: - type: string - description: Human-readable explanation that the operation is still being processed and the same request may be retried. - example: This login is still being processed. Retry the same request in a moment. - example: - status: PROCESSING - message: This login is still being processed. Retry the same request in a moment. AuthMethodResponse: title: Auth Method Response description: |- diff --git a/openapi.yaml b/openapi.yaml index 378972751..981f90521 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -740,11 +740,11 @@ paths: phoneNumber: '+14155559876' responses: '200': - description: Customer updated successfully. For Embedded Wallet email or SMS auth phone updates, this is returned only on the signed retry after the customer contact field and all tied matching OTP credentials have been updated. + description: Customer updated successfully. For Embedded Wallet email or SMS auth phone updates, this is returned only on the signed retry after the customer contact field and all tied matching OTP credentials have been updated. When the contact update's underlying wallet-provider activity is still in flight, the body is instead a `WalletOperationProcessing` carrying a `PROCESSING` status — re-send the byte-identical signed retry until it settles; the backend also reconciles it to terminal on its own. content: application/json: schema: - $ref: '#/components/schemas/CustomerOneOf' + $ref: '#/components/schemas/CustomerUpdateResponse' '202': description: Challenge issued for an Embedded Wallet email or SMS auth phone update. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair from a verified authentication credential on one of the customer's tied Embedded Wallets, then retry the same request with `Grid-Wallet-Signature` and `Request-Id`. content: @@ -13840,6 +13840,32 @@ components: mapping: INDIVIDUAL: '#/components/schemas/IndividualCustomerUpdateRequest' BUSINESS: '#/components/schemas/BusinessCustomerUpdateRequest' + WalletOperationProcessing: + title: Wallet Operation Processing + description: '`200` response returned by an Embedded Wallet operation that the wallet provider has accepted but not yet settled — a consensus- or approval-gated activity that is still in flight. It is not an error and needs no client action beyond patience: the backend reconciles the operation to its terminal state on its own. The client MAY re-send the byte-identical request to converge sooner; the request is idempotent and returns the settled success response once the operation completes.' + type: object + required: + - status + properties: + status: + type: string + enum: + - PROCESSING + description: Always `PROCESSING`. Marks a still-in-flight operation whose terminal result is not yet available. + example: PROCESSING + message: + type: string + description: Human-readable explanation that the operation is still being processed and the same request may be retried. + example: This login is still being processed. Retry the same request in a moment. + example: + status: PROCESSING + message: This login is still being processed. Retry the same request in a moment. + CustomerUpdateResponse: + title: Customer Update Response + description: 'Response body for `PATCH /customers/{customerId}`. Normally a `CustomerOneOf` — the updated customer. When the update is an Embedded Wallet email or SMS auth phone change and the underlying wallet-provider activity is still in flight on the signed retry, this is instead a `WalletOperationProcessing` body with `status: "PROCESSING"` — the client re-sends the byte-identical signed retry until it settles; the backend also reconciles the activity to terminal on its own.' + anyOf: + - $ref: '#/components/schemas/CustomerOneOf' + - $ref: '#/components/schemas/WalletOperationProcessing' SignedRequestChallenge: title: Signed Request Challenge type: object @@ -23527,26 +23553,6 @@ components: SMS_OTP: '#/components/schemas/SmsOtpCredentialCreateRequest' OAUTH: '#/components/schemas/OauthCredentialCreateRequest' PASSKEY: '#/components/schemas/PasskeyCredentialCreateRequest' - WalletOperationProcessing: - title: Wallet Operation Processing - description: '`200` response returned by an Embedded Wallet operation that the wallet provider has accepted but not yet settled — a consensus- or approval-gated activity that is still in flight. It is not an error and needs no client action beyond patience: the backend reconciles the operation to its terminal state on its own. The client MAY re-send the byte-identical request to converge sooner; the request is idempotent and returns the settled success response once the operation completes.' - type: object - required: - - status - properties: - status: - type: string - enum: - - PROCESSING - description: Always `PROCESSING`. Marks a still-in-flight operation whose terminal result is not yet available. - example: PROCESSING - message: - type: string - description: Human-readable explanation that the operation is still being processed and the same request may be retried. - example: This login is still being processed. Retry the same request in a moment. - example: - status: PROCESSING - message: This login is still being processed. Retry the same request in a moment. AuthMethodResponse: title: Auth Method Response description: |- diff --git a/openapi/components/schemas/customers/CustomerUpdateResponse.yaml b/openapi/components/schemas/customers/CustomerUpdateResponse.yaml new file mode 100644 index 000000000..4896afb97 --- /dev/null +++ b/openapi/components/schemas/customers/CustomerUpdateResponse.yaml @@ -0,0 +1,13 @@ +title: Customer Update Response +description: >- + Response body for `PATCH /customers/{customerId}`. Normally a + `CustomerOneOf` — the updated customer. When the update is an + Embedded Wallet email or SMS auth phone change and the underlying + wallet-provider activity is still in flight on the signed retry, this + is instead a `WalletOperationProcessing` body with `status: + "PROCESSING"` — the client re-sends the byte-identical signed retry + until it settles; the backend also reconciles the activity to + terminal on its own. +anyOf: + - $ref: ./CustomerOneOf.yaml + - $ref: ../common/WalletOperationProcessing.yaml diff --git a/openapi/paths/customers/customers_{customerId}.yaml b/openapi/paths/customers/customers_{customerId}.yaml index 7f72bd663..05fa1f8ca 100644 --- a/openapi/paths/customers/customers_{customerId}.yaml +++ b/openapi/paths/customers/customers_{customerId}.yaml @@ -168,11 +168,15 @@ patch: Customer updated successfully. For Embedded Wallet email or SMS auth phone updates, this is returned only on the signed retry after the customer contact field and all tied matching OTP credentials have been - updated. + updated. When the contact update's underlying wallet-provider activity + is still in flight, the body is instead a `WalletOperationProcessing` + carrying a `PROCESSING` status — re-send the byte-identical signed + retry until it settles; the backend also reconciles it to terminal on + its own. content: application/json: schema: - $ref: ../../components/schemas/customers/CustomerOneOf.yaml + $ref: ../../components/schemas/customers/CustomerUpdateResponse.yaml '202': description: >- Challenge issued for an Embedded Wallet email or SMS auth phone update. From 121f36f7f5d697ed55aa7a1fa62765cf63dab1cf Mon Sep 17 00:00:00 2001 From: carsonp6 Date: Thu, 20 Aug 2026 18:01:47 -0700 Subject: [PATCH 2/3] =?UTF-8?q?Drop=20the=20PATCH-customers=20widening=20?= =?UTF-8?q?=E2=80=94=20contact=20updates=20are=20moving=20to=20dedicated?= =?UTF-8?q?=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the WalletOperationProcessing additions from PATCH /customers/{customerId} (the new CustomerUpdateResponse schema, the 200-response description, and the three supporting mentions in the auth guide) so this PR only covers the six auth-credential and auth-session endpoints. --- mintlify/openapi.yaml | 50 ++++++++----------- openapi.yaml | 50 ++++++++----------- .../customers/CustomerUpdateResponse.yaml | 13 ----- .../customers/customers_{customerId}.yaml | 8 +-- 4 files changed, 46 insertions(+), 75 deletions(-) delete mode 100644 openapi/components/schemas/customers/CustomerUpdateResponse.yaml diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 981f90521..378972751 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -740,11 +740,11 @@ paths: phoneNumber: '+14155559876' responses: '200': - description: Customer updated successfully. For Embedded Wallet email or SMS auth phone updates, this is returned only on the signed retry after the customer contact field and all tied matching OTP credentials have been updated. When the contact update's underlying wallet-provider activity is still in flight, the body is instead a `WalletOperationProcessing` carrying a `PROCESSING` status — re-send the byte-identical signed retry until it settles; the backend also reconciles it to terminal on its own. + description: Customer updated successfully. For Embedded Wallet email or SMS auth phone updates, this is returned only on the signed retry after the customer contact field and all tied matching OTP credentials have been updated. content: application/json: schema: - $ref: '#/components/schemas/CustomerUpdateResponse' + $ref: '#/components/schemas/CustomerOneOf' '202': description: Challenge issued for an Embedded Wallet email or SMS auth phone update. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair from a verified authentication credential on one of the customer's tied Embedded Wallets, then retry the same request with `Grid-Wallet-Signature` and `Request-Id`. content: @@ -13840,32 +13840,6 @@ components: mapping: INDIVIDUAL: '#/components/schemas/IndividualCustomerUpdateRequest' BUSINESS: '#/components/schemas/BusinessCustomerUpdateRequest' - WalletOperationProcessing: - title: Wallet Operation Processing - description: '`200` response returned by an Embedded Wallet operation that the wallet provider has accepted but not yet settled — a consensus- or approval-gated activity that is still in flight. It is not an error and needs no client action beyond patience: the backend reconciles the operation to its terminal state on its own. The client MAY re-send the byte-identical request to converge sooner; the request is idempotent and returns the settled success response once the operation completes.' - type: object - required: - - status - properties: - status: - type: string - enum: - - PROCESSING - description: Always `PROCESSING`. Marks a still-in-flight operation whose terminal result is not yet available. - example: PROCESSING - message: - type: string - description: Human-readable explanation that the operation is still being processed and the same request may be retried. - example: This login is still being processed. Retry the same request in a moment. - example: - status: PROCESSING - message: This login is still being processed. Retry the same request in a moment. - CustomerUpdateResponse: - title: Customer Update Response - description: 'Response body for `PATCH /customers/{customerId}`. Normally a `CustomerOneOf` — the updated customer. When the update is an Embedded Wallet email or SMS auth phone change and the underlying wallet-provider activity is still in flight on the signed retry, this is instead a `WalletOperationProcessing` body with `status: "PROCESSING"` — the client re-sends the byte-identical signed retry until it settles; the backend also reconciles the activity to terminal on its own.' - anyOf: - - $ref: '#/components/schemas/CustomerOneOf' - - $ref: '#/components/schemas/WalletOperationProcessing' SignedRequestChallenge: title: Signed Request Challenge type: object @@ -23553,6 +23527,26 @@ components: SMS_OTP: '#/components/schemas/SmsOtpCredentialCreateRequest' OAUTH: '#/components/schemas/OauthCredentialCreateRequest' PASSKEY: '#/components/schemas/PasskeyCredentialCreateRequest' + WalletOperationProcessing: + title: Wallet Operation Processing + description: '`200` response returned by an Embedded Wallet operation that the wallet provider has accepted but not yet settled — a consensus- or approval-gated activity that is still in flight. It is not an error and needs no client action beyond patience: the backend reconciles the operation to its terminal state on its own. The client MAY re-send the byte-identical request to converge sooner; the request is idempotent and returns the settled success response once the operation completes.' + type: object + required: + - status + properties: + status: + type: string + enum: + - PROCESSING + description: Always `PROCESSING`. Marks a still-in-flight operation whose terminal result is not yet available. + example: PROCESSING + message: + type: string + description: Human-readable explanation that the operation is still being processed and the same request may be retried. + example: This login is still being processed. Retry the same request in a moment. + example: + status: PROCESSING + message: This login is still being processed. Retry the same request in a moment. AuthMethodResponse: title: Auth Method Response description: |- diff --git a/openapi.yaml b/openapi.yaml index 981f90521..378972751 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -740,11 +740,11 @@ paths: phoneNumber: '+14155559876' responses: '200': - description: Customer updated successfully. For Embedded Wallet email or SMS auth phone updates, this is returned only on the signed retry after the customer contact field and all tied matching OTP credentials have been updated. When the contact update's underlying wallet-provider activity is still in flight, the body is instead a `WalletOperationProcessing` carrying a `PROCESSING` status — re-send the byte-identical signed retry until it settles; the backend also reconciles it to terminal on its own. + description: Customer updated successfully. For Embedded Wallet email or SMS auth phone updates, this is returned only on the signed retry after the customer contact field and all tied matching OTP credentials have been updated. content: application/json: schema: - $ref: '#/components/schemas/CustomerUpdateResponse' + $ref: '#/components/schemas/CustomerOneOf' '202': description: Challenge issued for an Embedded Wallet email or SMS auth phone update. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair from a verified authentication credential on one of the customer's tied Embedded Wallets, then retry the same request with `Grid-Wallet-Signature` and `Request-Id`. content: @@ -13840,32 +13840,6 @@ components: mapping: INDIVIDUAL: '#/components/schemas/IndividualCustomerUpdateRequest' BUSINESS: '#/components/schemas/BusinessCustomerUpdateRequest' - WalletOperationProcessing: - title: Wallet Operation Processing - description: '`200` response returned by an Embedded Wallet operation that the wallet provider has accepted but not yet settled — a consensus- or approval-gated activity that is still in flight. It is not an error and needs no client action beyond patience: the backend reconciles the operation to its terminal state on its own. The client MAY re-send the byte-identical request to converge sooner; the request is idempotent and returns the settled success response once the operation completes.' - type: object - required: - - status - properties: - status: - type: string - enum: - - PROCESSING - description: Always `PROCESSING`. Marks a still-in-flight operation whose terminal result is not yet available. - example: PROCESSING - message: - type: string - description: Human-readable explanation that the operation is still being processed and the same request may be retried. - example: This login is still being processed. Retry the same request in a moment. - example: - status: PROCESSING - message: This login is still being processed. Retry the same request in a moment. - CustomerUpdateResponse: - title: Customer Update Response - description: 'Response body for `PATCH /customers/{customerId}`. Normally a `CustomerOneOf` — the updated customer. When the update is an Embedded Wallet email or SMS auth phone change and the underlying wallet-provider activity is still in flight on the signed retry, this is instead a `WalletOperationProcessing` body with `status: "PROCESSING"` — the client re-sends the byte-identical signed retry until it settles; the backend also reconciles the activity to terminal on its own.' - anyOf: - - $ref: '#/components/schemas/CustomerOneOf' - - $ref: '#/components/schemas/WalletOperationProcessing' SignedRequestChallenge: title: Signed Request Challenge type: object @@ -23553,6 +23527,26 @@ components: SMS_OTP: '#/components/schemas/SmsOtpCredentialCreateRequest' OAUTH: '#/components/schemas/OauthCredentialCreateRequest' PASSKEY: '#/components/schemas/PasskeyCredentialCreateRequest' + WalletOperationProcessing: + title: Wallet Operation Processing + description: '`200` response returned by an Embedded Wallet operation that the wallet provider has accepted but not yet settled — a consensus- or approval-gated activity that is still in flight. It is not an error and needs no client action beyond patience: the backend reconciles the operation to its terminal state on its own. The client MAY re-send the byte-identical request to converge sooner; the request is idempotent and returns the settled success response once the operation completes.' + type: object + required: + - status + properties: + status: + type: string + enum: + - PROCESSING + description: Always `PROCESSING`. Marks a still-in-flight operation whose terminal result is not yet available. + example: PROCESSING + message: + type: string + description: Human-readable explanation that the operation is still being processed and the same request may be retried. + example: This login is still being processed. Retry the same request in a moment. + example: + status: PROCESSING + message: This login is still being processed. Retry the same request in a moment. AuthMethodResponse: title: Auth Method Response description: |- diff --git a/openapi/components/schemas/customers/CustomerUpdateResponse.yaml b/openapi/components/schemas/customers/CustomerUpdateResponse.yaml deleted file mode 100644 index 4896afb97..000000000 --- a/openapi/components/schemas/customers/CustomerUpdateResponse.yaml +++ /dev/null @@ -1,13 +0,0 @@ -title: Customer Update Response -description: >- - Response body for `PATCH /customers/{customerId}`. Normally a - `CustomerOneOf` — the updated customer. When the update is an - Embedded Wallet email or SMS auth phone change and the underlying - wallet-provider activity is still in flight on the signed retry, this - is instead a `WalletOperationProcessing` body with `status: - "PROCESSING"` — the client re-sends the byte-identical signed retry - until it settles; the backend also reconciles the activity to - terminal on its own. -anyOf: - - $ref: ./CustomerOneOf.yaml - - $ref: ../common/WalletOperationProcessing.yaml diff --git a/openapi/paths/customers/customers_{customerId}.yaml b/openapi/paths/customers/customers_{customerId}.yaml index 05fa1f8ca..7f72bd663 100644 --- a/openapi/paths/customers/customers_{customerId}.yaml +++ b/openapi/paths/customers/customers_{customerId}.yaml @@ -168,15 +168,11 @@ patch: Customer updated successfully. For Embedded Wallet email or SMS auth phone updates, this is returned only on the signed retry after the customer contact field and all tied matching OTP credentials have been - updated. When the contact update's underlying wallet-provider activity - is still in flight, the body is instead a `WalletOperationProcessing` - carrying a `PROCESSING` status — re-send the byte-identical signed - retry until it settles; the backend also reconciles it to terminal on - its own. + updated. content: application/json: schema: - $ref: ../../components/schemas/customers/CustomerUpdateResponse.yaml + $ref: ../../components/schemas/customers/CustomerOneOf.yaml '202': description: >- Challenge issued for an Embedded Wallet email or SMS auth phone update. From 104bd0fa7b2e8b635f2ed45f20a43b67330438e7 Mon Sep 17 00:00:00 2001 From: carsonp6 Date: Fri, 21 Aug 2026 13:50:01 -0700 Subject: [PATCH 3/3] feat: add 200/PROCESSING to the wallet export endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit POST /internal-accounts/{internalAccountId}/export (exportInternalAccount, EXPORT_WALLET) is the last signed-retry endpoint left off the 200/PROCESSING contract (SP-3579). Its wallet-provider activity is only optimistically synchronous like the rest, but the spec documented a single terminal 200 — so an approval- or consensus-gated export had no documented shape at all, and the undocumented one it used to answer (202 + operationId) was a dead end: no fetch endpoint exists, the encrypted credentials are deliberately never stored, and the challenge had already been consumed, so every re-send was a 401. The 200 widens to a new InternalAccountExportResult wrapper — anyOf: [InternalAccountExportResponse, WalletOperationProcessing] — the competing-200 case, same shape and same anyOf-over-oneOf reasoning as the verify endpoint (neither member shares a discriminator field with WalletOperationProcessing's `status`, and this repo's spectral rules require a discriminator on every oneOf). The existing 202 (SignedRequestChallenge) is untouched: that is the sign-and-resubmit leg, a different state. Export differs from the other six endpoints in one integrator-visible way, so the prose says it: because the credentials are never stored server-side, re-sending the signed retry is the only way to receive them. Grid's own reconciliation settles the operation but has nothing to hand back, so the "stop retrying and check back later" advice that holds for the others does not hold here — the completion webhook tells the client when a re-send will succeed. The challenge is consumed only by the attempt that actually returns credentials, so a PROCESSING response leaves it usable and a delivered export still refuses a second one. Docs: export joins the still-processing endpoint table and the shares-its-200 callout in authentication.mdx, and the export walkthrough gets the branch in its sequence diagram plus the pointer every other flow already has. The bundled openapi.yaml moves WalletOperationProcessing earlier in components — generator ordering follows first reference, and export now references it before the auth paths do. Sequencing: companion to the webdev export PR that makes the endpoint behave this way (branch grid-turnkey-export-processing). Behind Knob.GRID_TURNKEY_ASYNC_EXPORT, default off. Co-Authored-By: Claude Fable 5 --- mintlify/openapi.yaml | 52 +++++++++++-------- .../global-accounts/authentication.mdx | 5 +- .../global-accounts/exporting-wallet.mdx | 6 +++ openapi.yaml | 52 +++++++++++-------- .../InternalAccountExportResult.yaml | 14 +++++ .../internal_accounts_{id}_export.yaml | 25 ++++++++- 6 files changed, 106 insertions(+), 48 deletions(-) create mode 100644 openapi/components/schemas/internal_accounts/InternalAccountExportResult.yaml diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 378972751..bd05bebe7 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -5842,6 +5842,8 @@ paths: 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 export may not settle within that request: an approval- or consensus-gated wallet-provider activity answers `200` with `status: "PROCESSING"` instead. The credentials are never stored server-side, so collecting them is the client's job — re-send the byte-identical signed retry (same headers, same body) until it returns `encryptedWalletCredentials`. The `Request-Id` challenge stays usable until an attempt actually delivers them, so a `PROCESSING` response never burns it; a delivered export does, and a later re-send is then rejected with `401`. Subscribe to `wallet_operation.completed` to learn when re-sending will succeed rather than polling blind. + 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 tags: @@ -5882,11 +5884,11 @@ paths: clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 responses: '200': - description: Signed retry accepted. Returns the encrypted wallet credentials. + description: 'Signed retry accepted. Returns the encrypted wallet credentials (`InternalAccountExportResponse`); or, when the underlying wallet-provider activity is still in flight, a `WalletOperationProcessing` body with `status: "PROCESSING"` — the credentials are never stored server-side, so the client re-sends the byte-identical signed retry to collect them once the activity settles. The challenge stays usable until an attempt actually returns credentials.' content: application/json: schema: - $ref: '#/components/schemas/InternalAccountExportResponse' + $ref: '#/components/schemas/InternalAccountExportResult' '202': 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: @@ -23329,6 +23331,32 @@ components: 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..."}' + WalletOperationProcessing: + title: Wallet Operation Processing + description: '`200` response returned by an Embedded Wallet operation that the wallet provider has accepted but not yet settled — a consensus- or approval-gated activity that is still in flight. It is not an error and needs no client action beyond patience: the backend reconciles the operation to its terminal state on its own. The client MAY re-send the byte-identical request to converge sooner; the request is idempotent and returns the settled success response once the operation completes.' + type: object + required: + - status + properties: + status: + type: string + enum: + - PROCESSING + description: Always `PROCESSING`. Marks a still-in-flight operation whose terminal result is not yet available. + example: PROCESSING + message: + type: string + description: Human-readable explanation that the operation is still being processed and the same request may be retried. + example: This login is still being processed. Retry the same request in a moment. + example: + status: PROCESSING + message: This login is still being processed. Retry the same request in a moment. + InternalAccountExportResult: + title: Internal Account Export Result + description: 'Response body for the signed retry of `POST /internal-accounts/{internalAccountId}/export`. Normally an `InternalAccountExportResponse` — the encrypted wallet credentials. When the underlying wallet-provider activity is still in flight, this is instead a `WalletOperationProcessing` body with `status: "PROCESSING"`. The credentials are never stored server-side, so re-sending the byte-identical signed retry is how they are collected: the re-send returns them as soon as the activity has settled, and the challenge is consumed only by the attempt that delivers them.' + anyOf: + - $ref: '#/components/schemas/InternalAccountExportResponse' + - $ref: '#/components/schemas/WalletOperationProcessing' AuthMethodType: type: string enum: @@ -23527,26 +23555,6 @@ components: SMS_OTP: '#/components/schemas/SmsOtpCredentialCreateRequest' OAUTH: '#/components/schemas/OauthCredentialCreateRequest' PASSKEY: '#/components/schemas/PasskeyCredentialCreateRequest' - WalletOperationProcessing: - title: Wallet Operation Processing - description: '`200` response returned by an Embedded Wallet operation that the wallet provider has accepted but not yet settled — a consensus- or approval-gated activity that is still in flight. It is not an error and needs no client action beyond patience: the backend reconciles the operation to its terminal state on its own. The client MAY re-send the byte-identical request to converge sooner; the request is idempotent and returns the settled success response once the operation completes.' - type: object - required: - - status - properties: - status: - type: string - enum: - - PROCESSING - description: Always `PROCESSING`. Marks a still-in-flight operation whose terminal result is not yet available. - example: PROCESSING - message: - type: string - description: Human-readable explanation that the operation is still being processed and the same request may be retried. - example: This login is still being processed. Retry the same request in a moment. - example: - status: PROCESSING - message: This login is still being processed. Retry the same request in a moment. AuthMethodResponse: title: Auth Method Response description: |- diff --git a/mintlify/snippets/global-accounts/authentication.mdx b/mintlify/snippets/global-accounts/authentication.mdx index f6813890e..c7d6bc54b 100644 --- a/mintlify/snippets/global-accounts/authentication.mdx +++ b/mintlify/snippets/global-accounts/authentication.mdx @@ -702,15 +702,16 @@ Every signed-retry endpoint can return it on the retry step: | `POST /auth/sessions/{id}/refresh` | Refresh a session | | `DELETE /auth/credentials/{id}` | Revoke a credential | | `DELETE /auth/sessions/{id}` | Revoke a session | +| `POST /internal-accounts/{id}/export` | Export wallet credentials | To handle it: 1. **Re-send the exact same signed request** — identical body, `Grid-Wallet-Signature`, and `Request-Id`. Don't regenerate the stamp or start a fresh request; Grid correlates the retry with the same underlying operation and returns its real outcome once the provider settles, rather than starting a second one. 2. **Show a pending state in your UI** while you retry — "removing…" for a credential revoke, "signing in…" for a login — instead of treating `PROCESSING` as an error or leaving the caller blocked with no feedback. -3. **Keep retrying with backoff** until you get a terminal response — the success code above, or an error. Grid also reconciles the operation to its terminal state on its own, so even a client that stops retrying and checks back later (for example via `GET /auth/credentials` or `GET /auth/sessions`) will see the settled result. +3. **Keep retrying with backoff** until you get a terminal response — the success code above, or an error. Grid also reconciles the operation to its terminal state on its own, so even a client that stops retrying and checks back later (for example via `GET /auth/credentials` or `GET /auth/sessions`) will see the settled result. **Wallet export is the exception**: Grid never stores the encrypted credentials, so re-sending the signed retry is the only way to receive them. Grid's own reconciliation settles the operation but has nothing to hand back, and the export challenge expires like any other (typically 5 minutes) — if it lapses, start a fresh export. - `verify` and `challenge` already use `200` for their terminal success body (`AuthSession` or the challenge response). For those two, `200` alone doesn't tell you which case you're in — check the response body's `status` field for `"PROCESSING"` to distinguish it from a settled success. The other four endpoints (`add credential`, `session refresh`, `revoke credential`, `revoke session`) use `201` or `204` on success, so a `200` by itself already means still-processing. + `verify`, `challenge`, and `export` already use `200` for their terminal success body (`AuthSession`, the challenge response, or the encrypted credentials). For those three, `200` alone doesn't tell you which case you're in — check the response body's `status` field for `"PROCESSING"` to distinguish it from a settled success. The other four endpoints (`add credential`, `session refresh`, `revoke credential`, `revoke session`) use `201` or `204` on success, so a `200` by itself already means still-processing. ### Add an additional credential diff --git a/mintlify/snippets/global-accounts/exporting-wallet.mdx b/mintlify/snippets/global-accounts/exporting-wallet.mdx index e96eff327..f4d9b5841 100644 --- a/mintlify/snippets/global-accounts/exporting-wallet.mdx +++ b/mintlify/snippets/global-accounts/exporting-wallet.mdx @@ -16,6 +16,10 @@ sequenceDiagram C->>C: stamp(payloadToSign, sessionPrivateKey) C->>IB: { stamp } IB->>G: POST /internal-accounts/{id}/export { same clientPublicKey }
Grid-Wallet-Signature
Request-Id + alt still settling at the wallet provider + G-->>IB: 200 { status: "PROCESSING" } + IB->>G: Re-send the identical signed request + end G-->>IB: 200 { id, encryptedWalletCredentials } IB-->>C: { encryptedWalletCredentials } C->>C: decrypt with client private key
→ mnemonic @@ -65,6 +69,8 @@ sequenceDiagram "encryptedWalletCredentials": "{\"version\":\"v1.0.0\",\"data\":\"7b22656e6361707065645075626c6963223a2230346634356632612e2e2e222c2263697068657274657874223a22316661313032333339302e2e2e222c226f7267616e697a6174696f6e4964223a226f72675f326d39462e2e2e227d\",\"dataSignature\":\"3045022100...\",\"enclaveQuorumPublic\":\"04a1b2c3...\"}" } ``` + + Export shares its terminal status code with the still-processing response: a `200` here is only the encrypted seed if the body isn't `{ "status": "PROCESSING" }`. See handling a still-processing response — if you get `PROCESSING`, re-send the identical signed retry (same `clientPublicKey`, `Grid-Wallet-Signature`, and `Request-Id`) until you get the credentials. Because Grid never stores them, the re-send is the only way to receive them; subscribe to `wallet_operation.completed` to know when it will succeed. The `Request-Id` challenge is consumed only by the attempt that actually returns credentials, so a `PROCESSING` response leaves it usable. `encryptedWalletCredentials` is a signed wallet export envelope, not the base58check session-bundle format used by `encryptedSessionSigningKey`. Parse the envelope, verify `dataSignature` against `enclaveQuorumPublic`, decode the hex `data` JSON to get `encappedPublic` and `ciphertext`, then decrypt with the export private key that matches the `clientPublicKey` you sent on both export requests. diff --git a/openapi.yaml b/openapi.yaml index 378972751..bd05bebe7 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -5842,6 +5842,8 @@ paths: 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 export may not settle within that request: an approval- or consensus-gated wallet-provider activity answers `200` with `status: "PROCESSING"` instead. The credentials are never stored server-side, so collecting them is the client's job — re-send the byte-identical signed retry (same headers, same body) until it returns `encryptedWalletCredentials`. The `Request-Id` challenge stays usable until an attempt actually delivers them, so a `PROCESSING` response never burns it; a delivered export does, and a later re-send is then rejected with `401`. Subscribe to `wallet_operation.completed` to learn when re-sending will succeed rather than polling blind. + 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 tags: @@ -5882,11 +5884,11 @@ paths: clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 responses: '200': - description: Signed retry accepted. Returns the encrypted wallet credentials. + description: 'Signed retry accepted. Returns the encrypted wallet credentials (`InternalAccountExportResponse`); or, when the underlying wallet-provider activity is still in flight, a `WalletOperationProcessing` body with `status: "PROCESSING"` — the credentials are never stored server-side, so the client re-sends the byte-identical signed retry to collect them once the activity settles. The challenge stays usable until an attempt actually returns credentials.' content: application/json: schema: - $ref: '#/components/schemas/InternalAccountExportResponse' + $ref: '#/components/schemas/InternalAccountExportResult' '202': 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: @@ -23329,6 +23331,32 @@ components: 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..."}' + WalletOperationProcessing: + title: Wallet Operation Processing + description: '`200` response returned by an Embedded Wallet operation that the wallet provider has accepted but not yet settled — a consensus- or approval-gated activity that is still in flight. It is not an error and needs no client action beyond patience: the backend reconciles the operation to its terminal state on its own. The client MAY re-send the byte-identical request to converge sooner; the request is idempotent and returns the settled success response once the operation completes.' + type: object + required: + - status + properties: + status: + type: string + enum: + - PROCESSING + description: Always `PROCESSING`. Marks a still-in-flight operation whose terminal result is not yet available. + example: PROCESSING + message: + type: string + description: Human-readable explanation that the operation is still being processed and the same request may be retried. + example: This login is still being processed. Retry the same request in a moment. + example: + status: PROCESSING + message: This login is still being processed. Retry the same request in a moment. + InternalAccountExportResult: + title: Internal Account Export Result + description: 'Response body for the signed retry of `POST /internal-accounts/{internalAccountId}/export`. Normally an `InternalAccountExportResponse` — the encrypted wallet credentials. When the underlying wallet-provider activity is still in flight, this is instead a `WalletOperationProcessing` body with `status: "PROCESSING"`. The credentials are never stored server-side, so re-sending the byte-identical signed retry is how they are collected: the re-send returns them as soon as the activity has settled, and the challenge is consumed only by the attempt that delivers them.' + anyOf: + - $ref: '#/components/schemas/InternalAccountExportResponse' + - $ref: '#/components/schemas/WalletOperationProcessing' AuthMethodType: type: string enum: @@ -23527,26 +23555,6 @@ components: SMS_OTP: '#/components/schemas/SmsOtpCredentialCreateRequest' OAUTH: '#/components/schemas/OauthCredentialCreateRequest' PASSKEY: '#/components/schemas/PasskeyCredentialCreateRequest' - WalletOperationProcessing: - title: Wallet Operation Processing - description: '`200` response returned by an Embedded Wallet operation that the wallet provider has accepted but not yet settled — a consensus- or approval-gated activity that is still in flight. It is not an error and needs no client action beyond patience: the backend reconciles the operation to its terminal state on its own. The client MAY re-send the byte-identical request to converge sooner; the request is idempotent and returns the settled success response once the operation completes.' - type: object - required: - - status - properties: - status: - type: string - enum: - - PROCESSING - description: Always `PROCESSING`. Marks a still-in-flight operation whose terminal result is not yet available. - example: PROCESSING - message: - type: string - description: Human-readable explanation that the operation is still being processed and the same request may be retried. - example: This login is still being processed. Retry the same request in a moment. - example: - status: PROCESSING - message: This login is still being processed. Retry the same request in a moment. AuthMethodResponse: title: Auth Method Response description: |- diff --git a/openapi/components/schemas/internal_accounts/InternalAccountExportResult.yaml b/openapi/components/schemas/internal_accounts/InternalAccountExportResult.yaml new file mode 100644 index 000000000..725b22d13 --- /dev/null +++ b/openapi/components/schemas/internal_accounts/InternalAccountExportResult.yaml @@ -0,0 +1,14 @@ +title: Internal Account Export Result +description: >- + Response body for the signed retry of `POST + /internal-accounts/{internalAccountId}/export`. Normally an + `InternalAccountExportResponse` — the encrypted wallet credentials. When + the underlying wallet-provider activity is still in flight, this is + instead a `WalletOperationProcessing` body with `status: "PROCESSING"`. + The credentials are never stored server-side, so re-sending the + byte-identical signed retry is how they are collected: the re-send + returns them as soon as the activity has settled, and the challenge is + consumed only by the attempt that delivers them. +anyOf: + - $ref: ./InternalAccountExportResponse.yaml + - $ref: ../common/WalletOperationProcessing.yaml diff --git a/openapi/paths/internal_accounts/internal_accounts_{id}_export.yaml b/openapi/paths/internal_accounts/internal_accounts_{id}_export.yaml index a9769cbd0..478e5bec2 100644 --- a/openapi/paths/internal_accounts/internal_accounts_{id}_export.yaml +++ b/openapi/paths/internal_accounts/internal_accounts_{id}_export.yaml @@ -30,6 +30,19 @@ post: with the matching private key. + The export may not settle within that request: an approval- or + consensus-gated wallet-provider activity answers `200` with + `status: "PROCESSING"` instead. The credentials are never stored + server-side, so collecting them is the client's job — re-send the + byte-identical signed retry (same headers, same body) until it + returns `encryptedWalletCredentials`. The `Request-Id` challenge + stays usable until an attempt actually delivers them, so a + `PROCESSING` response never burns it; a delivered export does, and a + later re-send is then rejected with `401`. Subscribe to + `wallet_operation.completed` to learn when re-sending will succeed + rather than polling blind. + + 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 @@ -82,11 +95,19 @@ post: clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 responses: '200': - description: Signed retry accepted. Returns the encrypted wallet credentials. + description: >- + Signed retry accepted. Returns the encrypted wallet credentials + (`InternalAccountExportResponse`); or, when the underlying + wallet-provider activity is still in flight, a + `WalletOperationProcessing` body with `status: "PROCESSING"` — the + credentials are never stored server-side, so the client re-sends + the byte-identical signed retry to collect them once the activity + settles. The challenge stays usable until an attempt actually + returns credentials. content: application/json: schema: - $ref: ../../components/schemas/internal_accounts/InternalAccountExportResponse.yaml + $ref: ../../components/schemas/internal_accounts/InternalAccountExportResult.yaml '202': description: >- Challenge issued. The response contains `payloadToSign` (which