Uh oh!
There was an error while loading. Please reload this page.
[grid-api] async-ops: GET /operations + wallet_operation webhooks (DRAFT — do not merge until async feature deploys) - #559
Conversation
Mirror the async-ops public API surface from webdev sparkcore PR #28463
(which edited webdev's vendored grid-api/openapi.yaml) into the canonical
grid-api spec + docs.
Spec (openapi/ source, rebundled into openapi.yaml + mintlify/openapi.yaml):
- GET /operations/{operationId} polling endpoint (getOperationById), tagged
under a new "Operations" tag
- Operation / OperationResult / OperationError schemas
- wallet-operation webhook (WALLET_OPERATION.COMPLETED / .FAILED) +
WalletOperationWebhook / WalletOperationWebhookData schemas
- WALLET_OPERATION.COMPLETED / .FAILED added to the WebhookType enum
Adapted to the canonical spec's OpenAPI 3.1.0 dialect: nullable fields use
type-array / anyOf-with-`null` instead of the 3.0 `nullable: true` used in
webdev's vendored copy.
Docs:
- New api-reference/async-operations.mdx prose page (polling endpoint +
wallet_operation webhooks), wired into docs.json nav
- WALLET_OPERATION.* row added to the shared webhooks retry-policy table
redocly + spectral (--fail-severity=error) both pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
✱ Stainless preview builds for gridThis PR will update the cli csharp go kotlin openapi php python ruby typescript Edit this comment to update them. They will appear in their respective SDK's changelogs. ✅ grid-kotlinstudio · code · diff
✅ grid-pythonstudio · code · diff
✅ grid-clistudio · code · diff
✅ grid-typescriptstudio · code · diff
✅ grid-rubystudio · code · diff
✅ grid-gostudio · code · diff
✅ grid-csharpstudio · code · diff
✅ grid-phpstudio · code · diff
✅ grid-openapistudio · code · diff
This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push. |
carsonp6
commented
Aug 5, 2026
Superseded by #802. That PR is the clean, webhook-only version of the |
## What
Add the `wallet-operation` partner webhook to the public Grid API spec:
the webhook entry plus its `WalletOperationWebhook` /
`WalletOperationWebhookData` / `OperationError` schemas, and the two
`WALLET_OPERATION.*` values in the central `WebhookType` enum.
This documents a webhook that **already fires from the backend** on
terminal transitions of asynchronous embedded-wallet operations —
`WALLET_OPERATION.COMPLETED` on terminal success,
`WALLET_OPERATION.FAILED` on terminal failure. We are just formalizing
the public contract, shaped so it's self-contained and correlatable
(handle it from the payload alone, no follow-up API call needed).
## Shape
- Webhook envelope `type` (UPPERCASE, matching the `OBJECT.EVENT`
convention): `WALLET_OPERATION.COMPLETED`, `WALLET_OPERATION.FAILED`.
- `data` is a `status`-discriminated `oneOf`
(`WalletOperationCompletedData` / `WalletOperationFailedData`) rather
than one shared object, so the schema enforces what's actually true:
`completed` never carries `error`, `failed` always requires it.
- `data.requestId` — the **primary correlation key**. This is the same
`Request-Id` value the integrator supplied on the signed retry that
produced the terminal result (and kept re-sending through any `200 {
status: "PROCESSING" }` responses). It's how a partner ties this webhook
back to the request they made.
- `data.resourceType` / `data.resourceId` — the business resource the
operation affected, so the webhook alone is enough to update local state
without an extra `GET`: `AUTH_METHOD` (`AuthMethod:<uuid>`) for
`auth_credential.delete`, `SESSION` (`Session:<uuid>`) for
`session.revoke`, `INTERNAL_ACCOUNT` (`InternalAccount:<uuid>`) for
`wallet.export`.
- `data.operationId` — repositioned as a **Grid-internal support
reference**, not a correlator (a partner never sees this id anywhere
else, so it can't be used to match anything on their side).
- `data.operationType` (the specific op): `auth_credential.delete`,
`session.revoke`, `wallet.export`.
- `data.error` (`{ code }`) required on `failed`, absent (not even
`null`) on `completed`.
- The webhook carries no sensitive result material. For a data-returning
op (`wallet.export`), the result is **never** delivered in the webhook —
it is retrieved by resubmitting the original signed export request until
it returns the result.
- Documented the correlation model on the webhook itself (`requestId` to
match your request, the envelope `id` to dedupe redeliveries,
`operationId` for support) and added the missing `WALLET_OPERATION.*`
row to the webhook retry-policy table (`mintlify/snippets/webhooks.mdx`)
— it follows the same generic policy
(`gen_send_umaaas_webhook`/`send_grid_webhook` apply no per-type retry
carve-out for this event).
### Why `requestId` closes a real gap
The platform's async contract is moving from "202 + `operationId`,
poll/GET by id" to "200 + `PROCESSING` body, re-send the byte-identical
original request" (delete/revoke in webdev #33184, export in flight; the
`WalletOperationProcessing` response shape lands via #850). That new
`WalletOperationProcessing` body carries **no id at all** — the
partner's only durable handle on an in-flight operation is the
`Request-Id` they sent. Before this change, the webhook's only id
(`operationId`) was a value the partner had never seen and couldn't
derive, breaking correlation under any concurrency. `requestId` is the
fix: it's the exact value they already hold.
### Design choice: flat fields, not a discriminated resource union
This repo has a heavier precedent for "an id with a type"
(`TransactionDestinationOneOf`'s `oneOf` + `discriminator` + per-type
schema files). I used flat `resourceType` (enum) + `resourceId` (LSID
string) fields instead — each `operationType` maps to exactly one
resource shape (a bare id), so a full discriminated union would add
several files and a nested `oneOf` for no behavioral benefit. Happy to
switch to the heavier pattern if you'd rather match precedent exactly.
## Implementability check against sparkcore's emitter (informs but
doesn't change sparkcore here)
Every new field is sourced from data `EntGridTurnkeyActivity` **already
persists today** — no new sparkcore persistence/migration required:
| Spec field | Sparkcore source | Persisted today? |
|---|---|---|
| `requestId` | `activity.pending_request_id` (always non-null for the 3
partner-facing purposes — every submit path passes it as a required arg)
| Yes |
| `resourceId` / `resourceType` for `auth_credential.delete` |
`activity.correlation_key` (the deleted `AuthMethod` id) | Yes |
| `resourceId` / `resourceType` for `session.revoke` |
`activity.correlation_key` (the revoked `Session` id) | Yes |
| `resourceId` / `resourceType` for `wallet.export` |
`activity.internal_account_id` (set directly on the activity at submit
time) | Yes |
| `operationId` | `activity.id` (unchanged, already emitted) | Yes |
**Requires an emitter change (not in this PR):**
`sparkcore/sparkcore/grid/turnkey/operation_webhook.py`'s `_fire()`
needs new code to read `pending_request_id` and a small purpose→(field,
LSID prefix) lookup for the resource, and format both into the webhook
payload. No schema/migration work — this is pure wiring of
already-persisted columns. Tracked as a successor to #31886 (which
already owns flipping the envelope `type` casing); that PR should pick
up `requestId`/`resourceId`/`resourceType` too rather than a third
follow-up.
## Notes
- **Additive / non-breaking** — no `info.version` bump (stays
`2025-10-13`); no `servers.url` change.
- Regenerated bundles (`openapi.yaml`, `mintlify/openapi.yaml`) via `npm
run build:openapi`; `npm run lint:openapi` passes with 0 errors (same
663-problem baseline as `main`, all pre-existing warnings/infos).
- Replaces the webhook portion of the older draft #559. This PR is
intentionally **webhook-only** and drops the `GET
/operations/{operationId}` poll endpoint, per the
resubmit-until-terminal retrieval contract that superseded poll-an-id.
#559 will be closed in favor of this.
- sparkcore currently emits the envelope `type` in **lowercase**
(`wallet_operation.completed` / `wallet_operation.failed`) as an interim
placeholder pending this spec change (see the `# grid-api spec change is
deferred` note at the call site) — tracked by #31886, not changed here.
## Status
**Ready for review.** Merged onto latest `main` (clean, no conflicts).
`make build` / `make lint` both green.
Sequencing:
1. Merge this PR.
2. Regenerate webdev's vendored `grid-api` client
(`grid-api/update_schema.sh`) from the new spec.
3. Sparkcore emitter follow-up (successor to #31886): flip the envelope
`type` casing to the real generated enum, **and** wire
`requestId`/`resourceId`/`resourceType` into `operation_webhook.py` from
the already-persisted activity fields above.
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
What
Publish the canonical async-operations public surface in the Grid API spec + docs: the
GET /operations/{operationId}endpoint (withOperation/OperationResult/OperationErrorschemas), and theWALLET_OPERATION.COMPLETED/WALLET_OPERATION.FAILEDwebhooks (withWalletOperationWebhook*schemas +WebhookTypeenum values), plus theasync-operations.mdxMintlify guide and the webhooks snippet.Why
This is the partner-facing contract for the Turnkey request-driven async migration. When a state-changing or data-returning Turnkey op goes async it returns
202 {operationId: "Operation:<uuid>"}; partners then pollGET /operations/{operationId}and/or receive aWALLET_OPERATION.*webhook on terminal state. The endpoint is the only way to retrieve a data-returning result (wallet.export's HPKE bundle) — it is re-fetched on demand and never stored by Grid, never delivered in the webhook. This spec is what gates flippingGRID_TURNKEY_ASYNC_EXPORTON. See50-async-ops-api-design.md§1/§2.This is the external
lightsparkdev/grid-apisource-of-truth counterpart to the monorepo's vendored regen (sparkcore PR #28463); the two stay in lockstep.Notable points
statusisPROCESSING/COMPLETED/FAILED(uppercase) on the GET; webhookdata.statusis lowercasecompleted/failed— the docs call out branching on webhooktype, notdata.status.typecasing isWALLET_OPERATION.*(UPPERCASEOBJECT.EVENT), matching the establishedWebhookTypeconvention; reconciled atomically with the emitter in sparkcore PR #28460. Safe pre-GA since no partner has consumed the event yet.OperationResultis tagged bytypeso result shapes can grow per operation type without a breaking change; state-changing ops returnresult: null.Status
DRAFT — part of the Turnkey login-family migration / async-ops program; do not merge yet. Hold until the async feature actually deploys (the underlying knobs are still OFF), so the published docs don't promise an endpoint partners can't yet use.
Part of the Turnkey login-family migration program. See
00-program-plan.mdin the sparkcore login-migration docs.