Skip to content

[grid-api] async-ops: GET /operations + wallet_operation webhooks (DRAFT — do not merge until async feature deploys) - #559

Closed
carsonp6 wants to merge 1 commit into
mainfrom
grid-api-async-ops-public-surface
Closed

[grid-api] async-ops: GET /operations + wallet_operation webhooks (DRAFT — do not merge until async feature deploys)#559
carsonp6 wants to merge 1 commit into
mainfrom
grid-api-async-ops-public-surface

Conversation

@carsonp6

@carsonp6carsonp6 commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

What

Publish the canonical async-operations public surface in the Grid API spec + docs: the GET /operations/{operationId} endpoint (with Operation / OperationResult / OperationError schemas), and the WALLET_OPERATION.COMPLETED / WALLET_OPERATION.FAILED webhooks (with WalletOperationWebhook* schemas + WebhookType enum values), plus the async-operations.mdx Mintlify 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 a WALLET_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 flipping GRID_TURNKEY_ASYNC_EXPORT ON. See 50-async-ops-api-design.md §1/§2.

This is the external lightsparkdev/grid-api source-of-truth counterpart to the monorepo's vendored regen (sparkcore PR #28463); the two stay in lockstep.

Notable points

  • status is PROCESSING/COMPLETED/FAILED (uppercase) on the GET; webhook data.status is lowercase completed/failed — the docs call out branching on webhook type, not data.status.
  • Webhook type casing is WALLET_OPERATION.* (UPPERCASE OBJECT.EVENT), matching the established WebhookType convention; reconciled atomically with the emitter in sparkcore PR #28460. Safe pre-GA since no partner has consumed the event yet.
  • OperationResult is tagged by type so result shapes can grow per operation type without a breaking change; state-changing ops return result: 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.md in the sparkcore login-migration docs.

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>
@vercel

vercelBot commented Jun 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
grid-flow-builderIgnoredIgnoredJun 9, 2026 5:27pm

Request Review

@github-actionsgithub-actionsBot added the breaking-change Introduces a breaking change to the OpenAPI spec label Jun 9, 2026
@github-actions

github-actionsBot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

✱ Stainless preview builds for grid

This PR will update the grid SDKs with the following commit messages.

cli

chore(internal): regenerate SDK with no functional changes

csharp

feat(api): add WalletOperationWebhookEvent webhook type

go

feat(api): add wallet operation webhook event type

kotlin

feat(api): add walletOperation event to webhook types

openapi

feat(api): add operations resource, wallet-operation webhook, operation types

php

feat(api): add wallet operation webhook event

python

feat(api): add WalletOperationWebhookEvent type

ruby

feat(api): add wallet operation webhook event

typescript

feat(api): add WalletOperationWebhookEvent to webhooks

Edit this comment to update them. They will appear in their respective SDK's changelogs.

grid-kotlinstudio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅build ✅lint ✅test ✅ (prev: test ❗)

New diagnostics (1 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /operations/{operationId}`
grid-pythonstudio · code · diff

Your SDK build had at least one "note" diagnostic, which is a regression from the base state.
generate ✅build ✅lint ❗test ❗

pip install https://pkg.stainless.com/s/grid-python/3f880416b5af805eeb562d36d1aadf706c018da1/grid-0.0.1-py3-none-any.whl
New diagnostics (1 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /operations/{operationId}`
grid-clistudio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ⚠️build ⏭️ (prev: build ❗) → lint ⏭️ (prev: lint ❗) → test ❗

New diagnostics (1 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /operations/{operationId}`
grid-typescriptstudio · code · diff

Your SDK build had at least one "note" diagnostic, which is a regression from the base state.
generate ✅build ✅lint ✅test ✅

npm install https://pkg.stainless.com/s/grid-typescript/914375eb109b147e5ec6f7f819971e8d57e4c687/dist.tar.gz
New diagnostics (1 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /operations/{operationId}`
grid-rubystudio · code · diff

Your SDK build had at least one "note" diagnostic, which is a regression from the base state.
generate ✅build ✅lint ✅test ✅

New diagnostics (1 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /operations/{operationId}`
grid-gostudio · code · diff

Your SDK build had at least one "note" diagnostic, which is a regression from the base state.
generate ✅build ✅lint ❗test ❗

go get github.com/stainless-sdks/grid-go@7a1b27129464ec67c40c0f57d054c97f2e6681b8
New diagnostics (1 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /operations/{operationId}`
grid-csharpstudio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ⚠️build ❗lint ✅test ❗

New diagnostics (2 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /operations/{operationId}`
💡 Name/Renamed: 248 names were renamed due to language constraints, so fallback names will be used instead.
grid-phpstudio · code · diff

Your SDK build had at least one "note" diagnostic, which is a regression from the base state.
generate ✅lint ✅test ✅

New diagnostics (1 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /operations/{operationId}`
grid-openapistudio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅

New diagnostics (1 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /operations/{operationId}`

This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-06-09 17:35:31 UTC

@carsonp6

Copy link
Copy Markdown
ContributorAuthor

Superseded by #802. That PR is the clean, webhook-only version of the wallet-operation partner webhook and matches the current partner contract: the GET /operations/{operationId} poll endpoint (and Operation/OperationResult schemas) here is descoped in favor of resubmit-until-terminal — a data-returning result (wallet.export) is retrieved by resubmitting the original signed request, never via a poll endpoint and never in the webhook. Webhook shapes/casing (WALLET_OPERATION.COMPLETED/.FAILED) are unchanged. Closing in favor of #802.

@carsonp6carsonp6 closed this Aug 5, 2026
carsonp6 added a commit that referenced this pull request Aug 25, 2026
## 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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-changeIntroduces a breaking change to the OpenAPI spec

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@carsonp6