Skip to content

feat(webhooks): add wallet-operation partner webhook - #802

Open
carsonp6 wants to merge 6 commits into
mainfrom
grid-api-wallet-operation-webhook
Open

feat(webhooks): add wallet-operation partner webhook#802
carsonp6 wants to merge 6 commits into
mainfrom
grid-api-wallet-operation-webhook

Conversation

@carsonp6

@carsonp6 carsonp6 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

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

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.

Document the wallet-operation webhook that fires when an asynchronous
embedded-wallet operation reaches a terminal state:
WALLET_OPERATION.COMPLETED on terminal success, WALLET_OPERATION.FAILED
on terminal failure. The specific op is carried in data.operationType
(auth_credential.delete, session.revoke, wallet.export); data.status is
lowercase completed/failed.

Adds WalletOperationWebhook / WalletOperationWebhookData / OperationError
schemas, the two WALLET_OPERATION.* WebhookType enum values, and
registers the webhook in the root spec. Additive only; no API version
bump. A data-returning result (wallet.export) is retrieved by
resubmitting the original signed request until it returns the result --
never delivered in the webhook.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mintlify

mintlify Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
Grid 🟢 Ready View Preview Aug 5, 2026, 9:34 PM

@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

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

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
grid-flow-builder Ignored Ignored Preview Aug 22, 2026 12:00am
grid-wallet-demo Ignored Ignored Preview Aug 22, 2026 12:00am

Request Review

@github-actions

github-actions Bot commented Aug 5, 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

go

feat(api): add wallet operation webhook event type

kotlin

feat(api): add walletOperation webhook event type

openapi

feat(api): add wallet operation completed/failed webhook

php

feat(api): add WalletOperationWebhookEvent to webhooks

python

feat(api): add WalletOperationWebhookEvent webhook type

ruby

feat(api): add WalletOperationWebhookEvent to webhooks

typescript

feat(api): add WalletOperationWebhookEvent to webhooks

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

grid-typescript studio · code · diff

Your SDK build had at least one "warning" diagnostic, but this did not represent a regression.
generate ⚠️build ✅lint ❗test ✅

npm install https://pkg.stainless.com/s/grid-typescript/6b6e7cf0aefffdd9d6aceb87457ba2b75a7dda4b/dist.tar.gz
grid-openapi studio · code · diff

Your SDK build had at least one "warning" diagnostic, but this did not represent a regression.
generate ⚠️

grid-ruby studio · code · diff

Your SDK build had at least one "warning" diagnostic, but this did not represent a regression.
generate ⚠️build ✅lint ✅test ✅

grid-go studio · code · diff

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

go get github.com/stainless-sdks/grid-go@452ea50fb08139aeeccc60468859590ccc2b8637
New diagnostics (2 note)
💡 Schema/EnumHasOneMember: Confirm intentional use of `enum` with single member.
💡 Schema/EnumHasOneMember: Confirm intentional use of `enum` with single member.
grid-kotlin studio · 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)
💡 Schema/EnumHasOneMember: Confirm intentional use of `enum` with single member.
💡 Schema/EnumHasOneMember: Confirm intentional use of `enum` with single member.
grid-python studio · code · diff

Your SDK build had at least one "warning" diagnostic, but this did not represent a regression.
generate ⚠️build ✅lint ❗test ❗

pip install https://pkg.stainless.com/s/grid-python/be5151ae768b6c8f9c223994884c25372cab8836/grid-0.0.1-py3-none-any.whl
grid-php studio · code · diff

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

New diagnostics (2 note)
💡 Schema/EnumHasOneMember: Confirm intentional use of `enum` with single member.
💡 Schema/EnumHasOneMember: Confirm intentional use of `enum` with single member.
grid-cli studio · code · diff

Your SDK build had at least one "warning" diagnostic, but this did not represent a regression.
generate ⚠️build ❗lint ❗test ❗


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-08-22 00:05:44 UTC

@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds the public wallet-operation terminal webhook, its payload schemas, and the corresponding central webhook event values, then regenerates both bundled specifications. The new schema does not yet encode the documented relationship between event type, status, and required failure details.

  • Registers wallet-operation in the modular OpenAPI webhook map.
  • Defines completed and failed payload examples for embedded-wallet operations.
  • Adds operation, status, and error schemas plus the two WebhookType values.
  • Updates the root and Mintlify bundles.

Confidence Score: 4/5

The PR should not merge until the public schema enforces the documented failed-event error requirement and terminal event/status relationship.

Both event variants currently use one payload type in which status is independent and error is always optional, so generated SDKs and validators accept terminal webhook shapes that contradict the contract being introduced.

Files Needing Attention: openapi/components/schemas/webhooks/WalletOperationWebhookData.yaml, openapi/components/schemas/webhooks/WalletOperationWebhook.yaml

Important Files Changed

Filename Overview
openapi/components/schemas/webhooks/WalletOperationWebhookData.yaml Adds the operation payload, but leaves the documented status/error invariant unenforced.
openapi/components/schemas/webhooks/WalletOperationWebhook.yaml Adds both terminal event discriminants while sharing one payload schema that cannot narrow each event’s state.
openapi/webhooks/wallet-operation.yaml Registers and documents the new signed webhook with completed and failed examples.
openapi/components/schemas/webhooks/WebhookType.yaml Adds the two wallet-operation terminal event values consistently with the webhook schema.
openapi/openapi.yaml Wires the new modular webhook into the source specification.
openapi.yaml Regenerated root bundle contains the new webhook and component schemas.
mintlify/openapi.yaml Regenerated Mintlify bundle mirrors the new public webhook contract.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Async wallet operation] --> B{Terminal outcome}
  B -->|Success| C[WALLET_OPERATION.COMPLETED]
  B -->|Failure| D[WALLET_OPERATION.FAILED]
  C --> E[data.status = completed]
  D --> F[data.status = failed]
  F --> G[data.error.code required]
  C --> H[Signed export request may be resubmitted for result]
Loading
Prompt To Fix All With AI
### Issue 1
openapi/components/schemas/webhooks/WalletOperationWebhookData.yaml:25-30
**Terminal outcome fields are uncorrelated**

When generated SDKs or OpenAPI validators process this webhook, both event variants use a payload where `status` is independent and `error` is always optional, so contradictory events and failed events without failure details satisfy the published schema.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment on lines +25 to +30
example: completed
error:
anyOf:
- $ref: ./OperationError.yaml
- type: 'null'
description: Present only on `failed`; `null` otherwise.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Terminal outcome fields are uncorrelated

When generated SDKs or OpenAPI validators process this webhook, both event variants use a payload where status is independent and error is always optional, so contradictory events and failed events without failure details satisfy the published schema.

Knowledge Base Used: OpenAPI Spec Core: Structure, Build, and Shared Schemas

Prompt To Fix With AI
This is a comment left during a code review.
Path: openapi/components/schemas/webhooks/WalletOperationWebhookData.yaml
Line: 25-30

Comment:
**Terminal outcome fields are uncorrelated**

When generated SDKs or OpenAPI validators process this webhook, both event variants use a payload where `status` is independent and `error` is always optional, so contradictory events and failed events without failure details satisfy the published schema.

**Knowledge Base Used:** [OpenAPI Spec Core: Structure, Build, and Shared Schemas](https://app.greptile.com/lightspark/-/custom-context/knowledge-base/lightsparkdev/grid-api/-/docs/openapi-spec-core.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3647ddc. Split WalletOperationWebhookData into a status-discriminated oneOf (WalletOperationCompletedData / WalletOperationFailedData) instead of one shared object:

  • failed events now require error (previously optional) — I verified against the emitter (sparkcore/grid/turnkey/operation_webhook.py + the state machine's _apply_error_fields) that every real code path reaching FAILED_TERMINAL (remote failure, finalization failure, webhook-ingest failure, reconcile failure, unregistered-purpose) always sets last_error_code first, so error.code is genuinely always present on a real failed webhook — this isn't just tightening for its own sake.
  • completed events no longer allow an error property at all, matching what the emitter actually sends (the key is omitted entirely on success, never sent as null).

Left WalletOperationWebhook.type as a flat enum (not folded into the oneOf) — WebhookType.yaml already documents that type alone is the intended routing discriminator ("lets consumers route purely on type without inspecting data.status"), and the emitter derives type and data.status from the same single status variable, so they can't diverge in practice; doubling up the discriminator there would be redundant.

make build (redocly bundle) and make lint (redocly + spectral) are green, no new warnings introduced (663 problems before and after).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up on this same file: pushed dc96b25, which shapes the payload further per a design pass — added requestId (the integrator's own Request-Id, now the primary correlation key, since the platform's async contract is moving to a model where the partner never otherwise receives an id they could poll/match on) and resourceType/resourceId (the affected credential/session/account), so the webhook is self-contained. completed/failed still split as a discriminated oneOf with error required on failure. Full rationale in the PR description.

…allet-operation webhook

Splits WalletOperationWebhookData into a status-discriminated oneOf so the
schema matches what sparkcore actually guarantees: a `failed` event always
carries `error.code` (every FAILED_TERMINAL transition sets last_error_code)
and a `completed` event never carries `error` at all.
…correlatable

Adds requestId (the integrator's own Request-Id from the signed retry that
produced the terminal result) as the primary correlation key, and
resourceType/resourceId (the affected AuthMethod/Session/InternalAccount)
so the webhook can be handled without a follow-up API call. Repositions
operationId as a Grid-internal support reference, not a correlator.

Documents the correlation model (requestId to match your request, envelope
id to dedupe redeliveries, operationId for support) and adds the missing
WALLET_OPERATION.* row to the webhook retry-policy table.

All new fields are sourced from data EntGridTurnkeyActivity already
persists (pending_request_id, correlation_key, internal_account_id) — no
new sparkcore persistence required. Wiring them into the actual webhook
payload is a sparkcore emitter change tracked separately, not part of
this spec-only PR.
… example

DeleteApiKeysFailed named a provider activity type in the public spec.
Replace it with a Grid-vocabulary placeholder and note that codes are
Grid-defined and vendor-stable, since sparkcore doesn't map provider
statuses to a Grid taxonomy yet (tracked as part of the 31886-successor
emitter work).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he runtime fix

- Add `auth_credential.create` to the operationType/resourceType enums:
  for a create-type operation resourceId is the only way to learn the
  new credential's id, so its description (and the correlation-model
  section) is reworded to call that out as resourceId's own primary
  correlation role, distinct from requestId's.
- Update the OperationError example and the failed-webhook sample from
  the interim OPERATION_FAILED placeholder to SIGNER_PROVIDER_REJECTED,
  matching the vocabulary sparkcore now actually emits (webdev #33379).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant