diff --git a/mintlify/api-reference/async-operations.mdx b/mintlify/api-reference/async-operations.mdx new file mode 100644 index 00000000..e557eb6e --- /dev/null +++ b/mintlify/api-reference/async-operations.mdx @@ -0,0 +1,131 @@ +--- +title: "Asynchronous operations" +description: "Track long-running embedded-wallet operations with the operations endpoint and wallet_operation webhooks" +icon: "/images/icons/arrows-repeat-circle.svg" +"og:image": "/images/og/og-api-reference.png" +--- + +Some embedded-wallet operations cannot complete within a single request. When an operation is accepted but still in flight, Grid responds with `202 Accepted` and an `operationId` of the form `Operation:`. You then learn the terminal outcome in one of two ways: + +- **Poll** the operation with `GET /operations/{operationId}`. +- **Receive** a `WALLET_OPERATION.*` webhook when the operation reaches a terminal state. + +The two are companions: the webhook pushes the outcome, and the endpoint pulls it on demand. Both report the same terminal state, so you can rely on whichever fits your integration — or use the webhook as the primary signal and the endpoint as a reconciliation fallback. + +## Operation types + +| `operationType` | Description | Returns data | +|-----------------|-------------|--------------| +| `auth_credential.delete` | Remove an end-user authentication credential. | No | +| `session.revoke` | Revoke an active embedded-wallet session. | No | +| `wallet.export` | Export the encrypted wallet credentials bundle. | Yes | + +State-changing operations (`auth_credential.delete`, `session.revoke`) carry no result payload. Data-returning operations (`wallet.export`) expose their result through `GET /operations/{operationId}` only — the result is **never** delivered in the webhook. + +## Polling an operation + +Call `GET /operations/{operationId}` with the `operationId` from the original `202` response. The `status` field is one of `PROCESSING`, `COMPLETED`, or `FAILED`. + +```bash +curl -X GET "$GRID_BASE_URL/operations/Operation:019542f5-b3e7-1d02-0000-000000000099" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" +``` + +**While the operation is in flight (`200`):** + +```json +{ + "id": "Operation:019542f5-b3e7-1d02-0000-000000000099", + "operationType": "wallet.export", + "status": "PROCESSING", + "createdAt": "2026-06-08T14:30:00Z", + "updatedAt": "2026-06-08T14:30:00Z", + "completedAt": null, + "result": null, + "error": null +} +``` + +**Once a data-returning operation completes (`200`):** + +```json +{ + "id": "Operation:019542f5-b3e7-1d02-0000-000000000099", + "operationType": "wallet.export", + "status": "COMPLETED", + "createdAt": "2026-06-08T14:30:00Z", + "updatedAt": "2026-06-08T14:31:00Z", + "completedAt": "2026-06-08T14:31:00Z", + "result": { + "type": "wallet.export", + "encryptedWalletCredentials": "{\"version\":\"v1.0.0\",\"data\":\"...\"}" + }, + "error": null +} +``` + +For `wallet.export`, `result.encryptedWalletCredentials` is the HPKE-encrypted wallet credentials bundle, encrypted to the `targetPublicKey` the client committed to when the export was requested. It is re-fetched on demand and never stored by Grid. For `auth_credential.delete` and `session.revoke`, `result` is always `null`. + +**If the operation fails (`200`):** + +```json +{ + "id": "Operation:019542f5-b3e7-1d02-0000-00000000009a", + "operationType": "session.revoke", + "status": "FAILED", + "createdAt": "2026-06-08T14:30:00Z", + "updatedAt": "2026-06-08T14:32:00Z", + "completedAt": "2026-06-08T14:32:00Z", + "result": null, + "error": { + "code": "DeleteApiKeysFailed" + } +} +``` + +On failure, `error.code` is a machine-readable failure code you can branch on. + +## Receiving the webhook + +When an operation reaches a terminal state, Grid sends a `wallet_operation` webhook: `WALLET_OPERATION.COMPLETED` on success and `WALLET_OPERATION.FAILED` on failure. Route purely on `type`, then use `data.operationType` to identify the specific operation. + +The webhook carries no sensitive result material — only the `operationId`, `operationType`, `status`, and (on failure) `error.code`. To retrieve the result of a data-returning operation such as `wallet.export`, call `GET /operations/{operationId}`. + +**Completed:** + +```json +{ + "id": "Webhook:019542f5-b3e7-1d02-0000-000000000040", + "type": "WALLET_OPERATION.COMPLETED", + "timestamp": "2026-06-08T14:31:00Z", + "data": { + "operationId": "Operation:019542f5-b3e7-1d02-0000-000000000099", + "operationType": "wallet.export", + "status": "completed" + } +} +``` + +**Failed:** + +```json +{ + "id": "Webhook:019542f5-b3e7-1d02-0000-000000000041", + "type": "WALLET_OPERATION.FAILED", + "timestamp": "2026-06-08T14:32:00Z", + "data": { + "operationId": "Operation:019542f5-b3e7-1d02-0000-00000000009a", + "operationType": "session.revoke", + "status": "failed", + "error": { + "code": "DeleteApiKeysFailed" + } + } +} +``` + +`wallet_operation` webhooks are signed and retried like every other Grid webhook — verify the `X-Grid-Signature` header and dedupe on the webhook `id`. See [Webhooks](/api-reference/webhooks) for the verification process and retry policy. + + + The webhook `data.status` is lowercase (`completed` / `failed`), while the `status` returned by `GET /operations/{operationId}` is uppercase (`PROCESSING` / `COMPLETED` / `FAILED`). Branch on the webhook `type` rather than `data.status` where possible. + diff --git a/mintlify/docs.json b/mintlify/docs.json index 5bc96138..29ceef83 100644 --- a/mintlify/docs.json +++ b/mintlify/docs.json @@ -380,6 +380,7 @@ "api-reference/terminology", "api-reference/authentication", "api-reference/webhooks", + "api-reference/async-operations", "api-reference/sandbox-testing", "api-reference/sdks" ] diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 634d37ee..4363c253 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -55,6 +55,8 @@ tags: description: Endpoints called by the agent itself using its own credentials (obtained via device code redemption). Scoped to the agent's associated customer — all requests automatically operate on behalf of that customer and are subject to the agent's policy. When an action requires approval, the resulting transaction enters a pending state and must be approved by the platform via `POST /transactions/{transactionId}/approve`. - name: Cards description: Card management endpoints. Issue debit cards against an internal account, freeze / unfreeze, close, manage card funding sources, and list card transactions. + - name: Operations + description: Endpoints for polling the status and result of asynchronous embedded-wallet operations (e.g. `wallet.export`, `auth_credential.delete`, `session.revoke`) by the `operationId` returned in a prior `202` response. paths: /config: get: @@ -6776,6 +6778,58 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /operations/{operationId}: + parameters: + - name: operationId + in: path + description: The `Operation:` id returned in a prior `202` response. + required: true + schema: + type: string + get: + summary: Get operation by ID + description: | + Retrieve the status of an asynchronous operation by the `operationId` returned in a prior `202` response, and — for a completed data-returning operation (`wallet.export`) — its result. + + This is the poll companion to the `WALLET_OPERATION.*` webhook: the webhook pushes the terminal outcome, this endpoint pulls it on demand. + + `status` is one of `PROCESSING`, `COMPLETED`, or `FAILED`. For a completed `wallet.export`, `result.encryptedWalletCredentials` carries the HPKE-encrypted wallet credentials, re-fetched on demand and never stored by Grid. For state-changing operations (`auth_credential.delete`, `session.revoke`) `result` is always `null`. On `FAILED`, `error.code` describes the failure. + operationId: getOperationById + tags: + - Operations + security: + - BasicAuth: [] + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Operation' + '400': + description: Malformed operation id + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Operation not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' webhooks: agent-action: post: @@ -7887,6 +7941,82 @@ webhooks: application/json: schema: $ref: '#/components/schemas/Error409' + wallet-operation: + post: + summary: Wallet operation completed or failed + description: | + Webhook that is called when an asynchronous embedded-wallet operation reaches a terminal state. Fires `WALLET_OPERATION.COMPLETED` on terminal success and `WALLET_OPERATION.FAILED` on terminal failure. + + The specific operation is carried in `data.operationType` (`auth_credential.delete`, `session.revoke`, or `wallet.export`). The webhook carries no sensitive result material — only the `operationId`, `operationType`, `status`, and (on failure) `error.code`. For a data-returning operation (`wallet.export`), retrieve the result with `GET /operations/{operationId}`; it is never delivered in the webhook. + + This endpoint should be implemented by clients of the Grid API. + + ### Authentication + + The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid. + To verify the signature: + 1. Get the Grid public key provided to you during integration + 2. Decode the base64 signature from the header + 3. Create a SHA-256 hash of the request body + 4. Verify the signature using the public key and the hash + + If the signature verification succeeds, the webhook is authentic. If not, it should be rejected. + operationId: walletOperationWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WalletOperationWebhook' + examples: + completed: + summary: Wallet export completed + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000040 + type: WALLET_OPERATION.COMPLETED + timestamp: '2026-06-08T14:31:00Z' + data: + operationId: Operation:019542f5-b3e7-1d02-0000-000000000099 + operationType: wallet.export + status: completed + failed: + summary: Session revoke failed terminally + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000041 + type: WALLET_OPERATION.FAILED + timestamp: '2026-06-08T14:32:00Z' + data: + operationId: Operation:019542f5-b3e7-1d02-0000-00000000009a + operationType: session.revoke + status: failed + error: + code: DeleteApiKeysFailed + responses: + '200': + description: | + Webhook received successfully + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized - Signature validation failed + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '409': + description: Conflict - Webhook has already been processed (duplicate id) + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' components: securitySchemes: BasicAuth: @@ -18469,6 +18599,90 @@ components: description: Return amount in the smallest unit of the transaction's currency. Must be less than or equal to the net settled amount (settled minus previously-refunded). exclusiveMinimum: 0 example: 1500 + OperationResult: + type: object + description: | + Result of a completed data-returning operation. Tagged by `type` so the result shape can grow per operation type without a breaking change. Present only when `status` is `COMPLETED` and the operation is data-returning (`wallet.export`); `null` otherwise. + required: + - type + properties: + type: + type: string + description: The operation type this result belongs to. + enum: + - wallet.export + example: wallet.export + encryptedWalletCredentials: + type: string + description: | + For `wallet.export`: the HPKE-encrypted wallet credentials bundle, encrypted to the `targetPublicKey` the client committed to when the export was requested. Re-fetched on demand; never stored by Grid. + example: '{"version":"v1.0.0","data":"..."}' + OperationError: + type: object + required: + - code + properties: + code: + type: string + description: Machine-readable failure code for a `FAILED` operation. + example: DeleteApiKeysFailed + Operation: + type: object + description: An asynchronous embedded-wallet operation and its current status. + required: + - id + - operationType + - status + - createdAt + - updatedAt + properties: + id: + type: string + description: The `Operation:` id (equal to the `operationId` returned in the prior `202`). + example: Operation:019542f5-b3e7-1d02-0000-000000000099 + operationType: + type: string + description: The kind of operation. + enum: + - auth_credential.delete + - session.revoke + - wallet.export + example: wallet.export + status: + type: string + description: Current status of the operation. + enum: + - PROCESSING + - COMPLETED + - FAILED + example: COMPLETED + createdAt: + type: string + format: date-time + description: When the operation was created. + example: '2026-06-08T14:30:00Z' + updatedAt: + type: string + format: date-time + description: When the operation was last updated. + example: '2026-06-08T14:31:00Z' + completedAt: + type: + - string + - 'null' + format: date-time + description: When the operation reached a terminal state; `null` while `PROCESSING`. + example: '2026-06-08T14:31:00Z' + result: + anyOf: + - $ref: '#/components/schemas/OperationResult' + - type: 'null' + description: Present only for a `COMPLETED` data-returning operation; `null` otherwise. + error: + anyOf: + - $ref: '#/components/schemas/OperationError' + - type: 'null' + description: Present only when `status` is `FAILED`; `null` otherwise. WebhookType: type: string enum: @@ -18503,6 +18717,8 @@ components: - AGENT_ACTION.PENDING_APPROVAL - CARD.STATE_CHANGE - CARD.FUNDING_SOURCE_CHANGE + - WALLET_OPERATION.COMPLETED + - WALLET_OPERATION.FAILED - TEST description: Type of webhook event in OBJECT.EVENT dot-notation. The part before the dot identifies the resource, the part after identifies the event. This lets consumers route purely on type without inspecting data.status. BaseWebhook: @@ -18723,6 +18939,51 @@ components: type: string enum: - CARD.FUNDING_SOURCE_CHANGE + WalletOperationWebhookData: + type: object + required: + - operationId + - operationType + - status + properties: + operationId: + type: string + description: The `Operation:` id, resolvable via `GET /operations/{operationId}`. + example: Operation:019542f5-b3e7-1d02-0000-000000000099 + operationType: + type: string + description: The kind of operation that reached a terminal state. + enum: + - auth_credential.delete + - session.revoke + - wallet.export + example: wallet.export + status: + type: string + description: Terminal status of the operation. + enum: + - completed + - failed + example: completed + error: + anyOf: + - $ref: '#/components/schemas/OperationError' + - type: 'null' + description: Present only on `failed`; `null` otherwise. + WalletOperationWebhook: + allOf: + - $ref: '#/components/schemas/BaseWebhook' + - type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/WalletOperationWebhookData' + type: + type: string + enum: + - WALLET_OPERATION.COMPLETED + - WALLET_OPERATION.FAILED requestBodies: DocumentUploadRequestBody: required: true diff --git a/mintlify/snippets/webhooks.mdx b/mintlify/snippets/webhooks.mdx index 6ee767ba..60ad149d 100644 --- a/mintlify/snippets/webhooks.mdx +++ b/mintlify/snippets/webhooks.mdx @@ -203,3 +203,4 @@ The Grid API will retry webhooks with the following policy based on the webhook | `INVITATION.*` | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) | | `CUSTOMER.*` | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) | | `ACCOUNT.*` | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) | +| `WALLET_OPERATION.*` | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks). The terminal outcome can also be polled with `GET /operations/{operationId}` — see [Asynchronous operations](/api-reference/async-operations) | diff --git a/openapi.yaml b/openapi.yaml index 634d37ee..4363c253 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -55,6 +55,8 @@ tags: description: Endpoints called by the agent itself using its own credentials (obtained via device code redemption). Scoped to the agent's associated customer — all requests automatically operate on behalf of that customer and are subject to the agent's policy. When an action requires approval, the resulting transaction enters a pending state and must be approved by the platform via `POST /transactions/{transactionId}/approve`. - name: Cards description: Card management endpoints. Issue debit cards against an internal account, freeze / unfreeze, close, manage card funding sources, and list card transactions. + - name: Operations + description: Endpoints for polling the status and result of asynchronous embedded-wallet operations (e.g. `wallet.export`, `auth_credential.delete`, `session.revoke`) by the `operationId` returned in a prior `202` response. paths: /config: get: @@ -6776,6 +6778,58 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /operations/{operationId}: + parameters: + - name: operationId + in: path + description: The `Operation:` id returned in a prior `202` response. + required: true + schema: + type: string + get: + summary: Get operation by ID + description: | + Retrieve the status of an asynchronous operation by the `operationId` returned in a prior `202` response, and — for a completed data-returning operation (`wallet.export`) — its result. + + This is the poll companion to the `WALLET_OPERATION.*` webhook: the webhook pushes the terminal outcome, this endpoint pulls it on demand. + + `status` is one of `PROCESSING`, `COMPLETED`, or `FAILED`. For a completed `wallet.export`, `result.encryptedWalletCredentials` carries the HPKE-encrypted wallet credentials, re-fetched on demand and never stored by Grid. For state-changing operations (`auth_credential.delete`, `session.revoke`) `result` is always `null`. On `FAILED`, `error.code` describes the failure. + operationId: getOperationById + tags: + - Operations + security: + - BasicAuth: [] + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Operation' + '400': + description: Malformed operation id + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Operation not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' webhooks: agent-action: post: @@ -7887,6 +7941,82 @@ webhooks: application/json: schema: $ref: '#/components/schemas/Error409' + wallet-operation: + post: + summary: Wallet operation completed or failed + description: | + Webhook that is called when an asynchronous embedded-wallet operation reaches a terminal state. Fires `WALLET_OPERATION.COMPLETED` on terminal success and `WALLET_OPERATION.FAILED` on terminal failure. + + The specific operation is carried in `data.operationType` (`auth_credential.delete`, `session.revoke`, or `wallet.export`). The webhook carries no sensitive result material — only the `operationId`, `operationType`, `status`, and (on failure) `error.code`. For a data-returning operation (`wallet.export`), retrieve the result with `GET /operations/{operationId}`; it is never delivered in the webhook. + + This endpoint should be implemented by clients of the Grid API. + + ### Authentication + + The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid. + To verify the signature: + 1. Get the Grid public key provided to you during integration + 2. Decode the base64 signature from the header + 3. Create a SHA-256 hash of the request body + 4. Verify the signature using the public key and the hash + + If the signature verification succeeds, the webhook is authentic. If not, it should be rejected. + operationId: walletOperationWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WalletOperationWebhook' + examples: + completed: + summary: Wallet export completed + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000040 + type: WALLET_OPERATION.COMPLETED + timestamp: '2026-06-08T14:31:00Z' + data: + operationId: Operation:019542f5-b3e7-1d02-0000-000000000099 + operationType: wallet.export + status: completed + failed: + summary: Session revoke failed terminally + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000041 + type: WALLET_OPERATION.FAILED + timestamp: '2026-06-08T14:32:00Z' + data: + operationId: Operation:019542f5-b3e7-1d02-0000-00000000009a + operationType: session.revoke + status: failed + error: + code: DeleteApiKeysFailed + responses: + '200': + description: | + Webhook received successfully + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized - Signature validation failed + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '409': + description: Conflict - Webhook has already been processed (duplicate id) + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' components: securitySchemes: BasicAuth: @@ -18469,6 +18599,90 @@ components: description: Return amount in the smallest unit of the transaction's currency. Must be less than or equal to the net settled amount (settled minus previously-refunded). exclusiveMinimum: 0 example: 1500 + OperationResult: + type: object + description: | + Result of a completed data-returning operation. Tagged by `type` so the result shape can grow per operation type without a breaking change. Present only when `status` is `COMPLETED` and the operation is data-returning (`wallet.export`); `null` otherwise. + required: + - type + properties: + type: + type: string + description: The operation type this result belongs to. + enum: + - wallet.export + example: wallet.export + encryptedWalletCredentials: + type: string + description: | + For `wallet.export`: the HPKE-encrypted wallet credentials bundle, encrypted to the `targetPublicKey` the client committed to when the export was requested. Re-fetched on demand; never stored by Grid. + example: '{"version":"v1.0.0","data":"..."}' + OperationError: + type: object + required: + - code + properties: + code: + type: string + description: Machine-readable failure code for a `FAILED` operation. + example: DeleteApiKeysFailed + Operation: + type: object + description: An asynchronous embedded-wallet operation and its current status. + required: + - id + - operationType + - status + - createdAt + - updatedAt + properties: + id: + type: string + description: The `Operation:` id (equal to the `operationId` returned in the prior `202`). + example: Operation:019542f5-b3e7-1d02-0000-000000000099 + operationType: + type: string + description: The kind of operation. + enum: + - auth_credential.delete + - session.revoke + - wallet.export + example: wallet.export + status: + type: string + description: Current status of the operation. + enum: + - PROCESSING + - COMPLETED + - FAILED + example: COMPLETED + createdAt: + type: string + format: date-time + description: When the operation was created. + example: '2026-06-08T14:30:00Z' + updatedAt: + type: string + format: date-time + description: When the operation was last updated. + example: '2026-06-08T14:31:00Z' + completedAt: + type: + - string + - 'null' + format: date-time + description: When the operation reached a terminal state; `null` while `PROCESSING`. + example: '2026-06-08T14:31:00Z' + result: + anyOf: + - $ref: '#/components/schemas/OperationResult' + - type: 'null' + description: Present only for a `COMPLETED` data-returning operation; `null` otherwise. + error: + anyOf: + - $ref: '#/components/schemas/OperationError' + - type: 'null' + description: Present only when `status` is `FAILED`; `null` otherwise. WebhookType: type: string enum: @@ -18503,6 +18717,8 @@ components: - AGENT_ACTION.PENDING_APPROVAL - CARD.STATE_CHANGE - CARD.FUNDING_SOURCE_CHANGE + - WALLET_OPERATION.COMPLETED + - WALLET_OPERATION.FAILED - TEST description: Type of webhook event in OBJECT.EVENT dot-notation. The part before the dot identifies the resource, the part after identifies the event. This lets consumers route purely on type without inspecting data.status. BaseWebhook: @@ -18723,6 +18939,51 @@ components: type: string enum: - CARD.FUNDING_SOURCE_CHANGE + WalletOperationWebhookData: + type: object + required: + - operationId + - operationType + - status + properties: + operationId: + type: string + description: The `Operation:` id, resolvable via `GET /operations/{operationId}`. + example: Operation:019542f5-b3e7-1d02-0000-000000000099 + operationType: + type: string + description: The kind of operation that reached a terminal state. + enum: + - auth_credential.delete + - session.revoke + - wallet.export + example: wallet.export + status: + type: string + description: Terminal status of the operation. + enum: + - completed + - failed + example: completed + error: + anyOf: + - $ref: '#/components/schemas/OperationError' + - type: 'null' + description: Present only on `failed`; `null` otherwise. + WalletOperationWebhook: + allOf: + - $ref: '#/components/schemas/BaseWebhook' + - type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/WalletOperationWebhookData' + type: + type: string + enum: + - WALLET_OPERATION.COMPLETED + - WALLET_OPERATION.FAILED requestBodies: DocumentUploadRequestBody: required: true diff --git a/openapi/components/schemas/operations/Operation.yaml b/openapi/components/schemas/operations/Operation.yaml new file mode 100644 index 00000000..e27242c2 --- /dev/null +++ b/openapi/components/schemas/operations/Operation.yaml @@ -0,0 +1,58 @@ +type: object +description: An asynchronous embedded-wallet operation and its current status. +required: + - id + - operationType + - status + - createdAt + - updatedAt +properties: + id: + type: string + description: >- + The `Operation:` id (equal to the `operationId` returned in the + prior `202`). + example: Operation:019542f5-b3e7-1d02-0000-000000000099 + operationType: + type: string + description: The kind of operation. + enum: + - auth_credential.delete + - session.revoke + - wallet.export + example: wallet.export + status: + type: string + description: Current status of the operation. + enum: + - PROCESSING + - COMPLETED + - FAILED + example: COMPLETED + createdAt: + type: string + format: date-time + description: When the operation was created. + example: '2026-06-08T14:30:00Z' + updatedAt: + type: string + format: date-time + description: When the operation was last updated. + example: '2026-06-08T14:31:00Z' + completedAt: + type: + - string + - 'null' + format: date-time + description: When the operation reached a terminal state; `null` while `PROCESSING`. + example: '2026-06-08T14:31:00Z' + result: + anyOf: + - $ref: ./OperationResult.yaml + - type: 'null' + description: Present only for a `COMPLETED` data-returning operation; `null` otherwise. + error: + anyOf: + - $ref: ./OperationError.yaml + - type: 'null' + description: Present only when `status` is `FAILED`; `null` otherwise. diff --git a/openapi/components/schemas/operations/OperationError.yaml b/openapi/components/schemas/operations/OperationError.yaml new file mode 100644 index 00000000..7a453334 --- /dev/null +++ b/openapi/components/schemas/operations/OperationError.yaml @@ -0,0 +1,8 @@ +type: object +required: + - code +properties: + code: + type: string + description: Machine-readable failure code for a `FAILED` operation. + example: DeleteApiKeysFailed diff --git a/openapi/components/schemas/operations/OperationResult.yaml b/openapi/components/schemas/operations/OperationResult.yaml new file mode 100644 index 00000000..375dd723 --- /dev/null +++ b/openapi/components/schemas/operations/OperationResult.yaml @@ -0,0 +1,22 @@ +type: object +description: > + Result of a completed data-returning operation. Tagged by `type` so the + result shape can grow per operation type without a breaking change. Present + only when `status` is `COMPLETED` and the operation is data-returning + (`wallet.export`); `null` otherwise. +required: + - type +properties: + type: + type: string + description: The operation type this result belongs to. + enum: + - wallet.export + example: wallet.export + encryptedWalletCredentials: + type: string + description: > + For `wallet.export`: the HPKE-encrypted wallet credentials bundle, + encrypted to the `targetPublicKey` the client committed to when the + export was requested. Re-fetched on demand; never stored by Grid. + example: '{"version":"v1.0.0","data":"..."}' diff --git a/openapi/components/schemas/webhooks/WalletOperationWebhook.yaml b/openapi/components/schemas/webhooks/WalletOperationWebhook.yaml new file mode 100644 index 00000000..a7dd45d8 --- /dev/null +++ b/openapi/components/schemas/webhooks/WalletOperationWebhook.yaml @@ -0,0 +1,13 @@ +allOf: + - $ref: ./BaseWebhook.yaml + - type: object + required: + - data + properties: + data: + $ref: ./WalletOperationWebhookData.yaml + type: + type: string + enum: + - WALLET_OPERATION.COMPLETED + - WALLET_OPERATION.FAILED diff --git a/openapi/components/schemas/webhooks/WalletOperationWebhookData.yaml b/openapi/components/schemas/webhooks/WalletOperationWebhookData.yaml new file mode 100644 index 00000000..218a8987 --- /dev/null +++ b/openapi/components/schemas/webhooks/WalletOperationWebhookData.yaml @@ -0,0 +1,30 @@ +type: object +required: + - operationId + - operationType + - status +properties: + operationId: + type: string + description: The `Operation:` id, resolvable via `GET /operations/{operationId}`. + example: Operation:019542f5-b3e7-1d02-0000-000000000099 + operationType: + type: string + description: The kind of operation that reached a terminal state. + enum: + - auth_credential.delete + - session.revoke + - wallet.export + example: wallet.export + status: + type: string + description: Terminal status of the operation. + enum: + - completed + - failed + example: completed + error: + anyOf: + - $ref: ../operations/OperationError.yaml + - type: 'null' + description: Present only on `failed`; `null` otherwise. diff --git a/openapi/components/schemas/webhooks/WebhookType.yaml b/openapi/components/schemas/webhooks/WebhookType.yaml index c587cc6b..9f8e498a 100644 --- a/openapi/components/schemas/webhooks/WebhookType.yaml +++ b/openapi/components/schemas/webhooks/WebhookType.yaml @@ -31,6 +31,8 @@ enum: - AGENT_ACTION.PENDING_APPROVAL - CARD.STATE_CHANGE - CARD.FUNDING_SOURCE_CHANGE + - WALLET_OPERATION.COMPLETED + - WALLET_OPERATION.FAILED - TEST description: >- Type of webhook event in OBJECT.EVENT dot-notation. The part before the dot diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 44648e9c..9147e395 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -77,6 +77,12 @@ tags: Card management endpoints. Issue debit cards against an internal account, freeze / unfreeze, close, manage card funding sources, and list card transactions. + - name: Operations + description: >- + Endpoints for polling the status and result of asynchronous + embedded-wallet operations (e.g. `wallet.export`, + `auth_credential.delete`, `session.revoke`) by the `operationId` returned + in a prior `202` response. servers: - url: https://api.lightspark.com/grid/2025-10-13 description: Production server @@ -278,6 +284,8 @@ paths: $ref: paths/sandbox/cards/sandbox_cards_{id}_simulate_clearing.yaml /sandbox/cards/{id}/simulate/return: $ref: paths/sandbox/cards/sandbox_cards_{id}_simulate_return.yaml + /operations/{operationId}: + $ref: paths/operations/operations_{operationId}.yaml webhooks: agent-action: $ref: webhooks/agent-action.yaml @@ -301,6 +309,8 @@ webhooks: $ref: webhooks/card-state-change.yaml card-funding-source-change: $ref: webhooks/card-funding-source-change.yaml + wallet-operation: + $ref: webhooks/wallet-operation.yaml security: - BasicAuth: [] - AgentAuth: [] diff --git a/openapi/paths/operations/operations_{operationId}.yaml b/openapi/paths/operations/operations_{operationId}.yaml new file mode 100644 index 00000000..5502710e --- /dev/null +++ b/openapi/paths/operations/operations_{operationId}.yaml @@ -0,0 +1,61 @@ +parameters: + - name: operationId + in: path + description: The `Operation:` id returned in a prior `202` response. + required: true + schema: + type: string +get: + summary: Get operation by ID + description: > + Retrieve the status of an asynchronous operation by the `operationId` + returned in a prior `202` response, and — for a completed data-returning + operation (`wallet.export`) — its result. + + + This is the poll companion to the `WALLET_OPERATION.*` webhook: the webhook + pushes the terminal outcome, this endpoint pulls it on demand. + + + `status` is one of `PROCESSING`, `COMPLETED`, or `FAILED`. For a completed + `wallet.export`, `result.encryptedWalletCredentials` carries the + HPKE-encrypted wallet credentials, re-fetched on demand and never stored by + Grid. For state-changing operations (`auth_credential.delete`, + `session.revoke`) `result` is always `null`. On `FAILED`, `error.code` + describes the failure. + operationId: getOperationById + tags: + - Operations + security: + - BasicAuth: [] + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: ../../components/schemas/operations/Operation.yaml + '400': + description: Malformed operation id + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Operation not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '500': + description: Internal service error + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error500.yaml diff --git a/openapi/webhooks/wallet-operation.yaml b/openapi/webhooks/wallet-operation.yaml new file mode 100644 index 00000000..e1f4ef3a --- /dev/null +++ b/openapi/webhooks/wallet-operation.yaml @@ -0,0 +1,94 @@ +post: + summary: Wallet operation completed or failed + description: > + Webhook that is called when an asynchronous embedded-wallet operation + reaches a terminal state. Fires `WALLET_OPERATION.COMPLETED` on terminal + success and `WALLET_OPERATION.FAILED` on terminal failure. + + + The specific operation is carried in `data.operationType` + (`auth_credential.delete`, `session.revoke`, or `wallet.export`). The + webhook carries no sensitive result material — only the `operationId`, + `operationType`, `status`, and (on failure) `error.code`. For a + data-returning operation (`wallet.export`), retrieve the result with + `GET /operations/{operationId}`; it is never delivered in the webhook. + + + This endpoint should be implemented by clients of the Grid API. + + + ### Authentication + + + The webhook includes a signature in the `X-Grid-Signature` header that + allows you to verify that the webhook was sent by Grid. + + To verify the signature: + + 1. Get the Grid public key provided to you during integration + + 2. Decode the base64 signature from the header + + 3. Create a SHA-256 hash of the request body + + 4. Verify the signature using the public key and the hash + + + If the signature verification succeeds, the webhook is authentic. If not, it + should be rejected. + operationId: walletOperationWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../components/schemas/webhooks/WalletOperationWebhook.yaml + examples: + completed: + summary: Wallet export completed + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000040 + type: WALLET_OPERATION.COMPLETED + timestamp: '2026-06-08T14:31:00Z' + data: + operationId: Operation:019542f5-b3e7-1d02-0000-000000000099 + operationType: wallet.export + status: completed + failed: + summary: Session revoke failed terminally + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000041 + type: WALLET_OPERATION.FAILED + timestamp: '2026-06-08T14:32:00Z' + data: + operationId: Operation:019542f5-b3e7-1d02-0000-00000000009a + operationType: session.revoke + status: failed + error: + code: DeleteApiKeysFailed + responses: + '200': + description: > + Webhook received successfully + '400': + description: Bad request + content: + application/json: + schema: + $ref: ../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized - Signature validation failed + content: + application/json: + schema: + $ref: ../components/schemas/errors/Error401.yaml + '409': + description: Conflict - Webhook has already been processed (duplicate id) + content: + application/json: + schema: + $ref: ../components/schemas/errors/Error409.yaml