diff --git a/.stainless/stainless.yml b/.stainless/stainless.yml index 06457154..4f79aa4b 100644 --- a/.stainless/stainless.yml +++ b/.stainless/stainless.yml @@ -27,13 +27,14 @@ targets: publish: npm: auth_method: oidc - # jsr: - # package_name: "@lightsparkdev/grid" + jsr: + package_name: "@lightsparkdev/grid" options: mcp_server: + package_name: "@lightsparkdev/grid-mcp" + enable_all_resources: true host: stainless: true - package_name: "@lightsparkdev/grid-mcp" kotlin: edition: kotlin.2025-10-08 reverse_domain: com.lightspark.grid @@ -109,21 +110,35 @@ resources: subresources: external_accounts: models: - usd_account_info: '#/components/schemas/UsdExternalAccountInfo' - brl_account_info: '#/components/schemas/BrlExternalAccountInfo' - cad_account_info: '#/components/schemas/CadExternalAccountInfo' - mxn_account_info: '#/components/schemas/MxnExternalAccountInfo' - dkk_account_info: '#/components/schemas/DkkExternalAccountInfo' - inr_account_info: '#/components/schemas/InrExternalAccountInfo' - gbp_account_info: '#/components/schemas/GbpExternalAccountInfo' - hkd_account_info: '#/components/schemas/HkdExternalAccountInfo' - idr_account_info: '#/components/schemas/IdrExternalAccountInfo' - myr_account_info: '#/components/schemas/MyrExternalAccountInfo' - ngn_account_info: '#/components/schemas/NgnExternalAccountInfo' - php_account_info: '#/components/schemas/PhpExternalAccountInfo' - sgd_account_info: '#/components/schemas/SgdExternalAccountInfo' - thb_account_info: '#/components/schemas/ThbExternalAccountInfo' - vnd_account_info: '#/components/schemas/VndExternalAccountInfo' + # New ExternalAccountInfo models (13) + brl_external_account_info: "#/components/schemas/BrlExternalAccountInfo" + dkk_external_account_info: "#/components/schemas/DkkExternalAccountInfo" + gbp_external_account_info: "#/components/schemas/GbpExternalAccountInfo" + hkd_external_account_info: "#/components/schemas/HkdExternalAccountInfo" + idr_external_account_info: "#/components/schemas/IdrExternalAccountInfo" + inr_external_account_info: "#/components/schemas/InrExternalAccountInfo" + mxn_external_account_info: "#/components/schemas/MxnExternalAccountInfo" + myr_external_account_info: "#/components/schemas/MyrExternalAccountInfo" + php_external_account_info: "#/components/schemas/PhpExternalAccountInfo" + sgd_external_account_info: "#/components/schemas/SgdExternalAccountInfo" + thb_external_account_info: "#/components/schemas/ThbExternalAccountInfo" + usd_external_account_info: "#/components/schemas/UsdExternalAccountInfo" + vnd_external_account_info: "#/components/schemas/VndExternalAccountInfo" + + # New Beneficiary models (13) + brl_beneficiary: "#/components/schemas/BrlBeneficiary" + dkk_beneficiary: "#/components/schemas/DkkBeneficiary" + gbp_beneficiary: "#/components/schemas/GbpBeneficiary" + hkd_beneficiary: "#/components/schemas/HkdBeneficiary" + idr_beneficiary: "#/components/schemas/IdrBeneficiary" + inr_beneficiary: "#/components/schemas/InrBeneficiary" + mxn_beneficiary: "#/components/schemas/MxnBeneficiary" + myr_beneficiary: "#/components/schemas/MyrBeneficiary" + php_beneficiary: "#/components/schemas/PhpBeneficiary" + sgd_beneficiary: "#/components/schemas/SgdBeneficiary" + thb_beneficiary: "#/components/schemas/ThbBeneficiary" + usd_beneficiary: "#/components/schemas/UsdBeneficiary" + vnd_beneficiary: "#/components/schemas/VndBeneficiary" spark_wallet_info: '#/components/schemas/SparkWalletExternalAccountInfo' solana_wallet_info: '#/components/schemas/SolanaWalletExternalAccountInfo' tron_wallet_info: '#/components/schemas/TronWalletExternalAccountInfo' @@ -136,8 +151,10 @@ resources: external_account_create: '#/components/schemas/ExternalAccountCreateRequest' # base_external_account_info: "#/components/schemas/BaseExternalAccountInfo" # base_beneficiary: "#/components/schemas/BaseBeneficiary" - beneficiary_one_of: "#/components/schemas/BeneficiaryOneOf" external_account_info_one_of: "#/components/schemas/ExternalAccountInfoOneOf" + business_beneficiary: "#/components/schemas/BusinessBeneficiary" + address: "#/components/schemas/Address" + beneficiary_verified_data: "#/components/schemas/BeneficiaryVerifiedData" methods: list: get /customers/external-accounts create: post /customers/external-accounts @@ -167,7 +184,7 @@ resources: transfer_in: models: transaction: '#/components/schemas/Transaction' - base_transaction_destination: "#/components/schemas/BaseTransactionDestination" + #base_transaction_destination: "#/components/schemas/BaseTransactionDestination" methods: create: post /transfer-in @@ -189,10 +206,6 @@ resources: payment_instructions: '#/components/schemas/PaymentInstructions' outgoing_rate_details: '#/components/schemas/OutgoingRateDetails' quote: '#/components/schemas/Quote' - base_payment_account_info: "#/components/schemas/BasePaymentAccountInfo" - base_quote_source: "#/components/schemas/BaseQuoteSource" - quote_source_one_of: "#/components/schemas/QuoteSourceOneOf" - base_destination: "#/components/schemas/BaseDestination" quote_destination_one_of: "#/components/schemas/QuoteDestinationOneOf" methods: retrieve: get /quotes/{quoteId} @@ -203,11 +216,10 @@ resources: transactions: models: transaction_type: '#/components/schemas/TransactionType' - transaction_destination_one_of: '#/components/schemas/TransactionDestinationOneOf' - counterparty_information: '#/components/schemas/CounterpartyInformation' incoming_transaction: '#/components/schemas/IncomingTransaction' + outgoing_transaction: '#/components/schemas/OutgoingTransaction' transaction_status: '#/components/schemas/TransactionStatus' - base_transaction_source: "#/components/schemas/BaseTransactionSource" + #base_transaction_source: "#/components/schemas/BaseTransactionSource" transaction_source_one_of: "#/components/schemas/TransactionSourceOneOf" methods: list: get /transactions @@ -215,13 +227,6 @@ resources: approve: post /transactions/{transactionId}/approve reject: post /transactions/{transactionId}/reject - webhooks: - methods: - unwrap: - type: webhook_unwrap - discriminator: type - - invitations: models: currency_amount: '#/components/schemas/CurrencyAmount' @@ -235,7 +240,7 @@ resources: sandbox: methods: send_funds: post /sandbox/send - send_test_webhook: post /webhooks/test + send_test: post /webhooks/test subresources: uma: methods: @@ -265,6 +270,15 @@ resources: endpoint: get /exchange-rates paginated: false + webhooks: + methods: + unwrap: + type: webhook_unwrap + discriminator: type + $shared: + models: + bulk_customer_import_error_entry: "#/components/schemas/BulkCustomerImportErrorEntry" + settings: # All generated integration tests that hit the prism mock http server are marked # as skipped. Removing this setting or setting it to false enables tests, but @@ -410,30 +424,33 @@ openapi: # - "$.components.schemas.RealtimeFundingQuoteSource.allOf[1].properties" # keys: [ "sourceType" ] - # ── destinationType: transaction destination schemas ── - - command: remove - reason: >- - Remove inline destinationType enums from transaction and quote - destination allOf variants to avoid conflicting types with their - base schemas which define destinationType via shared $ref enums - args: - target: - - "$.components.schemas.AccountTransactionDestination.allOf[0]" - - "$.components.schemas.UmaAddressTransactionDestination.allOf[0]" - keys: [ "$ref" ] - - # ── type: transaction base schema ── - - command: remove - reason: >- - Remove inline type enum from Transaction base schema to avoid - conflicting types when allOf merges with IncomingTransaction and - OutgoingTransaction which define type as single-value enums - args: - target: - - "$.components.schemas.Transaction.properties" - keys: [ "type" ] - - # ── beneficiaryType: beneficiary schemas ── + # # ── destinationType: transaction and quote destination schemas ── + # - command: remove + # reason: >- + # Remove inline destinationType enums from transaction and quote + # destination allOf variants to avoid conflicting types with their + # base schemas which define destinationType via shared $ref enums + # args: + # target: + # - "$.components.schemas.AccountTransactionDestination.allOf[1].properties" + # - "$.components.schemas.UmaAddressTransactionDestination.allOf[1].properties" + # - "$.components.schemas.AccountDestination.allOf[1].properties" + # - "$.components.schemas.UmaAddressDestination.allOf[1].properties" + # - "$.components.schemas.ExternalAccountDetailsDestination.allOf[1].properties" + # keys: [ "destinationType" ] + + # # ── beneficiaryType: beneficiary schemas ── + # - command: remove + # reason: >- + # Remove inline beneficiaryType enums from beneficiary allOf variants + # to avoid conflicting types with BaseBeneficiary which defines + # beneficiaryType via a shared $ref enum + # args: + # target: + # - "$.components.schemas.IndividualBeneficiary.allOf[1].properties" + # - "$.components.schemas.BusinessBeneficiary.allOf[1].properties" + # keys: [ "beneficiaryType" ] + # ── customerType: remove from base schemas ── - command: remove reason: >- Remove customerType $ref from base schemas so the inline single-value @@ -488,22 +505,12 @@ openapi: stripping the accountType discriminator, which causes TS2312 errors args: target: - - "$.components.schemas.PaymentUsdAccountInfo.allOf[0]" - - "$.components.schemas.PaymentBrlAccountInfo.allOf[0]" - - "$.components.schemas.PaymentMxnAccountInfo.allOf[0]" - - "$.components.schemas.PaymentDkkAccountInfo.allOf[0]" - - "$.components.schemas.PaymentEurAccountInfo.allOf[0]" - - "$.components.schemas.PaymentInrAccountInfo.allOf[0]" + - "$.components.schemas.PaymentClabeAccountInfo.allOf[0]" + - "$.components.schemas.PaymentUsAccountInfo.allOf[0]" + - "$.components.schemas.PaymentPixAccountInfo.allOf[0]" + - "$.components.schemas.PaymentIbanAccountInfo.allOf[0]" + - "$.components.schemas.PaymentUpiAccountInfo.allOf[0]" - "$.components.schemas.PaymentNgnAccountInfo.allOf[0]" - - "$.components.schemas.PaymentCadAccountInfo.allOf[0]" - - "$.components.schemas.PaymentGbpAccountInfo.allOf[0]" - - "$.components.schemas.PaymentHkdAccountInfo.allOf[0]" - - "$.components.schemas.PaymentIdrAccountInfo.allOf[0]" - - "$.components.schemas.PaymentMyrAccountInfo.allOf[0]" - - "$.components.schemas.PaymentPhpAccountInfo.allOf[0]" - - "$.components.schemas.PaymentSgdAccountInfo.allOf[0]" - - "$.components.schemas.PaymentThbAccountInfo.allOf[0]" - - "$.components.schemas.PaymentVndAccountInfo.allOf[0]" - "$.components.schemas.PaymentSparkWalletInfo.allOf[0]" - "$.components.schemas.PaymentLightningInvoiceInfo.allOf[0]" - "$.components.schemas.PaymentSolanaWalletInfo.allOf[0]" diff --git a/docs/plans/2026-02-23-webhook-schema-design.md b/docs/plans/2026-02-23-webhook-schema-design.md new file mode 100644 index 00000000..fab86dff --- /dev/null +++ b/docs/plans/2026-02-23-webhook-schema-design.md @@ -0,0 +1,211 @@ +# Webhook Schema Design + +**Date:** 2026-02-23 +**Status:** Approved + +## Problem + +Current webhook schemas are inconsistent: +- Payment webhooks embed full `Transaction` objects under a `transaction` key +- KYC and account webhooks use flat fields at the top level +- Bulk upload uses `bulkCustomerImportJob` as its key +- Invitation embeds full `UmaInvitation` under `invitation` +- The `type` field (e.g., `OUTGOING_PAYMENT`) doesn't distinguish between status transitions — consumers must inspect nested fields + +## Design Decisions + +### 1. Consistent envelope + +Every webhook follows this structure: + +```json +{ + "id": "Webhook:019542f5-...", + "type": "OUTGOING_PAYMENT.COMPLETED", + "timestamp": "2025-08-15T14:32:00Z", + "data": { ... } +} +``` + +| Field | Description | +|---|---| +| `id` | Unique webhook delivery ID (for idempotency). Renamed from `webhookId`. | +| `type` | Status-specific event type in `OBJECT.EVENT` dot-notation (e.g., `OUTGOING_PAYMENT.COMPLETED`) | +| `timestamp` | ISO 8601 timestamp of when the webhook was sent | +| `data` | The resource object — always under `data`, never varying keys | + +### 2. Full resource embed (Stripe-style) + +`data` contains the **full resource object** as the corresponding GET endpoint would return it. This eliminates the need for consumers to make follow-up API calls. + +This is viable because Grid plans to implement API versioning, which will allow resource schemas to evolve without breaking webhook consumers. + +### 3. Status-specific event types + +Each status transition gets its own type 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`, and also enables wildcard subscriptions (e.g., `OUTGOING_PAYMENT.*`). + +## Webhook Type Catalog + +### Transaction webhooks + +`data` = full transaction object (same as `GET /transactions/{id}`) + +| Type | When fired | Notable fields in data | +|---|---|---| +| `OUTGOING_PAYMENT.COMPLETED` | Outgoing payment settles successfully | `sentAmount`, `receivedAmount`, `exchangeRate`, `rateDetails` | +| `OUTGOING_PAYMENT.FAILED` | Outgoing payment fails | `failureReason` | +| `OUTGOING_PAYMENT.REFUNDED` | Outgoing payment is refunded | `refund` | +| `INCOMING_PAYMENT.PENDING` | Incoming payment needs approval | `counterpartyInformation`, `requestedReceiverCustomerInfoFields`, `reconciliationInstructions` | +| `INCOMING_PAYMENT.COMPLETED` | Incoming payment settles | `receivedAmount`, `reconciliationInstructions` | +| `INCOMING_PAYMENT.FAILED` | Incoming payment fails | `failureReason` | + +**Special case — `INCOMING_PAYMENT.PENDING`:** This is an approval webhook, not just a notification. The consumer must respond with: +- `200` to approve +- `202` to process asynchronously (must call approve/reject endpoint within 5s) +- `403` to reject +- `422` to request more counterparty information + +`requestedReceiverCustomerInfoFields` is included inside `data` alongside the transaction fields. + +### KYC webhooks + +`data` = `{ customerId, platformCustomerId, kycStatus }` + +| Type | When fired | +|---|---| +| `KYC.APPROVED` | Customer KYC approved | +| `KYC.REJECTED` | Customer KYC rejected | +| `KYC.EXPIRED` | KYC session expired | +| `KYC.MANUALLY_APPROVED` | Manually approved | +| `KYC.MANUALLY_REJECTED` | Manually rejected | + +### Account webhooks + +`data` = `{ accountId, customerId, platformCustomerId, oldBalance, newBalance }` + +| Type | When fired | +|---|---| +| `ACCOUNT_BALANCE.UPDATED` | Internal account balance changes | + +### Other webhooks + +| Type | Data | When fired | +|---|---|---| +| `INVITATION.CLAIMED` | Full `UmaInvitation` object | Invitation is claimed | +| `BULK_UPLOAD.COMPLETED` | Full `BulkCustomerImportJob` object | Bulk upload succeeds | +| `BULK_UPLOAD.FAILED` | Full `BulkCustomerImportJob` with errors | Bulk upload fails | +| `TEST` | Minimal/empty | Connectivity test | + +## Example Payloads + +### Outgoing payment completed + +```json +{ + "id": "Webhook:019542f5-b3e7-1d02-0000-000000000007", + "type": "OUTGOING_PAYMENT.COMPLETED", + "timestamp": "2025-08-15T14:32:00Z", + "data": { + "id": "Transaction:019542f5-b3e7-1d02-0000-000000000005", + "status": "COMPLETED", + "type": "OUTGOING", + "customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001", + "platformCustomerId": "18d3e5f7b4a9c2", + "destination": {}, + "sentAmount": { "amount": 10550, "currency": { "code": "USD", "name": "United States Dollar", "symbol": "$", "decimals": 2 } }, + "receivedAmount": { "amount": 9706, "currency": { "code": "EUR", "name": "Euro", "symbol": "€", "decimals": 2 } }, + "exchangeRate": 0.92, + "quoteId": "Quote:019542f5-b3e7-1d02-0000-000000000006", + "settledAt": "2025-08-15T14:30:00Z", + "createdAt": "2025-08-15T14:25:18Z", + "description": "Payment for invoice #1234", + "paymentInstructions": [], + "rateDetails": {} + } +} +``` + +### Incoming payment pending (approval) + +```json +{ + "id": "Webhook:019542f5-b3e7-1d02-0000-000000000007", + "type": "INCOMING_PAYMENT.PENDING", + "timestamp": "2025-08-15T14:32:00Z", + "data": { + "id": "Transaction:019542f5-b3e7-1d02-0000-000000000005", + "status": "PENDING", + "type": "INCOMING", + "customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001", + "platformCustomerId": "18d3e5f7b4a9c2", + "destination": {}, + "receivedAmount": { "amount": 50000, "currency": { "code": "USD", "name": "United States Dollar", "symbol": "$", "decimals": 2 } }, + "counterpartyInformation": { + "FULL_NAME": "John Sender", + "BIRTH_DATE": "1985-06-15", + "NATIONALITY": "US" + }, + "reconciliationInstructions": { "reference": "REF-123456789" }, + "requestedReceiverCustomerInfoFields": [ + { "name": "NATIONALITY", "mandatory": true }, + { "name": "ADDRESS", "mandatory": false } + ] + } +} +``` + +### KYC approved + +```json +{ + "id": "Webhook:019542f5-b3e7-1d02-0000-000000000007", + "type": "KYC.APPROVED", + "timestamp": "2025-08-15T14:32:00Z", + "data": { + "customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001", + "platformCustomerId": "...", + "kycStatus": "APPROVED" + } +} +``` + +### Account balance updated + +```json +{ + "id": "Webhook:019542f5-b3e7-1d02-0000-000000000007", + "type": "ACCOUNT_BALANCE.UPDATED", + "timestamp": "2025-08-15T14:32:00Z", + "data": { + "accountId": "Account:019542f5-...", + "customerId": "Customer:019542f5-...", + "platformCustomerId": "...", + "oldBalance": { "amount": 50000, "currency": { "code": "USD", "name": "United States Dollar", "symbol": "$", "decimals": 2 } }, + "newBalance": { "amount": 10000, "currency": { "code": "USD", "name": "United States Dollar", "symbol": "$", "decimals": 2 } } + } +} +``` + +## Migration Notes + +### Breaking changes from current schema +1. `webhookId` → `id` (field rename) +2. Resource keys (`transaction`, `account`, `invitation`, `bulkCustomerImportJob`) → `data` (unified key) +3. `type` values change: `OUTGOING_PAYMENT` → `OUTGOING_PAYMENT.COMPLETED` / `OUTGOING_PAYMENT.FAILED` etc. +4. KYC type splits: `KYC_STATUS` → `KYC.APPROVED`, `KYC.REJECTED`, etc. +5. Account type rename: `ACCOUNT_STATUS` → `ACCOUNT_BALANCE.UPDATED` + +### What stays the same +- Signature verification (`X-Grid-Signature` header) +- Response codes and their semantics (200, 202, 400, 401, 403, 409, 422) +- The incoming payment approval flow (unchanged behavior, just restructured payload) +- All shared component schemas (`CurrencyAmount`, `CounterpartyFieldDefinition`, etc.) + +## Industry Context + +This design follows the Stripe pattern (full resource embed, consistent envelope, event ID for idempotency) adapted for Grid's conventions: +- `OBJECT.EVENT` dot-notation with `UPPER_SNAKE_CASE` segments (e.g., `OUTGOING_PAYMENT.COMPLETED`) +- Status-specific types for routing without inspecting data +- Grid's existing signature verification mechanism + +Stripe makes this work through API versioning — Grid plans to do the same, which will allow resource schemas to evolve without breaking webhook consumers. diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index e149e131..ab984365 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -2148,13 +2148,13 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /webhooks/test: + /sandbox/webhooks/test: post: summary: Send a test webhook description: Send a test webhook to the configured endpoint operationId: sendTestWebhook tags: - - Webhooks + - Sandbox security: - BasicAuth: [] responses: @@ -3059,7 +3059,10 @@ webhooks: pendingPayment: summary: Pending payment example requiring approval value: - transaction: + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: INCOMING_PAYMENT.PENDING + timestamp: '2025-08-15T14:32:00Z' + data: id: Transaction:019542f5-b3e7-1d02-0000-000000000005 status: PENDING type: INCOMING @@ -3074,24 +3077,24 @@ webhooks: decimals: 2 customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 platformCustomerId: 18d3e5f7b4a9c2 - reconciliationInstructions: - reference: REF-123456789 counterpartyInformation: FULL_NAME: John Sender BIRTH_DATE: '1985-06-15' NATIONALITY: US - requestedReceiverCustomerInfoFields: - - name: NATIONALITY - mandatory: true - - name: ADDRESS - mandatory: false - timestamp: '2025-08-15T14:32:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000007 - type: INCOMING_PAYMENT + reconciliationInstructions: + reference: REF-123456789 + requestedReceiverCustomerInfoFields: + - name: NATIONALITY + mandatory: true + - name: ADDRESS + mandatory: false incomingCompletedPayment: summary: Completed payment notification value: - transaction: + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: INCOMING_PAYMENT.COMPLETED + timestamp: '2025-08-15T14:32:00Z' + data: id: Transaction:019542f5-b3e7-1d02-0000-000000000005 status: COMPLETED type: INCOMING @@ -3111,9 +3114,6 @@ webhooks: description: Payment for services reconciliationInstructions: reference: REF-123456789 - timestamp: '2025-08-15T14:32:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000007 - type: INCOMING_PAYMENT responses: '200': description: | @@ -3150,7 +3150,7 @@ webhooks: schema: $ref: '#/components/schemas/IncomingPaymentWebhookForbiddenResponse' '409': - description: Conflict - Webhook has already been processed (duplicate webhookId) + description: Conflict - Webhook has already been processed (duplicate id) content: application/json: schema: @@ -3180,7 +3180,7 @@ webhooks: If the signature verification succeeds, the webhook is authentic. If not, it should be rejected. - This webhook is informational only and is sent when an outgoing payment completes successfully or fails. + This webhook is informational only and is sent when an outgoing payment completes successfully, fails, or is refunded. operationId: outgoingPaymentWebhook tags: - Webhooks @@ -3196,7 +3196,10 @@ webhooks: outgoingCompletedPayment: summary: Completed outgoing payment value: - transaction: + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: OUTGOING_PAYMENT.COMPLETED + timestamp: '2025-08-15T14:32:00Z' + data: id: Transaction:019542f5-b3e7-1d02-0000-000000000005 status: COMPLETED type: OUTGOING @@ -3218,30 +3221,20 @@ webhooks: decimals: 2 customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 platformCustomerId: 18d3e5f7b4a9c2 - settlementTime: '2025-08-15T14:30:00Z' - createdAt: '2025-08-15T14:25:18Z' - description: 'Payment for invoice #1234' exchangeRate: 0.92 quoteId: Quote:019542f5-b3e7-1d02-0000-000000000006 - paymentInstructions: - - accountOrWalletInfo: - reference: UMA-Q12345-REF - accountType: US_ACCOUNT - accountNumber: 987654321 - routingNumber: 123456789 - accountCategory: CHECKING - bankName: Chase Bank - - accountOrWalletInfo: - accountType: SOLANA_WALLET - assetType: USDC - address: 4Nd1m6Qkq7RfKuE5vQ9qP9Tn6H94Ueqb4xXHzsAbd8Wg - timestamp: '2025-08-15T14:32:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000007 - type: OUTGOING_PAYMENT + settledAt: '2025-08-15T14:30:00Z' + createdAt: '2025-08-15T14:25:18Z' + description: 'Payment for invoice #1234' + paymentInstructions: [] + rateDetails: {} failedPayment: summary: Failed outgoing payment value: - transaction: + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: OUTGOING_PAYMENT.FAILED + timestamp: '2025-08-15T14:32:00Z' + data: id: Transaction:019542f5-b3e7-1d02-0000-000000000005 status: FAILED type: OUTGOING @@ -3258,21 +3251,7 @@ webhooks: platformCustomerId: 18d3e5f7b4a9c2 createdAt: '2025-08-15T14:25:18Z' quoteId: Quote:019542f5-b3e7-1d02-0000-000000000006 - paymentInstructions: - - accountOrWalletInfo: - reference: UMA-Q12345-REF - accountType: US_ACCOUNT - accountNumber: 987654321 - routingNumber: 123456789 - accountCategory: CHECKING - bankName: Chase Bank - - accountOrWalletInfo: - accountType: SOLANA_WALLET - assetType: USDC - address: 4Nd1m6Qkq7RfKuE5vQ9qP9Tn6H94Ueqb4xXHzsAbd8Wg - timestamp: '2025-08-15T14:32:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000007 - type: OUTGOING_PAYMENT + failureReason: INSUFFICIENT_FUNDS responses: '200': description: Webhook received successfully @@ -3289,7 +3268,7 @@ webhooks: schema: $ref: '#/components/schemas/Error401' '409': - description: Conflict - Webhook has already been processed (duplicate webhookId) + description: Conflict - Webhook has already been processed (duplicate id) content: application/json: schema: @@ -3327,9 +3306,10 @@ webhooks: testWebhook: summary: Test webhook example value: - timestamp: '2025-08-15T14:32:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000001 + id: Webhook:019542f5-b3e7-1d02-0000-000000000001 type: TEST + timestamp: '2025-08-15T14:32:00Z' + data: {} responses: '200': description: Webhook received successfully. This confirms your webhook endpoint is properly configured. @@ -3346,7 +3326,7 @@ webhooks: schema: $ref: '#/components/schemas/Error401' '409': - description: Conflict - Webhook has already been processed (duplicate webhookId) + description: Conflict - Webhook has already been processed (duplicate id) content: application/json: schema: @@ -3379,12 +3359,15 @@ webhooks: content: application/json: schema: - $ref: '#/components/schemas/BulkUploadWebhookRequest' + $ref: '#/components/schemas/BulkUploadWebhook' examples: completedUpload: summary: Successful bulk upload completion value: - bulkCustomerImportJob: + id: Webhook:019542f5-b3e7-1d02-0000-000000000008 + type: BULK_UPLOAD.COMPLETED + timestamp: '2025-08-15T14:32:00Z' + data: jobId: Job:019542f5-b3e7-1d02-0000-000000000006 status: COMPLETED progress: @@ -3393,13 +3376,13 @@ webhooks: successful: 5000 failed: 0 errors: [] - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000008 - type: BULK_UPLOAD - timestamp: '2025-08-15T14:32:00Z' failedUpload: summary: Failed bulk upload value: - bulkCustomerImportJob: + id: Webhook:019542f5-b3e7-1d02-0000-000000000008 + type: BULK_UPLOAD.FAILED + timestamp: '2025-08-15T14:32:00Z' + data: jobId: Job:019542f5-b3e7-1d02-0000-000000000006 status: FAILED progress: @@ -3415,9 +3398,6 @@ webhooks: details: reason: missing_required_column column: umaAddress - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000008 - type: BULK_UPLOAD - timestamp: '2025-08-15T14:32:00Z' responses: '200': description: Webhook received successfully @@ -3434,7 +3414,7 @@ webhooks: schema: $ref: '#/components/schemas/Error401' '409': - description: Conflict - Webhook has already been processed (duplicate webhookId) + description: Conflict - Webhook has already been processed (duplicate id) content: application/json: schema: @@ -3481,7 +3461,10 @@ webhooks: claimedInvitation: summary: Invitation claimed notification value: - invitation: + id: Webhook:019542f5-b3e7-1d02-0000-000000000008 + type: INVITATION.CLAIMED + timestamp: '2025-09-01T15:45:00Z' + data: code: 019542f5 createdAt: '2025-09-01T14:30:00Z' claimedAt: '2025-09-01T15:45:00Z' @@ -3489,9 +3472,6 @@ webhooks: inviteeUma: $invitee@uma.domain status: CLAIMED url: https://uma.me/i/019542f5 - timestamp: '2025-09-01T15:45:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000008 - type: INVITATION_CLAIMED responses: '200': description: Webhook received successfully @@ -3508,7 +3488,7 @@ webhooks: schema: $ref: '#/components/schemas/Error401' '409': - description: Conflict - Webhook has already been processed (duplicate webhookId) + description: Conflict - Webhook has already been processed (duplicate id) content: application/json: schema: @@ -3552,16 +3532,51 @@ webhooks: content: application/json: schema: - $ref: '#/components/schemas/KycStatusWebhook' + $ref: '#/components/schemas/CustomerKycWebhook' examples: kycApprovedWebhook: summary: When a customer KYC has been approved value: - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - kycStatus: APPROVED - type: KYC_STATUS + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: CUSTOMER.KYC_APPROVED + timestamp: '2025-08-15T14:32:00Z' + data: + id: Customer:019542f5-b3e7-1d02-0000-000000000001 + platformCustomerId: 9f84e0c2a72c4fa + customerType: INDIVIDUAL + umaAddress: $john.doe@uma.domain.com + kycStatus: APPROVED + fullName: John Michael Doe + birthDate: '1990-01-15' + nationality: US + address: + line1: 123 Main Street + line2: Apt 4B + city: San Francisco + state: CA + postalCode: '94105' + country: US + createdAt: '2025-07-21T17:32:28Z' + updatedAt: '2025-07-21T17:32:28Z' + isDeleted: false + kycRejectedWebhook: + summary: When a customer KYC has been rejected + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000008 + type: CUSTOMER.KYC_REJECTED timestamp: '2025-08-15T14:32:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000007 + data: + id: Customer:019542f5-b3e7-1d02-0000-000000000002 + platformCustomerId: 4b7c1e9d3f5a8e2 + customerType: INDIVIDUAL + umaAddress: $jane.smith@uma.domain.com + kycStatus: REJECTED + fullName: Jane Smith + birthDate: '1988-03-22' + nationality: US + createdAt: '2025-07-21T17:32:28Z' + updatedAt: '2025-08-15T14:32:00Z' + isDeleted: false responses: '200': description: | @@ -3579,16 +3594,16 @@ webhooks: schema: $ref: '#/components/schemas/Error401' '409': - description: Conflict - Webhook has already been processed (duplicate webhookId) + description: Conflict - Webhook has already been processed (duplicate id) content: application/json: schema: $ref: '#/components/schemas/Error409' account-status: post: - summary: Account status notification webhook + summary: Account status webhook description: | - Webhook that is called when the balance of an account changes + Webhook that is called when the status of an internal account changes. This includes balance updates and may include additional account events in the future. This endpoint should be implemented by clients of the Grid API. ### Authentication @@ -3601,8 +3616,8 @@ webhooks: If the signature verification succeeds, the webhook is authentic. If not, it should be rejected. - ### Account status - When the balance of an internal account changes, we will push a notification with information on the account, the new balance, and who the account belongs to. + ### Event types + - `ACCOUNT.BALANCE_UPDATED` — Fired when the balance of an internal account changes. The `data` payload contains the full internal account object along with the `oldBalance` prior to the change. operationId: accountStatusWebhook tags: - Webhooks @@ -3618,27 +3633,29 @@ webhooks: balanceDecrease: summary: A transaction just cleared a customer account and the balance has decreased value: - account: - accountId: Account:019542f5-b3e7-1d02-0000-000000000005 - oldBalance: - amount: 50000 + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: ACCOUNT.BALANCE_UPDATED + timestamp: '2025-08-15T14:32:00Z' + data: + id: InternalAccount:019542f5-b3e7-1d02-0000-000000000005 + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + balance: + amount: 10000 currency: code: USD name: United States Dollar symbol: $ decimals: 2 - newBalance: - amount: 10000 + oldBalance: + amount: 50000 currency: code: USD name: United States Dollar symbol: $ decimals: 2 - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - platformCustomerId: 019542f5-b3e7-1d02-0000-000000000001 - timestamp: '2025-08-15T14:32:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000007 - type: ACCOUNT_STATUS + fundingPaymentInstructions: [] + createdAt: '2025-08-01T10:00:00Z' + updatedAt: '2025-08-15T14:32:00Z' responses: '200': description: | @@ -3656,7 +3673,7 @@ webhooks: schema: $ref: '#/components/schemas/Error401' '409': - description: Conflict - Webhook has already been processed (duplicate webhookId) + description: Conflict - Webhook has already been processed (duplicate id) content: application/json: schema: @@ -3673,7 +3690,6 @@ components: name: X-Grid-Signature description: | Secp256r1 (P-256) asymmetric signature of the webhook payload, which can be used 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 @@ -3682,18 +3698,6 @@ components: If the signature verification succeeds, the webhook is authentic. If not, it should be rejected. schemas: - AllErrors: - anyOf: - - $ref: '#/components/schemas/Error400' - - $ref: '#/components/schemas/Error401' - - $ref: '#/components/schemas/Error403' - - $ref: '#/components/schemas/Error404' - - $ref: '#/components/schemas/Error409' - - $ref: '#/components/schemas/Error410' - - $ref: '#/components/schemas/Error412' - - $ref: '#/components/schemas/Error424' - - $ref: '#/components/schemas/Error500' - - $ref: '#/components/schemas/Error501' CustomerInfoFieldName: type: string enum: @@ -7101,6 +7105,24 @@ components: Optional Plaid account ID if the customer selected a specific account. If not provided, the default account will be used. example: plaid_account_id_123 + ExternalAccountReference: + type: object + required: + - accountId + properties: + accountId: + type: string + description: Reference to an external account ID + example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + InternalAccountReference: + type: object + required: + - accountId + properties: + accountId: + type: string + description: Reference to an internal account ID + example: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 TransferInRequest: type: object required: @@ -7108,24 +7130,10 @@ components: - destination properties: source: - type: object - required: - - accountId - properties: - accountId: - type: string - description: Reference to an external account ID - example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + $ref: '#/components/schemas/ExternalAccountReference' description: Source external account details destination: - type: object - required: - - accountId - properties: - accountId: - type: string - description: Reference to an internal account ID - example: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + $ref: '#/components/schemas/InternalAccountReference' description: Destination internal account details amount: type: integer @@ -7619,24 +7627,10 @@ components: - destination properties: source: - type: object - required: - - accountId - properties: - accountId: - type: string - description: Reference to an internal account ID - example: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + $ref: '#/components/schemas/InternalAccountReference' description: Source internal account details destination: - type: object - required: - - accountId - properties: - accountId: - type: string - description: Reference to an external account ID - example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + $ref: '#/components/schemas/ExternalAccountReference' description: Destination external account details amount: type: integer @@ -8492,127 +8486,76 @@ components: description: A list of permissions to grant to the token items: $ref: '#/components/schemas/Permission' - IncomingPaymentWebhook: - allOf: - - $ref: '#/components/schemas/BaseWebhook' - - type: object - required: - - transaction - properties: - transaction: - $ref: '#/components/schemas/IncomingTransaction' - type: - $ref: '#/components/schemas/WebhookType' - description: Type of webhook event - example: INCOMING_PAYMENT - requestedReceiverCustomerInfoFields: - type: array - items: - $ref: '#/components/schemas/CounterpartyFieldDefinition' - description: Information required by the sender's VASP about the recipient. Platform must provide these in the 200 OK response if approving. Note that this only includes fields which Grid does not already have from initial customer registration. + WebhookType: + type: string + enum: + - OUTGOING_PAYMENT.PENDING + - OUTGOING_PAYMENT.PROCESSING + - OUTGOING_PAYMENT.SENT + - OUTGOING_PAYMENT.COMPLETED + - OUTGOING_PAYMENT.FAILED + - OUTGOING_PAYMENT.REFUNDED + - OUTGOING_PAYMENT.EXPIRED + - INCOMING_PAYMENT.PENDING + - INCOMING_PAYMENT.COMPLETED + - INCOMING_PAYMENT.FAILED + - CUSTOMER.KYC_APPROVED + - CUSTOMER.KYC_REJECTED + - CUSTOMER.KYC_SUBMITTED + - CUSTOMER.KYC_MANUALLY_APPROVED + - CUSTOMER.KYC_MANUALLY_REJECTED + - ACCOUNT.BALANCE_UPDATED + - INVITATION.CLAIMED + - BULK_UPLOAD.COMPLETED + - BULK_UPLOAD.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: type: object required: - timestamp - id - type + - data properties: - timestamp: - type: string - format: date-time - description: ISO8601 timestamp when the webhook was sent (can be used to prevent replay attacks) - example: '2025-08-15T14:32:00Z' id: type: string description: Unique identifier for this webhook delivery (can be used for idempotency) example: Webhook:019542f5-b3e7-1d02-0000-000000000007 type: $ref: '#/components/schemas/WebhookType' - description: Type of webhook event - discriminator: - propertyName: type - mapping: - INCOMING_PAYMENT: '#/components/schemas/IncomingPaymentWebhook' - OUTGOING_PAYMENT: '#/components/schemas/OutgoingPaymentWebhook' - TEST: '#/components/schemas/TestWebhookRequest' - BULK_UPLOAD: '#/components/schemas/BulkUploadWebhookRequest' - INVITATION_CLAIMED: '#/components/schemas/InvitationClaimedWebhook' - KYC_STATUS: '#/components/schemas/KycStatusWebhook' - WebhookType: - type: string - enum: - - INCOMING_PAYMENT - - OUTGOING_PAYMENT - - TEST - - BULK_UPLOAD - - INVITATION_CLAIMED - - KYC_STATUS - - ACCOUNT_STATUS - description: Type of webhook event, used by the receiver to identify which webhook is being received - OutgoingPaymentWebhook: - allOf: - - $ref: '#/components/schemas/BaseWebhook' - - type: object - required: - - transaction - properties: - transaction: - $ref: '#/components/schemas/OutgoingTransaction' - type: - $ref: '#/components/schemas/WebhookType' - description: Type of webhook event - example: OUTGOING_PAYMENT - TestWebhookRequest: - allOf: - - $ref: '#/components/schemas/BaseWebhook' - - type: object - properties: - type: - $ref: '#/components/schemas/WebhookType' - description: Type of webhook event - example: TEST - BulkUploadWebhookRequest: - allOf: - - $ref: '#/components/schemas/BaseWebhook' - - type: object - required: - - bulkCustomerImportJob - properties: - bulkCustomerImportJob: - $ref: '#/components/schemas/BulkCustomerImportJob' - type: - $ref: '#/components/schemas/WebhookType' - description: Type of webhook event - example: BULK_UPLOAD - InvitationClaimedWebhook: + description: Status-specific event type in OBJECT.EVENT dot-notation (e.g., OUTGOING_PAYMENT.COMPLETED) + timestamp: + type: string + format: date-time + description: ISO 8601 timestamp of when the webhook was sent + example: '2025-08-15T14:32:00Z' + data: + type: object + description: The resource object. Contains the full resource as the corresponding GET endpoint would return it. + IncomingPaymentWebhook: allOf: - $ref: '#/components/schemas/BaseWebhook' - type: object required: - - invitation + - data properties: - invitation: - $ref: '#/components/schemas/UmaInvitation' + data: + allOf: + - $ref: '#/components/schemas/IncomingTransaction' + - type: object + properties: + requestedReceiverCustomerInfoFields: + type: array + items: + $ref: '#/components/schemas/CounterpartyFieldDefinition' + description: Information required by the sender's VASP about the recipient. Platform must provide these in the 200 OK response if approving. Note that this only includes fields which Grid does not already have from initial customer registration. type: type: string enum: - - INVITATION_CLAIMED - description: Type of webhook event - example: INVITATION_CLAIMED - KycStatusWebhook: - allOf: - - $ref: '#/components/schemas/BaseWebhook' - - type: object - required: - - customerId - - kycStatus - properties: - customerId: - type: string - description: System generated id of the customer - example: Customer:019542f5-b3e7-1d02-0000-000000000001 - kycStatus: - $ref: '#/components/schemas/KycStatus' + - INCOMING_PAYMENT.PENDING + - INCOMING_PAYMENT.COMPLETED + - INCOMING_PAYMENT.FAILED IncomingPaymentWebhookResponse: type: object properties: @@ -8642,25 +8585,94 @@ components: example: - TAX_ID - REGISTRATION_NUMBER - AccountStatusWebhook: + OutgoingPaymentWebhook: allOf: - $ref: '#/components/schemas/BaseWebhook' - type: object required: - - accountId + - data properties: - accountId: + data: + $ref: '#/components/schemas/OutgoingTransaction' + type: type: string - description: The id of the account whose balance has changed - oldBalance: - $ref: '#/components/schemas/CurrencyAmount' - newBalance: - $ref: '#/components/schemas/CurrencyAmount' - customerId: + enum: + - OUTGOING_PAYMENT.PENDING + - OUTGOING_PAYMENT.PROCESSING + - OUTGOING_PAYMENT.SENT + - OUTGOING_PAYMENT.COMPLETED + - OUTGOING_PAYMENT.FAILED + - OUTGOING_PAYMENT.REFUNDED + - OUTGOING_PAYMENT.EXPIRED + TestWebhookRequest: + allOf: + - $ref: '#/components/schemas/BaseWebhook' + - type: object + properties: + type: type: string - description: The ID of the customer associated with the internal account - example: Customer:019542f5-b3e7-1d02-0000-000000000001 - platformCustomerId: + enum: + - TEST + BulkUploadWebhook: + allOf: + - $ref: '#/components/schemas/BaseWebhook' + - type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/BulkCustomerImportJob' + type: type: string - description: The ID of the customer as associated in your platform - example: 019542f5-b3e7-1d02-0000-000000000001 + enum: + - BULK_UPLOAD.COMPLETED + - BULK_UPLOAD.FAILED + InvitationClaimedWebhook: + allOf: + - $ref: '#/components/schemas/BaseWebhook' + - type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/UmaInvitation' + type: + type: string + enum: + - INVITATION.CLAIMED + CustomerKycWebhook: + allOf: + - $ref: '#/components/schemas/BaseWebhook' + - type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/IndividualCustomer' + type: + type: string + enum: + - CUSTOMER.KYC_APPROVED + - CUSTOMER.KYC_REJECTED + - CUSTOMER.KYC_SUBMITTED + - CUSTOMER.KYC_MANUALLY_APPROVED + - CUSTOMER.KYC_MANUALLY_REJECTED + AccountStatusWebhook: + allOf: + - $ref: '#/components/schemas/BaseWebhook' + - type: object + required: + - data + properties: + data: + allOf: + - $ref: '#/components/schemas/InternalAccount' + - type: object + properties: + oldBalance: + $ref: '#/components/schemas/CurrencyAmount' + description: The account balance before the change + type: + type: string + enum: + - ACCOUNT.BALANCE_UPDATED diff --git a/mintlify/snippets/kyc/kyc-webhooks.mdx b/mintlify/snippets/kyc/kyc-webhooks.mdx index e380222c..678edea8 100644 --- a/mintlify/snippets/kyc/kyc-webhooks.mdx +++ b/mintlify/snippets/kyc/kyc-webhooks.mdx @@ -8,31 +8,52 @@ For regulated platforms, customers are created with `APPROVED` KYC status by def ```json { - "webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000020", - "type": "KYC_STATUS", - "timestamp": "2023-07-21T17:32:28Z", - "customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001", - "kycStatus": "APPROVED", - "platformCustomerId": "1234567" + "id": "Webhook:019542f5-b3e7-1d02-0000-000000000020", + "type": "CUSTOMER.KYC_APPROVED", + "timestamp": "2025-07-21T17:32:28Z", + "data": { + "id": "Customer:019542f5-b3e7-1d02-0000-000000000001", + "platformCustomerId": "9f84e0c2a72c4fa", + "customerType": "INDIVIDUAL", + "umaAddress": "$john.doe@uma.domain.com", + "kycStatus": "APPROVED", + "fullName": "John Michael Doe", + "birthDate": "1990-01-15", + "nationality": "US", + "address": { + "line1": "123 Main Street", + "line2": "Apt 4B", + "city": "San Francisco", + "state": "CA", + "postalCode": "94105", + "country": "US" + }, + "createdAt": "2025-07-21T17:32:28Z", + "updatedAt": "2025-07-21T17:32:28Z", + "isDeleted": false + } } ``` **Webhook Headers:** - `Content-Type: application/json` -- `X-Webhook-Signature: sha256=abc123...` +- `X-Grid-Signature: {"v": "1", "s": "base64_signature..."}` + + +Unique identifier for this webhook delivery. Use this for idempotency to prevent processing duplicate webhooks. + - -System-generated unique identifier of the customer whose KYC status has changed. + +Status-specific event type. KYC webhooks use `CUSTOMER.*` types: +- `CUSTOMER.KYC_APPROVED`: Customer verification completed successfully +- `CUSTOMER.KYC_REJECTED`: Customer verification was rejected +- `CUSTOMER.KYC_SUBMITTED`: KYC verification was initially submitted +- `CUSTOMER.KYC_MANUALLY_APPROVED`: Customer was manually approved by platform +- `CUSTOMER.KYC_MANUALLY_REJECTED`: Customer was manually rejected by platform - -Final KYC verification status. Webhooks are only sent for final states: -- `APPROVED`: Customer verification completed successfully -- `REJECTED`: Customer verification was rejected -- `EXPIRED`: KYC verification has expired and needs renewal -- `CANCELED`: Verification process was canceled -- `MANUALLY_APPROVED`: Customer was manually approved by platform -- `MANUALLY_REJECTED`: Customer was manually rejected by platform + +The full customer resource object, same as the corresponding `GET /customers/{id}` endpoint would return. Includes all customer fields such as `id`, `kycStatus`, `fullName`, `birthDate`, `nationality`, `address`, etc. @@ -43,50 +64,40 @@ Intermediate states like `PENDING_REVIEW` do not trigger webhook notifications. ```javascript // Example webhook handler for KYC status updates // Note: Only final states trigger webhook notifications -app.post('/webhooks/kyc-status', (req, res) => { - const { customerId, kycStatus } = req.body; - - switch (kycStatus) { - case 'APPROVED': +app.post('/webhooks/kyc-status', async (req, res) => { + const { type, data } = req.body; + + switch (type) { + case 'CUSTOMER.KYC_APPROVED': // Activate customer account - await activateCustomer(customerId); - await sendWelcomeEmail(customerId); + await activateCustomer(data.id); + await sendWelcomeEmail(data.id); break; - - case 'REJECTED': + + case 'CUSTOMER.KYC_REJECTED': // Notify support and customer - await notifySupport(customerId, 'KYC_REJECTED'); - await sendRejectionEmail(customerId); + await notifySupport(data.id, 'KYC_REJECTED'); + await sendRejectionEmail(data.id); break; - - case 'MANUALLY_APPROVED': + + case 'CUSTOMER.KYC_MANUALLY_APPROVED': // Handle manual approval - await activateCustomer(customerId); - await sendWelcomeEmail(customerId); + await activateCustomer(data.id); + await sendWelcomeEmail(data.id); break; - - case 'MANUALLY_REJECTED': + + case 'CUSTOMER.KYC_MANUALLY_REJECTED': // Handle manual rejection - await notifySupport(customerId, 'KYC_MANUALLY_REJECTED'); - await sendRejectionEmail(customerId); - break; - - case 'EXPIRED': - // Handle expired KYC - await notifyCustomerForReKyc(customerId); - break; - - case 'CANCELED': - // Handle canceled verification - await logKycCancelation(customerId); + await notifySupport(data.id, 'KYC_MANUALLY_REJECTED'); + await sendRejectionEmail(data.id); break; - + default: - // Log unexpected statuses - console.log(`Unexpected KYC status ${kycStatus} for customer ${customerId}`); + // Log unexpected types + console.log(`Unexpected webhook type ${type} for customer ${data.id}`); } - + res.status(200).send('OK'); }); ``` - \ No newline at end of file + diff --git a/mintlify/snippets/webhooks.mdx b/mintlify/snippets/webhooks.mdx index b44a1741..b087ddbf 100644 --- a/mintlify/snippets/webhooks.mdx +++ b/mintlify/snippets/webhooks.mdx @@ -29,11 +29,11 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE... app.post('/webhooks/uma', (req, res) => { const signatureHeader = req.header('X-Grid-Signature'); - + if (!signatureHeader) { return res.status(401).json({ error: 'Signature missing' }); } - + try { let signature: Buffer; try { @@ -49,7 +49,7 @@ app.post('/webhooks/uma', (req, res) => { // If JSON parsing fails, treat as direct base64 signature = Buffer.from(signatureHeader, "base64"); } - + // Create verifier with the public key and correct algorithm const verifier = crypto.createVerify("SHA256"); const payload = await request.text(); @@ -65,22 +65,22 @@ app.post('/webhooks/uma', (req, res) => { }, signature, ); - + if (!isValid) { return res.status(401).json({ error: 'Invalid signature' }); } - + // Webhook is verified, process it based on type const webhookData = req.body; - - if (webhookData.type === 'INCOMING_PAYMENT') { + + if (webhookData.type.startsWith('INCOMING_PAYMENT.')) { // Process incoming payment webhook // ... - } else if (webhookData.type === 'OUTGOING_PAYMENT') { + } else if (webhookData.type.startsWith('OUTGOING_PAYMENT.')) { // Process outgoing payment webhook // ... } - + // Acknowledge receipt of the webhook return res.status(200).json({ received: true }); } catch (error) { @@ -121,19 +121,19 @@ def handle_webhook(): signature = request.headers.get('X-Grid-Signature') if not signature: return jsonify({'error': 'Signature missing'}), 401 - + try: # Get the raw request body request_body = request.get_data() - + # Create a SHA-256 hash of the request body hash_obj = hashes.Hash(hashes.SHA256()) hash_obj.update(request_body) digest = hash_obj.finalize() - + # Decode the base64 signature signature_bytes = base64.b64decode(signature) - + # Verify the signature try: public_key.verify( @@ -143,19 +143,19 @@ def handle_webhook(): ) except Exception as e: return jsonify({'error': 'Invalid signature'}), 401 - + # Webhook is verified, process it based on type webhook_data = request.json - - if webhook_data['type'] == 'INCOMING_PAYMENT': + + if webhook_data['type'].startswith('INCOMING_PAYMENT.'): # Process incoming payment webhook # ... pass - elif webhook_data['type'] == 'OUTGOING_PAYMENT': + elif webhook_data['type'].startswith('OUTGOING_PAYMENT.'): # Process outgoing payment webhook # ... pass - + # Acknowledge receipt of the webhook return jsonify({'received': True}), 200 except Exception as e: @@ -174,10 +174,10 @@ An example of the test webhook payload is shown below: ```json { - "test": true, - "timestamp": "2023-08-15T14:32:00Z", - "webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000007", - "type": "TEST" + "id": "Webhook:019542f5-b3e7-1d02-0000-000000000007", + "type": "TEST", + "timestamp": "2025-08-15T14:32:00Z", + "data": {} } ``` @@ -187,7 +187,7 @@ You should verify the signature of the webhook using the Grid public key and the - **Always verify signatures**: Never process webhooks without verifying their signatures. - **Use HTTPS**: Ensure your webhook endpoint uses HTTPS to prevent man-in-the-middle attacks. -- **Implement idempotency**: Use the `webhookId` field to prevent processing duplicate webhooks. +- **Implement idempotency**: Use the `id` field to prevent processing duplicate webhooks. - **Timeout handling**: Implement proper timeout handling and respond to webhooks promptly. ## Retry Policy @@ -196,10 +196,10 @@ The Grid API will retry webhooks with the following policy based on the webhook | Webhook Type | Retry Policy | Notes | |-------------|-------------|-------| -| TEST | No retries | Used for testing webhook configuration | -| OUTGOING_PAYMENT | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) | -| INCOMING_PAYMENT | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on: 409 (duplicate webhook) or PENDING status since it is served as an approval mechanism in-flow | -| BULK_UPLOAD | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) | -| INVITATION_CLAIMED | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) | -| KYC_STATUS | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) | -| ACCOUNT_STATUS | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) | \ No newline at end of file +| `TEST` | No retries | Used for testing webhook configuration | +| `OUTGOING_PAYMENT.*` | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) | +| `INCOMING_PAYMENT.*` | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on: 409 (duplicate webhook) or PENDING status since it is served as an approval mechanism in-flow | +| `BULK_UPLOAD.*` | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) | +| `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) | diff --git a/openapi.yaml b/openapi.yaml index e149e131..ab984365 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2148,13 +2148,13 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /webhooks/test: + /sandbox/webhooks/test: post: summary: Send a test webhook description: Send a test webhook to the configured endpoint operationId: sendTestWebhook tags: - - Webhooks + - Sandbox security: - BasicAuth: [] responses: @@ -3059,7 +3059,10 @@ webhooks: pendingPayment: summary: Pending payment example requiring approval value: - transaction: + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: INCOMING_PAYMENT.PENDING + timestamp: '2025-08-15T14:32:00Z' + data: id: Transaction:019542f5-b3e7-1d02-0000-000000000005 status: PENDING type: INCOMING @@ -3074,24 +3077,24 @@ webhooks: decimals: 2 customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 platformCustomerId: 18d3e5f7b4a9c2 - reconciliationInstructions: - reference: REF-123456789 counterpartyInformation: FULL_NAME: John Sender BIRTH_DATE: '1985-06-15' NATIONALITY: US - requestedReceiverCustomerInfoFields: - - name: NATIONALITY - mandatory: true - - name: ADDRESS - mandatory: false - timestamp: '2025-08-15T14:32:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000007 - type: INCOMING_PAYMENT + reconciliationInstructions: + reference: REF-123456789 + requestedReceiverCustomerInfoFields: + - name: NATIONALITY + mandatory: true + - name: ADDRESS + mandatory: false incomingCompletedPayment: summary: Completed payment notification value: - transaction: + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: INCOMING_PAYMENT.COMPLETED + timestamp: '2025-08-15T14:32:00Z' + data: id: Transaction:019542f5-b3e7-1d02-0000-000000000005 status: COMPLETED type: INCOMING @@ -3111,9 +3114,6 @@ webhooks: description: Payment for services reconciliationInstructions: reference: REF-123456789 - timestamp: '2025-08-15T14:32:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000007 - type: INCOMING_PAYMENT responses: '200': description: | @@ -3150,7 +3150,7 @@ webhooks: schema: $ref: '#/components/schemas/IncomingPaymentWebhookForbiddenResponse' '409': - description: Conflict - Webhook has already been processed (duplicate webhookId) + description: Conflict - Webhook has already been processed (duplicate id) content: application/json: schema: @@ -3180,7 +3180,7 @@ webhooks: If the signature verification succeeds, the webhook is authentic. If not, it should be rejected. - This webhook is informational only and is sent when an outgoing payment completes successfully or fails. + This webhook is informational only and is sent when an outgoing payment completes successfully, fails, or is refunded. operationId: outgoingPaymentWebhook tags: - Webhooks @@ -3196,7 +3196,10 @@ webhooks: outgoingCompletedPayment: summary: Completed outgoing payment value: - transaction: + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: OUTGOING_PAYMENT.COMPLETED + timestamp: '2025-08-15T14:32:00Z' + data: id: Transaction:019542f5-b3e7-1d02-0000-000000000005 status: COMPLETED type: OUTGOING @@ -3218,30 +3221,20 @@ webhooks: decimals: 2 customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 platformCustomerId: 18d3e5f7b4a9c2 - settlementTime: '2025-08-15T14:30:00Z' - createdAt: '2025-08-15T14:25:18Z' - description: 'Payment for invoice #1234' exchangeRate: 0.92 quoteId: Quote:019542f5-b3e7-1d02-0000-000000000006 - paymentInstructions: - - accountOrWalletInfo: - reference: UMA-Q12345-REF - accountType: US_ACCOUNT - accountNumber: 987654321 - routingNumber: 123456789 - accountCategory: CHECKING - bankName: Chase Bank - - accountOrWalletInfo: - accountType: SOLANA_WALLET - assetType: USDC - address: 4Nd1m6Qkq7RfKuE5vQ9qP9Tn6H94Ueqb4xXHzsAbd8Wg - timestamp: '2025-08-15T14:32:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000007 - type: OUTGOING_PAYMENT + settledAt: '2025-08-15T14:30:00Z' + createdAt: '2025-08-15T14:25:18Z' + description: 'Payment for invoice #1234' + paymentInstructions: [] + rateDetails: {} failedPayment: summary: Failed outgoing payment value: - transaction: + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: OUTGOING_PAYMENT.FAILED + timestamp: '2025-08-15T14:32:00Z' + data: id: Transaction:019542f5-b3e7-1d02-0000-000000000005 status: FAILED type: OUTGOING @@ -3258,21 +3251,7 @@ webhooks: platformCustomerId: 18d3e5f7b4a9c2 createdAt: '2025-08-15T14:25:18Z' quoteId: Quote:019542f5-b3e7-1d02-0000-000000000006 - paymentInstructions: - - accountOrWalletInfo: - reference: UMA-Q12345-REF - accountType: US_ACCOUNT - accountNumber: 987654321 - routingNumber: 123456789 - accountCategory: CHECKING - bankName: Chase Bank - - accountOrWalletInfo: - accountType: SOLANA_WALLET - assetType: USDC - address: 4Nd1m6Qkq7RfKuE5vQ9qP9Tn6H94Ueqb4xXHzsAbd8Wg - timestamp: '2025-08-15T14:32:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000007 - type: OUTGOING_PAYMENT + failureReason: INSUFFICIENT_FUNDS responses: '200': description: Webhook received successfully @@ -3289,7 +3268,7 @@ webhooks: schema: $ref: '#/components/schemas/Error401' '409': - description: Conflict - Webhook has already been processed (duplicate webhookId) + description: Conflict - Webhook has already been processed (duplicate id) content: application/json: schema: @@ -3327,9 +3306,10 @@ webhooks: testWebhook: summary: Test webhook example value: - timestamp: '2025-08-15T14:32:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000001 + id: Webhook:019542f5-b3e7-1d02-0000-000000000001 type: TEST + timestamp: '2025-08-15T14:32:00Z' + data: {} responses: '200': description: Webhook received successfully. This confirms your webhook endpoint is properly configured. @@ -3346,7 +3326,7 @@ webhooks: schema: $ref: '#/components/schemas/Error401' '409': - description: Conflict - Webhook has already been processed (duplicate webhookId) + description: Conflict - Webhook has already been processed (duplicate id) content: application/json: schema: @@ -3379,12 +3359,15 @@ webhooks: content: application/json: schema: - $ref: '#/components/schemas/BulkUploadWebhookRequest' + $ref: '#/components/schemas/BulkUploadWebhook' examples: completedUpload: summary: Successful bulk upload completion value: - bulkCustomerImportJob: + id: Webhook:019542f5-b3e7-1d02-0000-000000000008 + type: BULK_UPLOAD.COMPLETED + timestamp: '2025-08-15T14:32:00Z' + data: jobId: Job:019542f5-b3e7-1d02-0000-000000000006 status: COMPLETED progress: @@ -3393,13 +3376,13 @@ webhooks: successful: 5000 failed: 0 errors: [] - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000008 - type: BULK_UPLOAD - timestamp: '2025-08-15T14:32:00Z' failedUpload: summary: Failed bulk upload value: - bulkCustomerImportJob: + id: Webhook:019542f5-b3e7-1d02-0000-000000000008 + type: BULK_UPLOAD.FAILED + timestamp: '2025-08-15T14:32:00Z' + data: jobId: Job:019542f5-b3e7-1d02-0000-000000000006 status: FAILED progress: @@ -3415,9 +3398,6 @@ webhooks: details: reason: missing_required_column column: umaAddress - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000008 - type: BULK_UPLOAD - timestamp: '2025-08-15T14:32:00Z' responses: '200': description: Webhook received successfully @@ -3434,7 +3414,7 @@ webhooks: schema: $ref: '#/components/schemas/Error401' '409': - description: Conflict - Webhook has already been processed (duplicate webhookId) + description: Conflict - Webhook has already been processed (duplicate id) content: application/json: schema: @@ -3481,7 +3461,10 @@ webhooks: claimedInvitation: summary: Invitation claimed notification value: - invitation: + id: Webhook:019542f5-b3e7-1d02-0000-000000000008 + type: INVITATION.CLAIMED + timestamp: '2025-09-01T15:45:00Z' + data: code: 019542f5 createdAt: '2025-09-01T14:30:00Z' claimedAt: '2025-09-01T15:45:00Z' @@ -3489,9 +3472,6 @@ webhooks: inviteeUma: $invitee@uma.domain status: CLAIMED url: https://uma.me/i/019542f5 - timestamp: '2025-09-01T15:45:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000008 - type: INVITATION_CLAIMED responses: '200': description: Webhook received successfully @@ -3508,7 +3488,7 @@ webhooks: schema: $ref: '#/components/schemas/Error401' '409': - description: Conflict - Webhook has already been processed (duplicate webhookId) + description: Conflict - Webhook has already been processed (duplicate id) content: application/json: schema: @@ -3552,16 +3532,51 @@ webhooks: content: application/json: schema: - $ref: '#/components/schemas/KycStatusWebhook' + $ref: '#/components/schemas/CustomerKycWebhook' examples: kycApprovedWebhook: summary: When a customer KYC has been approved value: - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - kycStatus: APPROVED - type: KYC_STATUS + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: CUSTOMER.KYC_APPROVED + timestamp: '2025-08-15T14:32:00Z' + data: + id: Customer:019542f5-b3e7-1d02-0000-000000000001 + platformCustomerId: 9f84e0c2a72c4fa + customerType: INDIVIDUAL + umaAddress: $john.doe@uma.domain.com + kycStatus: APPROVED + fullName: John Michael Doe + birthDate: '1990-01-15' + nationality: US + address: + line1: 123 Main Street + line2: Apt 4B + city: San Francisco + state: CA + postalCode: '94105' + country: US + createdAt: '2025-07-21T17:32:28Z' + updatedAt: '2025-07-21T17:32:28Z' + isDeleted: false + kycRejectedWebhook: + summary: When a customer KYC has been rejected + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000008 + type: CUSTOMER.KYC_REJECTED timestamp: '2025-08-15T14:32:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000007 + data: + id: Customer:019542f5-b3e7-1d02-0000-000000000002 + platformCustomerId: 4b7c1e9d3f5a8e2 + customerType: INDIVIDUAL + umaAddress: $jane.smith@uma.domain.com + kycStatus: REJECTED + fullName: Jane Smith + birthDate: '1988-03-22' + nationality: US + createdAt: '2025-07-21T17:32:28Z' + updatedAt: '2025-08-15T14:32:00Z' + isDeleted: false responses: '200': description: | @@ -3579,16 +3594,16 @@ webhooks: schema: $ref: '#/components/schemas/Error401' '409': - description: Conflict - Webhook has already been processed (duplicate webhookId) + description: Conflict - Webhook has already been processed (duplicate id) content: application/json: schema: $ref: '#/components/schemas/Error409' account-status: post: - summary: Account status notification webhook + summary: Account status webhook description: | - Webhook that is called when the balance of an account changes + Webhook that is called when the status of an internal account changes. This includes balance updates and may include additional account events in the future. This endpoint should be implemented by clients of the Grid API. ### Authentication @@ -3601,8 +3616,8 @@ webhooks: If the signature verification succeeds, the webhook is authentic. If not, it should be rejected. - ### Account status - When the balance of an internal account changes, we will push a notification with information on the account, the new balance, and who the account belongs to. + ### Event types + - `ACCOUNT.BALANCE_UPDATED` — Fired when the balance of an internal account changes. The `data` payload contains the full internal account object along with the `oldBalance` prior to the change. operationId: accountStatusWebhook tags: - Webhooks @@ -3618,27 +3633,29 @@ webhooks: balanceDecrease: summary: A transaction just cleared a customer account and the balance has decreased value: - account: - accountId: Account:019542f5-b3e7-1d02-0000-000000000005 - oldBalance: - amount: 50000 + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: ACCOUNT.BALANCE_UPDATED + timestamp: '2025-08-15T14:32:00Z' + data: + id: InternalAccount:019542f5-b3e7-1d02-0000-000000000005 + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + balance: + amount: 10000 currency: code: USD name: United States Dollar symbol: $ decimals: 2 - newBalance: - amount: 10000 + oldBalance: + amount: 50000 currency: code: USD name: United States Dollar symbol: $ decimals: 2 - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - platformCustomerId: 019542f5-b3e7-1d02-0000-000000000001 - timestamp: '2025-08-15T14:32:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000007 - type: ACCOUNT_STATUS + fundingPaymentInstructions: [] + createdAt: '2025-08-01T10:00:00Z' + updatedAt: '2025-08-15T14:32:00Z' responses: '200': description: | @@ -3656,7 +3673,7 @@ webhooks: schema: $ref: '#/components/schemas/Error401' '409': - description: Conflict - Webhook has already been processed (duplicate webhookId) + description: Conflict - Webhook has already been processed (duplicate id) content: application/json: schema: @@ -3673,7 +3690,6 @@ components: name: X-Grid-Signature description: | Secp256r1 (P-256) asymmetric signature of the webhook payload, which can be used 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 @@ -3682,18 +3698,6 @@ components: If the signature verification succeeds, the webhook is authentic. If not, it should be rejected. schemas: - AllErrors: - anyOf: - - $ref: '#/components/schemas/Error400' - - $ref: '#/components/schemas/Error401' - - $ref: '#/components/schemas/Error403' - - $ref: '#/components/schemas/Error404' - - $ref: '#/components/schemas/Error409' - - $ref: '#/components/schemas/Error410' - - $ref: '#/components/schemas/Error412' - - $ref: '#/components/schemas/Error424' - - $ref: '#/components/schemas/Error500' - - $ref: '#/components/schemas/Error501' CustomerInfoFieldName: type: string enum: @@ -7101,6 +7105,24 @@ components: Optional Plaid account ID if the customer selected a specific account. If not provided, the default account will be used. example: plaid_account_id_123 + ExternalAccountReference: + type: object + required: + - accountId + properties: + accountId: + type: string + description: Reference to an external account ID + example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + InternalAccountReference: + type: object + required: + - accountId + properties: + accountId: + type: string + description: Reference to an internal account ID + example: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 TransferInRequest: type: object required: @@ -7108,24 +7130,10 @@ components: - destination properties: source: - type: object - required: - - accountId - properties: - accountId: - type: string - description: Reference to an external account ID - example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + $ref: '#/components/schemas/ExternalAccountReference' description: Source external account details destination: - type: object - required: - - accountId - properties: - accountId: - type: string - description: Reference to an internal account ID - example: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + $ref: '#/components/schemas/InternalAccountReference' description: Destination internal account details amount: type: integer @@ -7619,24 +7627,10 @@ components: - destination properties: source: - type: object - required: - - accountId - properties: - accountId: - type: string - description: Reference to an internal account ID - example: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + $ref: '#/components/schemas/InternalAccountReference' description: Source internal account details destination: - type: object - required: - - accountId - properties: - accountId: - type: string - description: Reference to an external account ID - example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + $ref: '#/components/schemas/ExternalAccountReference' description: Destination external account details amount: type: integer @@ -8492,127 +8486,76 @@ components: description: A list of permissions to grant to the token items: $ref: '#/components/schemas/Permission' - IncomingPaymentWebhook: - allOf: - - $ref: '#/components/schemas/BaseWebhook' - - type: object - required: - - transaction - properties: - transaction: - $ref: '#/components/schemas/IncomingTransaction' - type: - $ref: '#/components/schemas/WebhookType' - description: Type of webhook event - example: INCOMING_PAYMENT - requestedReceiverCustomerInfoFields: - type: array - items: - $ref: '#/components/schemas/CounterpartyFieldDefinition' - description: Information required by the sender's VASP about the recipient. Platform must provide these in the 200 OK response if approving. Note that this only includes fields which Grid does not already have from initial customer registration. + WebhookType: + type: string + enum: + - OUTGOING_PAYMENT.PENDING + - OUTGOING_PAYMENT.PROCESSING + - OUTGOING_PAYMENT.SENT + - OUTGOING_PAYMENT.COMPLETED + - OUTGOING_PAYMENT.FAILED + - OUTGOING_PAYMENT.REFUNDED + - OUTGOING_PAYMENT.EXPIRED + - INCOMING_PAYMENT.PENDING + - INCOMING_PAYMENT.COMPLETED + - INCOMING_PAYMENT.FAILED + - CUSTOMER.KYC_APPROVED + - CUSTOMER.KYC_REJECTED + - CUSTOMER.KYC_SUBMITTED + - CUSTOMER.KYC_MANUALLY_APPROVED + - CUSTOMER.KYC_MANUALLY_REJECTED + - ACCOUNT.BALANCE_UPDATED + - INVITATION.CLAIMED + - BULK_UPLOAD.COMPLETED + - BULK_UPLOAD.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: type: object required: - timestamp - id - type + - data properties: - timestamp: - type: string - format: date-time - description: ISO8601 timestamp when the webhook was sent (can be used to prevent replay attacks) - example: '2025-08-15T14:32:00Z' id: type: string description: Unique identifier for this webhook delivery (can be used for idempotency) example: Webhook:019542f5-b3e7-1d02-0000-000000000007 type: $ref: '#/components/schemas/WebhookType' - description: Type of webhook event - discriminator: - propertyName: type - mapping: - INCOMING_PAYMENT: '#/components/schemas/IncomingPaymentWebhook' - OUTGOING_PAYMENT: '#/components/schemas/OutgoingPaymentWebhook' - TEST: '#/components/schemas/TestWebhookRequest' - BULK_UPLOAD: '#/components/schemas/BulkUploadWebhookRequest' - INVITATION_CLAIMED: '#/components/schemas/InvitationClaimedWebhook' - KYC_STATUS: '#/components/schemas/KycStatusWebhook' - WebhookType: - type: string - enum: - - INCOMING_PAYMENT - - OUTGOING_PAYMENT - - TEST - - BULK_UPLOAD - - INVITATION_CLAIMED - - KYC_STATUS - - ACCOUNT_STATUS - description: Type of webhook event, used by the receiver to identify which webhook is being received - OutgoingPaymentWebhook: - allOf: - - $ref: '#/components/schemas/BaseWebhook' - - type: object - required: - - transaction - properties: - transaction: - $ref: '#/components/schemas/OutgoingTransaction' - type: - $ref: '#/components/schemas/WebhookType' - description: Type of webhook event - example: OUTGOING_PAYMENT - TestWebhookRequest: - allOf: - - $ref: '#/components/schemas/BaseWebhook' - - type: object - properties: - type: - $ref: '#/components/schemas/WebhookType' - description: Type of webhook event - example: TEST - BulkUploadWebhookRequest: - allOf: - - $ref: '#/components/schemas/BaseWebhook' - - type: object - required: - - bulkCustomerImportJob - properties: - bulkCustomerImportJob: - $ref: '#/components/schemas/BulkCustomerImportJob' - type: - $ref: '#/components/schemas/WebhookType' - description: Type of webhook event - example: BULK_UPLOAD - InvitationClaimedWebhook: + description: Status-specific event type in OBJECT.EVENT dot-notation (e.g., OUTGOING_PAYMENT.COMPLETED) + timestamp: + type: string + format: date-time + description: ISO 8601 timestamp of when the webhook was sent + example: '2025-08-15T14:32:00Z' + data: + type: object + description: The resource object. Contains the full resource as the corresponding GET endpoint would return it. + IncomingPaymentWebhook: allOf: - $ref: '#/components/schemas/BaseWebhook' - type: object required: - - invitation + - data properties: - invitation: - $ref: '#/components/schemas/UmaInvitation' + data: + allOf: + - $ref: '#/components/schemas/IncomingTransaction' + - type: object + properties: + requestedReceiverCustomerInfoFields: + type: array + items: + $ref: '#/components/schemas/CounterpartyFieldDefinition' + description: Information required by the sender's VASP about the recipient. Platform must provide these in the 200 OK response if approving. Note that this only includes fields which Grid does not already have from initial customer registration. type: type: string enum: - - INVITATION_CLAIMED - description: Type of webhook event - example: INVITATION_CLAIMED - KycStatusWebhook: - allOf: - - $ref: '#/components/schemas/BaseWebhook' - - type: object - required: - - customerId - - kycStatus - properties: - customerId: - type: string - description: System generated id of the customer - example: Customer:019542f5-b3e7-1d02-0000-000000000001 - kycStatus: - $ref: '#/components/schemas/KycStatus' + - INCOMING_PAYMENT.PENDING + - INCOMING_PAYMENT.COMPLETED + - INCOMING_PAYMENT.FAILED IncomingPaymentWebhookResponse: type: object properties: @@ -8642,25 +8585,94 @@ components: example: - TAX_ID - REGISTRATION_NUMBER - AccountStatusWebhook: + OutgoingPaymentWebhook: allOf: - $ref: '#/components/schemas/BaseWebhook' - type: object required: - - accountId + - data properties: - accountId: + data: + $ref: '#/components/schemas/OutgoingTransaction' + type: type: string - description: The id of the account whose balance has changed - oldBalance: - $ref: '#/components/schemas/CurrencyAmount' - newBalance: - $ref: '#/components/schemas/CurrencyAmount' - customerId: + enum: + - OUTGOING_PAYMENT.PENDING + - OUTGOING_PAYMENT.PROCESSING + - OUTGOING_PAYMENT.SENT + - OUTGOING_PAYMENT.COMPLETED + - OUTGOING_PAYMENT.FAILED + - OUTGOING_PAYMENT.REFUNDED + - OUTGOING_PAYMENT.EXPIRED + TestWebhookRequest: + allOf: + - $ref: '#/components/schemas/BaseWebhook' + - type: object + properties: + type: type: string - description: The ID of the customer associated with the internal account - example: Customer:019542f5-b3e7-1d02-0000-000000000001 - platformCustomerId: + enum: + - TEST + BulkUploadWebhook: + allOf: + - $ref: '#/components/schemas/BaseWebhook' + - type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/BulkCustomerImportJob' + type: type: string - description: The ID of the customer as associated in your platform - example: 019542f5-b3e7-1d02-0000-000000000001 + enum: + - BULK_UPLOAD.COMPLETED + - BULK_UPLOAD.FAILED + InvitationClaimedWebhook: + allOf: + - $ref: '#/components/schemas/BaseWebhook' + - type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/UmaInvitation' + type: + type: string + enum: + - INVITATION.CLAIMED + CustomerKycWebhook: + allOf: + - $ref: '#/components/schemas/BaseWebhook' + - type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/IndividualCustomer' + type: + type: string + enum: + - CUSTOMER.KYC_APPROVED + - CUSTOMER.KYC_REJECTED + - CUSTOMER.KYC_SUBMITTED + - CUSTOMER.KYC_MANUALLY_APPROVED + - CUSTOMER.KYC_MANUALLY_REJECTED + AccountStatusWebhook: + allOf: + - $ref: '#/components/schemas/BaseWebhook' + - type: object + required: + - data + properties: + data: + allOf: + - $ref: '#/components/schemas/InternalAccount' + - type: object + properties: + oldBalance: + $ref: '#/components/schemas/CurrencyAmount' + description: The account balance before the change + type: + type: string + enum: + - ACCOUNT.BALANCE_UPDATED diff --git a/openapi/components/schemas/customers/BulkUploadWebhookRequest.yaml b/openapi/components/schemas/customers/BulkUploadWebhookRequest.yaml deleted file mode 100644 index 712b43bc..00000000 --- a/openapi/components/schemas/customers/BulkUploadWebhookRequest.yaml +++ /dev/null @@ -1,12 +0,0 @@ -allOf: - - $ref: ../webhooks/BaseWebhook.yaml - - type: object - required: - - bulkCustomerImportJob - properties: - bulkCustomerImportJob: - $ref: ./BulkCustomerImportJob.yaml - type: - $ref: ../webhooks/WebhookType.yaml - description: Type of webhook event - example: BULK_UPLOAD diff --git a/openapi/components/schemas/transfers/ExternalAccountReference.yaml b/openapi/components/schemas/transfers/ExternalAccountReference.yaml new file mode 100644 index 00000000..b152f892 --- /dev/null +++ b/openapi/components/schemas/transfers/ExternalAccountReference.yaml @@ -0,0 +1,8 @@ +type: object +required: + - accountId +properties: + accountId: + type: string + description: Reference to an external account ID + example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 diff --git a/openapi/components/schemas/transfers/InternalAccountReference.yaml b/openapi/components/schemas/transfers/InternalAccountReference.yaml new file mode 100644 index 00000000..1cfaf0ee --- /dev/null +++ b/openapi/components/schemas/transfers/InternalAccountReference.yaml @@ -0,0 +1,8 @@ +type: object +required: + - accountId +properties: + accountId: + type: string + description: Reference to an internal account ID + example: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 diff --git a/openapi/components/schemas/transfers/TransferInRequest.yaml b/openapi/components/schemas/transfers/TransferInRequest.yaml index 9a0fbd0f..88cf56ea 100644 --- a/openapi/components/schemas/transfers/TransferInRequest.yaml +++ b/openapi/components/schemas/transfers/TransferInRequest.yaml @@ -4,24 +4,10 @@ required: - destination properties: source: - type: object - required: - - accountId - properties: - accountId: - type: string - description: Reference to an external account ID - example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + $ref: ./ExternalAccountReference.yaml description: Source external account details destination: - type: object - required: - - accountId - properties: - accountId: - type: string - description: Reference to an internal account ID - example: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + $ref: ./InternalAccountReference.yaml description: Destination internal account details amount: type: integer diff --git a/openapi/components/schemas/transfers/TransferOutRequest.yaml b/openapi/components/schemas/transfers/TransferOutRequest.yaml index 302def09..50e97f6a 100644 --- a/openapi/components/schemas/transfers/TransferOutRequest.yaml +++ b/openapi/components/schemas/transfers/TransferOutRequest.yaml @@ -4,24 +4,10 @@ required: - destination properties: source: - type: object - required: - - accountId - properties: - accountId: - type: string - description: Reference to an internal account ID - example: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + $ref: ./InternalAccountReference.yaml description: Source internal account details destination: - type: object - required: - - accountId - properties: - accountId: - type: string - description: Reference to an external account ID - example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + $ref: ./ExternalAccountReference.yaml description: Destination external account details amount: type: integer diff --git a/openapi/components/schemas/webhooks/AccountStatusWebhook.yaml b/openapi/components/schemas/webhooks/AccountStatusWebhook.yaml index 522e83ce..9d6fc7c4 100644 --- a/openapi/components/schemas/webhooks/AccountStatusWebhook.yaml +++ b/openapi/components/schemas/webhooks/AccountStatusWebhook.yaml @@ -2,20 +2,17 @@ allOf: - $ref: ./BaseWebhook.yaml - type: object required: - - accountId + - data properties: - accountId: + data: + allOf: + - $ref: ../customers/InternalAccount.yaml + - type: object + properties: + oldBalance: + $ref: ../common/CurrencyAmount.yaml + description: The account balance before the change + type: type: string - description: The id of the account whose balance has changed - oldBalance: - $ref: ../common/CurrencyAmount.yaml - newBalance: - $ref: ../common/CurrencyAmount.yaml - customerId: - type: string - description: The ID of the customer associated with the internal account - example: Customer:019542f5-b3e7-1d02-0000-000000000001 - platformCustomerId: - type: string - description: The ID of the customer as associated in your platform - example: 019542f5-b3e7-1d02-0000-000000000001 + enum: + - ACCOUNT.BALANCE_UPDATED diff --git a/openapi/components/schemas/webhooks/BaseWebhook.yaml b/openapi/components/schemas/webhooks/BaseWebhook.yaml index 7ba29727..17a3740f 100644 --- a/openapi/components/schemas/webhooks/BaseWebhook.yaml +++ b/openapi/components/schemas/webhooks/BaseWebhook.yaml @@ -3,14 +3,8 @@ required: - timestamp - id - type + - data properties: - timestamp: - type: string - format: date-time - description: >- - ISO8601 timestamp when the webhook was sent (can be used to - prevent replay attacks) - example: '2025-08-15T14:32:00Z' id: type: string description: >- @@ -19,13 +13,17 @@ properties: example: Webhook:019542f5-b3e7-1d02-0000-000000000007 type: $ref: ./WebhookType.yaml - description: Type of webhook event -discriminator: - propertyName: type - mapping: - INCOMING_PAYMENT: ./IncomingPaymentWebhook.yaml - OUTGOING_PAYMENT: ./OutgoingPaymentWebhook.yaml - TEST: ./TestWebhookRequest.yaml - BULK_UPLOAD: ../customers/BulkUploadWebhookRequest.yaml - INVITATION_CLAIMED: ./InvitationClaimedWebhook.yaml - KYC_STATUS: ./KycStatusWebhook.yaml + description: >- + Status-specific event type in OBJECT.EVENT dot-notation + (e.g., OUTGOING_PAYMENT.COMPLETED) + timestamp: + type: string + format: date-time + description: >- + ISO 8601 timestamp of when the webhook was sent + example: '2025-08-15T14:32:00Z' + data: + type: object + description: >- + The resource object. Contains the full resource as the corresponding + GET endpoint would return it. diff --git a/openapi/components/schemas/webhooks/BulkUploadWebhook.yaml b/openapi/components/schemas/webhooks/BulkUploadWebhook.yaml new file mode 100644 index 00000000..04463232 --- /dev/null +++ b/openapi/components/schemas/webhooks/BulkUploadWebhook.yaml @@ -0,0 +1,13 @@ +allOf: + - $ref: ./BaseWebhook.yaml + - type: object + required: + - data + properties: + data: + $ref: ../customers/BulkCustomerImportJob.yaml + type: + type: string + enum: + - BULK_UPLOAD.COMPLETED + - BULK_UPLOAD.FAILED diff --git a/openapi/components/schemas/webhooks/CustomerKycWebhook.yaml b/openapi/components/schemas/webhooks/CustomerKycWebhook.yaml new file mode 100644 index 00000000..4b7c86cc --- /dev/null +++ b/openapi/components/schemas/webhooks/CustomerKycWebhook.yaml @@ -0,0 +1,16 @@ +allOf: + - $ref: ./BaseWebhook.yaml + - type: object + required: + - data + properties: + data: + $ref: ../customers/IndividualCustomer.yaml + type: + type: string + enum: + - CUSTOMER.KYC_APPROVED + - CUSTOMER.KYC_REJECTED + - CUSTOMER.KYC_SUBMITTED + - CUSTOMER.KYC_MANUALLY_APPROVED + - CUSTOMER.KYC_MANUALLY_REJECTED diff --git a/openapi/components/schemas/webhooks/IncomingPaymentWebhook.yaml b/openapi/components/schemas/webhooks/IncomingPaymentWebhook.yaml index df21f150..db3ef600 100644 --- a/openapi/components/schemas/webhooks/IncomingPaymentWebhook.yaml +++ b/openapi/components/schemas/webhooks/IncomingPaymentWebhook.yaml @@ -2,20 +2,25 @@ allOf: - $ref: ./BaseWebhook.yaml - type: object required: - - transaction + - data properties: - transaction: - $ref: ../transactions/IncomingTransaction.yaml + data: + allOf: + - $ref: ../transactions/IncomingTransaction.yaml + - type: object + properties: + requestedReceiverCustomerInfoFields: + type: array + items: + $ref: ../common/CounterpartyFieldDefinition.yaml + description: >- + Information required by the sender's VASP about the recipient. + Platform must provide these in the 200 OK response if approving. + Note that this only includes fields which Grid does not + already have from initial customer registration. type: - $ref: ./WebhookType.yaml - description: Type of webhook event - example: INCOMING_PAYMENT - requestedReceiverCustomerInfoFields: - type: array - items: - $ref: ../common/CounterpartyFieldDefinition.yaml - description: >- - Information required by the sender's VASP about the recipient. - Platform must provide these in the 200 OK response if approving. - Note that this only includes fields which Grid does not - already have from initial customer registration. + type: string + enum: + - INCOMING_PAYMENT.PENDING + - INCOMING_PAYMENT.COMPLETED + - INCOMING_PAYMENT.FAILED diff --git a/openapi/components/schemas/webhooks/InvitationClaimedWebhook.yaml b/openapi/components/schemas/webhooks/InvitationClaimedWebhook.yaml index a9087f37..bc9d0a48 100644 --- a/openapi/components/schemas/webhooks/InvitationClaimedWebhook.yaml +++ b/openapi/components/schemas/webhooks/InvitationClaimedWebhook.yaml @@ -2,12 +2,11 @@ allOf: - $ref: ./BaseWebhook.yaml - type: object required: - - invitation + - data properties: - invitation: + data: $ref: ../invitations/UmaInvitation.yaml type: type: string - enum: [INVITATION_CLAIMED] - description: Type of webhook event - example: INVITATION_CLAIMED + enum: + - INVITATION.CLAIMED diff --git a/openapi/components/schemas/webhooks/KycStatusWebhook.yaml b/openapi/components/schemas/webhooks/KycStatusWebhook.yaml deleted file mode 100644 index 082e71c4..00000000 --- a/openapi/components/schemas/webhooks/KycStatusWebhook.yaml +++ /dev/null @@ -1,13 +0,0 @@ -allOf: - - $ref: ./BaseWebhook.yaml - - type: object - required: - - customerId - - kycStatus - properties: - customerId: - type: string - description: System generated id of the customer - example: Customer:019542f5-b3e7-1d02-0000-000000000001 - kycStatus: - $ref: ../customers/KycStatus.yaml diff --git a/openapi/components/schemas/webhooks/OutgoingPaymentWebhook.yaml b/openapi/components/schemas/webhooks/OutgoingPaymentWebhook.yaml index 948a828d..b7d39897 100644 --- a/openapi/components/schemas/webhooks/OutgoingPaymentWebhook.yaml +++ b/openapi/components/schemas/webhooks/OutgoingPaymentWebhook.yaml @@ -2,11 +2,17 @@ allOf: - $ref: ./BaseWebhook.yaml - type: object required: - - transaction + - data properties: - transaction: + data: $ref: ../transactions/OutgoingTransaction.yaml type: - $ref: ./WebhookType.yaml - description: Type of webhook event - example: OUTGOING_PAYMENT + type: string + enum: + - OUTGOING_PAYMENT.PENDING + - OUTGOING_PAYMENT.PROCESSING + - OUTGOING_PAYMENT.SENT + - OUTGOING_PAYMENT.COMPLETED + - OUTGOING_PAYMENT.FAILED + - OUTGOING_PAYMENT.REFUNDED + - OUTGOING_PAYMENT.EXPIRED diff --git a/openapi/components/schemas/webhooks/TestWebhookRequest.yaml b/openapi/components/schemas/webhooks/TestWebhookRequest.yaml index c7282438..e3e19cbf 100644 --- a/openapi/components/schemas/webhooks/TestWebhookRequest.yaml +++ b/openapi/components/schemas/webhooks/TestWebhookRequest.yaml @@ -3,6 +3,6 @@ allOf: - type: object properties: type: - $ref: ./WebhookType.yaml - description: Type of webhook event - example: TEST + type: string + enum: + - TEST diff --git a/openapi/components/schemas/webhooks/WebhookType.yaml b/openapi/components/schemas/webhooks/WebhookType.yaml index 6ed69c8c..de0b3653 100644 --- a/openapi/components/schemas/webhooks/WebhookType.yaml +++ b/openapi/components/schemas/webhooks/WebhookType.yaml @@ -1,12 +1,26 @@ type: string enum: - - INCOMING_PAYMENT - - OUTGOING_PAYMENT + - OUTGOING_PAYMENT.PENDING + - OUTGOING_PAYMENT.PROCESSING + - OUTGOING_PAYMENT.SENT + - OUTGOING_PAYMENT.COMPLETED + - OUTGOING_PAYMENT.FAILED + - OUTGOING_PAYMENT.REFUNDED + - OUTGOING_PAYMENT.EXPIRED + - INCOMING_PAYMENT.PENDING + - INCOMING_PAYMENT.COMPLETED + - INCOMING_PAYMENT.FAILED + - CUSTOMER.KYC_APPROVED + - CUSTOMER.KYC_REJECTED + - CUSTOMER.KYC_SUBMITTED + - CUSTOMER.KYC_MANUALLY_APPROVED + - CUSTOMER.KYC_MANUALLY_REJECTED + - ACCOUNT.BALANCE_UPDATED + - INVITATION.CLAIMED + - BULK_UPLOAD.COMPLETED + - BULK_UPLOAD.FAILED - TEST - - BULK_UPLOAD - - INVITATION_CLAIMED - - KYC_STATUS - - ACCOUNT_STATUS description: >- - Type of webhook event, used by the receiver to identify which webhook is being - received + 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. diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 45d66a13..0382853b 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -58,7 +58,6 @@ components: Secp256r1 (P-256) asymmetric signature of the webhook payload, which can be used to verify that the webhook was sent by Grid. - To verify the signature: 1. Get the Grid public key provided to you during integration @@ -72,19 +71,6 @@ components: If the signature verification succeeds, the webhook is authentic. If not, it should be rejected. - schemas: - AllErrors: - anyOf: - - $ref: components/schemas/errors/Error400.yaml - - $ref: components/schemas/errors/Error401.yaml - - $ref: components/schemas/errors/Error403.yaml - - $ref: components/schemas/errors/Error404.yaml - - $ref: components/schemas/errors/Error409.yaml - - $ref: components/schemas/errors/Error410.yaml - - $ref: components/schemas/errors/Error412.yaml - - $ref: components/schemas/errors/Error424.yaml - - $ref: components/schemas/errors/Error500.yaml - - $ref: components/schemas/errors/Error501.yaml paths: /config: $ref: paths/platform/config.yaml @@ -130,8 +116,8 @@ paths: $ref: paths/transactions/transactions_{transactionId}_approve.yaml /transactions/{transactionId}/reject: $ref: paths/transactions/transactions_{transactionId}_reject.yaml - /webhooks/test: - $ref: paths/webhooks/webhooks_test.yaml + /sandbox/webhooks/test: + $ref: paths/sandbox/sandbox_webhooks_test.yaml /customers/bulk/csv: $ref: paths/customers/customers_bulk_csv.yaml /customers/bulk/jobs/{jobId}: @@ -168,7 +154,7 @@ webhooks: invitation-claimed: $ref: webhooks/invitation-claimed.yaml kyc-status: - $ref: webhooks/kyc-status.yaml + $ref: webhooks/customer-kyc.yaml account-status: $ref: webhooks/account-status.yaml security: diff --git a/openapi/paths/webhooks/webhooks_test.yaml b/openapi/paths/sandbox/sandbox_webhooks_test.yaml similarity index 98% rename from openapi/paths/webhooks/webhooks_test.yaml rename to openapi/paths/sandbox/sandbox_webhooks_test.yaml index 7186edc2..97f0d6e0 100644 --- a/openapi/paths/webhooks/webhooks_test.yaml +++ b/openapi/paths/sandbox/sandbox_webhooks_test.yaml @@ -3,7 +3,7 @@ post: description: Send a test webhook to the configured endpoint operationId: sendTestWebhook tags: - - Webhooks + - Sandbox security: - BasicAuth: [] responses: diff --git a/openapi/webhooks/account-status.yaml b/openapi/webhooks/account-status.yaml index 02954cf7..8ad022cf 100644 --- a/openapi/webhooks/account-status.yaml +++ b/openapi/webhooks/account-status.yaml @@ -1,7 +1,9 @@ post: - summary: Account status notification webhook + summary: Account status webhook description: > - Webhook that is called when the balance of an account changes + Webhook that is called when the status of an internal account changes. + This includes balance updates and may include additional account events + in the future. This endpoint should be implemented by clients of the Grid API. @@ -26,10 +28,11 @@ post: should be rejected. - ### Account status + ### Event types - When the balance of an internal account changes, we will push a notification with information on the account, the new - balance, and who the account belongs to. + - `ACCOUNT.BALANCE_UPDATED` — Fired when the balance of an internal + account changes. The `data` payload contains the full internal account + object along with the `oldBalance` prior to the change. operationId: accountStatusWebhook @@ -47,27 +50,29 @@ post: balanceDecrease: summary: A transaction just cleared a customer account and the balance has decreased value: - account: - accountId: Account:019542f5-b3e7-1d02-0000-000000000005 - oldBalance: - amount: 50000 + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: ACCOUNT.BALANCE_UPDATED + timestamp: '2025-08-15T14:32:00Z' + data: + id: InternalAccount:019542f5-b3e7-1d02-0000-000000000005 + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + balance: + amount: 10000 currency: code: USD name: United States Dollar symbol: $ decimals: 2 - newBalance: - amount: 10000 + oldBalance: + amount: 50000 currency: code: USD name: United States Dollar symbol: $ decimals: 2 - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - platformCustomerId: 019542f5-b3e7-1d02-0000-000000000001 - timestamp: '2025-08-15T14:32:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000007 - type: ACCOUNT_STATUS + fundingPaymentInstructions: [] + createdAt: '2025-08-01T10:00:00Z' + updatedAt: '2025-08-15T14:32:00Z' responses: '200': description: > @@ -85,7 +90,7 @@ post: schema: $ref: ../components/schemas/errors/Error401.yaml '409': - description: Conflict - Webhook has already been processed (duplicate webhookId) + description: Conflict - Webhook has already been processed (duplicate id) content: application/json: schema: diff --git a/openapi/webhooks/bulk-upload.yaml b/openapi/webhooks/bulk-upload.yaml index 81fddd3b..ccaed41f 100644 --- a/openapi/webhooks/bulk-upload.yaml +++ b/openapi/webhooks/bulk-upload.yaml @@ -38,12 +38,15 @@ post: content: application/json: schema: - $ref: '../components/schemas/customers/BulkUploadWebhookRequest.yaml' + $ref: '../components/schemas/webhooks/BulkUploadWebhook.yaml' examples: completedUpload: summary: Successful bulk upload completion value: - bulkCustomerImportJob: + id: Webhook:019542f5-b3e7-1d02-0000-000000000008 + type: BULK_UPLOAD.COMPLETED + timestamp: '2025-08-15T14:32:00Z' + data: jobId: Job:019542f5-b3e7-1d02-0000-000000000006 status: COMPLETED progress: @@ -52,13 +55,13 @@ post: successful: 5000 failed: 0 errors: [] - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000008 - type: BULK_UPLOAD - timestamp: '2025-08-15T14:32:00Z' failedUpload: summary: Failed bulk upload value: - bulkCustomerImportJob: + id: Webhook:019542f5-b3e7-1d02-0000-000000000008 + type: BULK_UPLOAD.FAILED + timestamp: '2025-08-15T14:32:00Z' + data: jobId: Job:019542f5-b3e7-1d02-0000-000000000006 status: FAILED progress: @@ -74,9 +77,6 @@ post: details: reason: missing_required_column column: umaAddress - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000008 - type: BULK_UPLOAD - timestamp: '2025-08-15T14:32:00Z' responses: '200': description: Webhook received successfully @@ -93,7 +93,7 @@ post: schema: $ref: ../components/schemas/errors/Error401.yaml '409': - description: Conflict - Webhook has already been processed (duplicate webhookId) + description: Conflict - Webhook has already been processed (duplicate id) content: application/json: schema: diff --git a/openapi/webhooks/kyc-status.yaml b/openapi/webhooks/customer-kyc.yaml similarity index 60% rename from openapi/webhooks/kyc-status.yaml rename to openapi/webhooks/customer-kyc.yaml index 92b243f2..1b70e7b0 100644 --- a/openapi/webhooks/kyc-status.yaml +++ b/openapi/webhooks/customer-kyc.yaml @@ -51,16 +51,51 @@ post: content: application/json: schema: - $ref: ../components/schemas/webhooks/KycStatusWebhook.yaml + $ref: ../components/schemas/webhooks/CustomerKycWebhook.yaml examples: kycApprovedWebhook: summary: When a customer KYC has been approved value: - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - kycStatus: APPROVED - type: KYC_STATUS + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: CUSTOMER.KYC_APPROVED timestamp: '2025-08-15T14:32:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000007 + data: + id: Customer:019542f5-b3e7-1d02-0000-000000000001 + platformCustomerId: 9f84e0c2a72c4fa + customerType: INDIVIDUAL + umaAddress: $john.doe@uma.domain.com + kycStatus: APPROVED + fullName: John Michael Doe + birthDate: '1990-01-15' + nationality: US + address: + line1: 123 Main Street + line2: Apt 4B + city: San Francisco + state: CA + postalCode: '94105' + country: US + createdAt: '2025-07-21T17:32:28Z' + updatedAt: '2025-07-21T17:32:28Z' + isDeleted: false + kycRejectedWebhook: + summary: When a customer KYC has been rejected + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000008 + type: CUSTOMER.KYC_REJECTED + timestamp: '2025-08-15T14:32:00Z' + data: + id: Customer:019542f5-b3e7-1d02-0000-000000000002 + platformCustomerId: 4b7c1e9d3f5a8e2 + customerType: INDIVIDUAL + umaAddress: $jane.smith@uma.domain.com + kycStatus: REJECTED + fullName: Jane Smith + birthDate: '1988-03-22' + nationality: US + createdAt: '2025-07-21T17:32:28Z' + updatedAt: '2025-08-15T14:32:00Z' + isDeleted: false responses: '200': description: > @@ -78,7 +113,7 @@ post: schema: $ref: ../components/schemas/errors/Error401.yaml '409': - description: Conflict - Webhook has already been processed (duplicate webhookId) + description: Conflict - Webhook has already been processed (duplicate id) content: application/json: schema: diff --git a/openapi/webhooks/incoming-payment.yaml b/openapi/webhooks/incoming-payment.yaml index 78ad1b99..c0646b01 100644 --- a/openapi/webhooks/incoming-payment.yaml +++ b/openapi/webhooks/incoming-payment.yaml @@ -70,7 +70,10 @@ post: pendingPayment: summary: Pending payment example requiring approval value: - transaction: + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: INCOMING_PAYMENT.PENDING + timestamp: '2025-08-15T14:32:00Z' + data: id: Transaction:019542f5-b3e7-1d02-0000-000000000005 status: PENDING type: INCOMING @@ -85,24 +88,24 @@ post: decimals: 2 customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 platformCustomerId: 18d3e5f7b4a9c2 - reconciliationInstructions: - reference: REF-123456789 counterpartyInformation: FULL_NAME: John Sender BIRTH_DATE: '1985-06-15' NATIONALITY: US - requestedReceiverCustomerInfoFields: - - name: NATIONALITY - mandatory: true - - name: ADDRESS - mandatory: false - timestamp: '2025-08-15T14:32:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000007 - type: INCOMING_PAYMENT + reconciliationInstructions: + reference: REF-123456789 + requestedReceiverCustomerInfoFields: + - name: NATIONALITY + mandatory: true + - name: ADDRESS + mandatory: false incomingCompletedPayment: summary: Completed payment notification value: - transaction: + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: INCOMING_PAYMENT.COMPLETED + timestamp: '2025-08-15T14:32:00Z' + data: id: Transaction:019542f5-b3e7-1d02-0000-000000000005 status: COMPLETED type: INCOMING @@ -122,9 +125,6 @@ post: description: Payment for services reconciliationInstructions: reference: REF-123456789 - timestamp: '2025-08-15T14:32:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000007 - type: INCOMING_PAYMENT responses: '200': description: > @@ -174,7 +174,7 @@ post: schema: $ref: ../components/schemas/webhooks/IncomingPaymentWebhookForbiddenResponse.yaml '409': - description: Conflict - Webhook has already been processed (duplicate webhookId) + description: Conflict - Webhook has already been processed (duplicate id) content: application/json: schema: diff --git a/openapi/webhooks/invitation-claimed.yaml b/openapi/webhooks/invitation-claimed.yaml index da81b05a..0f67ed9e 100644 --- a/openapi/webhooks/invitation-claimed.yaml +++ b/openapi/webhooks/invitation-claimed.yaml @@ -60,7 +60,10 @@ post: claimedInvitation: summary: Invitation claimed notification value: - invitation: + id: Webhook:019542f5-b3e7-1d02-0000-000000000008 + type: INVITATION.CLAIMED + timestamp: '2025-09-01T15:45:00Z' + data: code: 019542f5 createdAt: '2025-09-01T14:30:00Z' claimedAt: '2025-09-01T15:45:00Z' @@ -68,9 +71,6 @@ post: inviteeUma: $invitee@uma.domain status: CLAIMED url: https://uma.me/i/019542f5 - timestamp: '2025-09-01T15:45:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000008 - type: INVITATION_CLAIMED responses: '200': description: Webhook received successfully @@ -87,7 +87,7 @@ post: schema: $ref: ../components/schemas/errors/Error401.yaml '409': - description: Conflict - Webhook has already been processed (duplicate webhookId) + description: Conflict - Webhook has already been processed (duplicate id) content: application/json: schema: diff --git a/openapi/webhooks/outgoing-payment.yaml b/openapi/webhooks/outgoing-payment.yaml index f9f62ec4..1bba152d 100644 --- a/openapi/webhooks/outgoing-payment.yaml +++ b/openapi/webhooks/outgoing-payment.yaml @@ -27,7 +27,7 @@ post: This webhook is informational only and is sent when an outgoing payment - completes successfully or fails. + completes successfully, fails, or is refunded. operationId: outgoingPaymentWebhook tags: - Webhooks @@ -43,7 +43,10 @@ post: outgoingCompletedPayment: summary: Completed outgoing payment value: - transaction: + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: OUTGOING_PAYMENT.COMPLETED + timestamp: '2025-08-15T14:32:00Z' + data: id: Transaction:019542f5-b3e7-1d02-0000-000000000005 status: COMPLETED type: OUTGOING @@ -65,30 +68,20 @@ post: decimals: 2 customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 platformCustomerId: 18d3e5f7b4a9c2 - settlementTime: '2025-08-15T14:30:00Z' - createdAt: '2025-08-15T14:25:18Z' - description: 'Payment for invoice #1234' exchangeRate: 0.92 quoteId: Quote:019542f5-b3e7-1d02-0000-000000000006 - paymentInstructions: - - accountOrWalletInfo: - reference: UMA-Q12345-REF - accountType: US_ACCOUNT - accountNumber: 987654321 - routingNumber: 123456789 - accountCategory: CHECKING - bankName: Chase Bank - - accountOrWalletInfo: - accountType: SOLANA_WALLET - assetType: USDC - address: 4Nd1m6Qkq7RfKuE5vQ9qP9Tn6H94Ueqb4xXHzsAbd8Wg - timestamp: '2025-08-15T14:32:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000007 - type: OUTGOING_PAYMENT + settledAt: '2025-08-15T14:30:00Z' + createdAt: '2025-08-15T14:25:18Z' + description: 'Payment for invoice #1234' + paymentInstructions: [] + rateDetails: {} failedPayment: summary: Failed outgoing payment value: - transaction: + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: OUTGOING_PAYMENT.FAILED + timestamp: '2025-08-15T14:32:00Z' + data: id: Transaction:019542f5-b3e7-1d02-0000-000000000005 status: FAILED type: OUTGOING @@ -105,21 +98,7 @@ post: platformCustomerId: 18d3e5f7b4a9c2 createdAt: '2025-08-15T14:25:18Z' quoteId: Quote:019542f5-b3e7-1d02-0000-000000000006 - paymentInstructions: - - accountOrWalletInfo: - reference: UMA-Q12345-REF - accountType: US_ACCOUNT - accountNumber: 987654321 - routingNumber: 123456789 - accountCategory: CHECKING - bankName: Chase Bank - - accountOrWalletInfo: - accountType: SOLANA_WALLET - assetType: USDC - address: 4Nd1m6Qkq7RfKuE5vQ9qP9Tn6H94Ueqb4xXHzsAbd8Wg - timestamp: '2025-08-15T14:32:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000007 - type: OUTGOING_PAYMENT + failureReason: INSUFFICIENT_FUNDS responses: '200': description: Webhook received successfully @@ -136,7 +115,7 @@ post: schema: $ref: ../components/schemas/errors/Error401.yaml '409': - description: Conflict - Webhook has already been processed (duplicate webhookId) + description: Conflict - Webhook has already been processed (duplicate id) content: application/json: schema: diff --git a/openapi/webhooks/test-webhook.yaml b/openapi/webhooks/test-webhook.yaml index 242cd832..355264f6 100644 --- a/openapi/webhooks/test-webhook.yaml +++ b/openapi/webhooks/test-webhook.yaml @@ -45,9 +45,10 @@ post: testWebhook: summary: Test webhook example value: - timestamp: '2025-08-15T14:32:00Z' - webhookId: Webhook:019542f5-b3e7-1d02-0000-000000000001 + id: Webhook:019542f5-b3e7-1d02-0000-000000000001 type: TEST + timestamp: '2025-08-15T14:32:00Z' + data: {} responses: '200': description: >- @@ -66,7 +67,7 @@ post: schema: $ref: ../components/schemas/errors/Error401.yaml '409': - description: Conflict - Webhook has already been processed (duplicate webhookId) + description: Conflict - Webhook has already been processed (duplicate id) content: application/json: schema: