Skip to content

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

Merged
carsonp6 merged 6 commits into
mainfrom
grid-api-wallet-operation-webhook
Aug 25, 2026
Merged

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

Conversation

@carsonp6

@carsonp6carsonp6 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 EntGridTurnkeyActivityalready persists today — no new sparkcore persistence/migration required:

Spec fieldSparkcore sourcePersisted today?
requestIdactivity.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.deleteactivity.correlation_key (the deleted AuthMethod id)Yes
resourceId / resourceType for session.revokeactivity.correlation_key (the revoked Session id)Yes
resourceId / resourceType for wallet.exportactivity.internal_account_id (set directly on the activity at submit time)Yes
operationIdactivity.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

mintlifyBot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

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

ProjectStatusPreviewUpdated (UTC)
Grid🟢 ReadyView PreviewAug 5, 2026, 9:34 PM

@vercel

vercelBot commented Aug 5, 2026

Copy link
Copy Markdown

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

2 Skipped Deployments
ProjectDeploymentActionsUpdated (UTC)
grid-flow-builderIgnoredIgnoredPreviewAug 22, 2026 12:00am
grid-wallet-demoIgnoredIgnoredPreviewAug 22, 2026 12:00am

Request Review

@github-actions

github-actionsBot 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
⚠️grid-openapistudio · code

Your SDK build had at least one "warning" diagnostic.
generate ⚠️

⚠️grid-rubystudio · code

Your SDK build had at least one "warning" diagnostic.
generate ⚠️build ✅lint ✅test ✅

⚠️grid-gostudio · code

Your SDK build had a failure in the lint CI job, which is a regression from the base state.
generate ⚠️build ✅lint ❗test ❗

go get github.com/stainless-sdks/grid-go@7874ef4565edbfa439c0a4811b9af0486d17c627
⚠️grid-kotlinstudio · code

Your SDK build had a failure in the test CI job, which is a regression from the base state.
generate ⚠️build ✅lint ✅test ❗

⚠️grid-typescriptstudio · conflict

Your SDK build had at least one warning diagnostic.

⚠️grid-pythonstudio · code

Your SDK build had a failure in the lint CI job, which is a regression from the base state.
generate ⚠️build ✅lint ❗test ❗

pip install https://pkg.stainless.com/s/grid-python/c94528739ad8e244c15fe1577e5b45c3cbeba472/grid-0.0.1-py3-none-any.whl
⚠️grid-phpstudio · code

Your SDK build had at least one "warning" diagnostic.
generate ⚠️lint ✅test ✅

⚠️grid-clistudio · code

Your SDK build had a failure in the test CI job, which is a regression from the base state.
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-25 21:44:02 UTC

@greptile-apps

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

FilenameOverview
openapi/components/schemas/webhooks/WalletOperationWebhookData.yamlAdds the operation payload, but leaves the documented status/error invariant unenforced.
openapi/components/schemas/webhooks/WalletOperationWebhook.yamlAdds both terminal event discriminants while sharing one payload schema that cannot narrow each event’s state.
openapi/webhooks/wallet-operation.yamlRegisters and documents the new signed webhook with completed and failed examples.
openapi/components/schemas/webhooks/WebhookType.yamlAdds the two wallet-operation terminal event values consistently with the webhook schema.
openapi/openapi.yamlWires the new modular webhook into the source specification.
openapi.yamlRegenerated root bundle contains the new webhook and component schemas.
mintlify/openapi.yamlRegenerated 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.

P1Terminal 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
ContributorAuthor

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
ContributorAuthor

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>
@carsonp6
carsonp6 merged commit 1fc3f22 into mainAug 25, 2026
9 checks passed
@carsonp6
carsonp6 deleted the grid-api-wallet-operation-webhook branch August 25, 2026 21:38
Sign up for freeto 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.

2 participants

@carsonp6@shreyav