feat(webhooks): add wallet-operation partner webhook - #802
Conversation
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>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
✱ Stainless preview builds for gridThis PR will update the cli go kotlin openapi php python ruby typescript Edit this comment to update them. They will appear in their respective SDK's changelogs. ✅ grid-typescript studio · code · diff
✅ grid-openapi studio · code · diff
✅ grid-ruby studio · code · diff
✅ grid-go studio · code · diff
✅ grid-kotlin studio · code · diff
✅ grid-python studio · code · diff
✅ grid-php studio · code · diff
✅ grid-cli studio · code · diff
This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push. |
Greptile SummaryThis 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.
Confidence Score: 4/5The 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
|
| 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]
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
| example: completed | ||
| error: | ||
| anyOf: | ||
| - $ref: ./OperationError.yaml | ||
| - type: 'null' | ||
| description: Present only on `failed`; `null` otherwise. |
There was a problem hiding this 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
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!
There was a problem hiding this comment.
Fixed in 3647ddc. Split WalletOperationWebhookData into a status-discriminated oneOf (WalletOperationCompletedData / WalletOperationFailedData) instead of one shared object:
failedevents now requireerror(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 reachingFAILED_TERMINAL(remote failure, finalization failure, webhook-ingest failure, reconcile failure, unregistered-purpose) always setslast_error_codefirst, soerror.codeis genuinely always present on a realfailedwebhook — this isn't just tightening for its own sake.completedevents no longer allow anerrorproperty at all, matching what the emitter actually sends (the key is omitted entirely on success, never sent asnull).
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).
There was a problem hiding this comment.
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>
What
Add the
wallet-operationpartner webhook to the public Grid API spec: the webhook entry plus itsWalletOperationWebhook/WalletOperationWebhookData/OperationErrorschemas, and the twoWALLET_OPERATION.*values in the centralWebhookTypeenum.This documents a webhook that already fires from the backend on terminal transitions of asynchronous embedded-wallet operations —
WALLET_OPERATION.COMPLETEDon terminal success,WALLET_OPERATION.FAILEDon 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
type(UPPERCASE, matching theOBJECT.EVENTconvention):WALLET_OPERATION.COMPLETED,WALLET_OPERATION.FAILED.datais astatus-discriminatedoneOf(WalletOperationCompletedData/WalletOperationFailedData) rather than one shared object, so the schema enforces what's actually true:completednever carrieserror,failedalways requires it.data.requestId— the primary correlation key. This is the sameRequest-Idvalue the integrator supplied on the signed retry that produced the terminal result (and kept re-sending through any200 { 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 extraGET:AUTH_METHOD(AuthMethod:<uuid>) forauth_credential.delete,SESSION(Session:<uuid>) forsession.revoke,INTERNAL_ACCOUNT(InternalAccount:<uuid>) forwallet.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 onfailed, absent (not evennull) oncompleted.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.requestIdto match your request, the envelopeidto dedupe redeliveries,operationIdfor support) and added the missingWALLET_OPERATION.*row to the webhook retry-policy table (mintlify/snippets/webhooks.mdx) — it follows the same generic policy (gen_send_umaaas_webhook/send_grid_webhookapply no per-type retry carve-out for this event).Why
requestIdcloses a real gapThe platform's async contract is moving from "202 +
operationId, poll/GET by id" to "200 +PROCESSINGbody, re-send the byte-identical original request" (delete/revoke in webdev #33184, export in flight; theWalletOperationProcessingresponse shape lands via #850). That newWalletOperationProcessingbody carries no id at all — the partner's only durable handle on an in-flight operation is theRequest-Idthey 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.requestIdis 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'soneOf+discriminator+ per-type schema files). I used flatresourceType(enum) +resourceId(LSID string) fields instead — eachoperationTypemaps to exactly one resource shape (a bare id), so a full discriminated union would add several files and a nestedoneOffor 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:requestIdactivity.pending_request_id(always non-null for the 3 partner-facing purposes — every submit path passes it as a required arg)resourceId/resourceTypeforauth_credential.deleteactivity.correlation_key(the deletedAuthMethodid)resourceId/resourceTypeforsession.revokeactivity.correlation_key(the revokedSessionid)resourceId/resourceTypeforwallet.exportactivity.internal_account_id(set directly on the activity at submit time)operationIdactivity.id(unchanged, already emitted)Requires an emitter change (not in this PR):
sparkcore/sparkcore/grid/turnkey/operation_webhook.py's_fire()needs new code to readpending_request_idand 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 envelopetypecasing); that PR should pick uprequestId/resourceId/resourceTypetoo rather than a third follow-up.Notes
info.versionbump (stays2025-10-13); noservers.urlchange.openapi.yaml,mintlify/openapi.yaml) vianpm run build:openapi;npm run lint:openapipasses with 0 errors (same 663-problem baseline asmain, all pre-existing warnings/infos).GET /operations/{operationId}poll endpoint, per the resubmit-until-terminal retrieval contract that superseded poll-an-id. [grid-api] async-ops: GET /operations + wallet_operation webhooks (DRAFT — do not merge until async feature deploys) #559 will be closed in favor of this.typein lowercase (wallet_operation.completed/wallet_operation.failed) as an interim placeholder pending this spec change (see the# grid-api spec change is deferrednote at the call site) — tracked by #31886, not changed here.Status
Ready for review. Merged onto latest
main(clean, no conflicts).make build/make lintboth green.Sequencing:
grid-apiclient (grid-api/update_schema.sh) from the new spec.typecasing to the real generated enum, and wirerequestId/resourceId/resourceTypeintooperation_webhook.pyfrom the already-persisted activity fields above.