diff --git a/api-specs/v1/crowdsplits.yaml b/api-specs/v1/crowdsplits.yaml index 8bb6a3b..b576a98 100644 --- a/api-specs/v1/crowdsplits.yaml +++ b/api-specs/v1/crowdsplits.yaml @@ -38,20 +38,17 @@ info: Use the envelope's `id` for idempotency — the same `id` is used for all retry attempts of a given notification. - Failed deliveries are retried on a linear schedule controlled by the - `WEBHOOK_MAXIMUM_RETRY_COUNT` and `WEBHOOK_RETRY_INTERVAL` deployment - settings. The *n*-th retry is scheduled - `n × WEBHOOK_RETRY_INTERVAL × 60` seconds after the previous failed - attempt, and retries stop once `n × WEBHOOK_RETRY_INTERVAL` exceeds - `WEBHOOK_MAXIMUM_RETRY_COUNT`. Total delivery attempts equal - `floor(WEBHOOK_MAXIMUM_RETRY_COUNT / WEBHOOK_RETRY_INTERVAL) + 1`. + Failed deliveries are retried on the contract's exponential schedule + (§2.9.3): **7 attempts** at 0 / 30s / 5min / 30min / 2h / 8h / 24h + from initial dispatch. After the final attempt the notification moves + to a **dead-letter** state — list with + `GET /api/v1/merchant/webhooks/notifications?status=dead_lettered` + and replay via the notifications surface. - With the reference configuration - (`WEBHOOK_MAXIMUM_RETRY_COUNT=15`, `WEBHOOK_RETRY_INTERVAL=5`) this - yields **4 total delivery attempts**, with retries scheduled 5, 10, - and 15 minutes after each preceding failure. After the final attempt - the notification is left un-acknowledged and no further attempts are - made. + Webhook bodies are capped at **256 KB** (serialized, uncompressed). + On the rare oversized payload, only `provider_response` is replaced + by a truncated summary (`truncated: true`) whose `full_resource_url` + serves the full snapshot — see the **Provider Snapshots** operations. ## Pagination @@ -140,6 +137,11 @@ tags: description: | Proxy endpoint that forwards provider-specific calls whose shape varies by upstream provider. + - name: Statuspage + description: | + CS Admin/Owner endpoints for managing Atlassian Statuspage scheduled + maintenance windows. Incidents are managed automatically by the sync + cron based on health probe transitions. - name: System description: Health check and service-metadata endpoints. - name: Webhooks @@ -151,6 +153,13 @@ tags: Reference list of every webhook event CrowdSplit can deliver to a merchant URL. See the root-level `Webhooks` section above for the envelope shape, delivery headers, retry policy, and signing scheme. + - name: Provider Snapshots + description: | + Fetch the full untruncated `provider_response` for a specific resource + version (§10h). Only for resolving webhook payloads truncated by the + §2.9.3 256KB cap (`provider_response.truncated: true`) — follow the + payload's `full_resource_url`. Not for routine snapshot access; use + the per-resource GET endpoints for normal reads. paths: /api/auth/signup: post: @@ -586,50 +595,36 @@ paths: $ref: '#/components/schemas/ApiErrorResponse' '422': $ref: '#/components/responses/ValidationError' - /api/v1/payments: + /api/auth/signin/2fa: post: - operationId: createPayment - summary: Create a payment + operationId: signin2fa + summary: Complete sign-in with a 2FA code description: | - Create a payment intent with the provided details. Supports multiple payment providers - (Stripe) and payment methods (Card). - - The request body structure varies by provider and payment method type. - Field casing note: `provider`, `currency`, `payment_method.type`, and `capture_method` - are case-insensitive on input (automatically uppercased internally). + Public endpoint. Exchange the short-lived 2FA challenge token returned by + `POST /api/auth/signin` (when 2FA is enabled) plus a TOTP or backup code + for a full session. The refresh token is also set as an HTTP-only cookie. tags: - - Payments - security: - - bearerAuth: [] - parameters: - - $ref: '#/components/parameters/RefAppKeyHeader' - - $ref: '#/components/parameters/PaymentTypeHeader' + - Auth + security: [] requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/CreatePaymentRequest' - examples: - stripeCard: - summary: Stripe card payment - value: - provider: stripe - source: - amount: 5000 - currency: usd - customer: - id: 550e8400-e29b-41d4-a716-446655440000 - payment_method: - type: card - id: 660e8400-e29b-41d4-a716-446655440000 - capture_method: automatic - confirm: true - metadata: - order_id: ORD-12345 + type: object + required: + - twoFactorToken + - code + properties: + twoFactorToken: + type: string + description: Challenge token from signin (tfaPending, 5m lifetime) + code: + type: string + description: TOTP code or single-use backup code responses: '200': - description: Payment created successfully + description: Authenticated content: application/json: schema: @@ -637,48 +632,62 @@ paths: - $ref: '#/components/schemas/ApiResponse' - type: object properties: - msg: - example: Transaction was Initiated Successfully! data: - $ref: '#/components/schemas/PaymentResponse' - '400': - description: Validation error or invalid state - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorResponse' + type: object + properties: + id: + type: string + email: + type: string + format: email + roles: + type: array + items: + type: string + accessToken: + type: string + refreshToken: + type: string + createdMillis: + type: integer '401': $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' - '404': - description: Customer not found - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorResponse' '422': $ref: '#/components/responses/ValidationError' - /api/v1/payments/{id}/confirm: + /api/auth/logout: post: - operationId: confirmPayment - summary: Confirm a payment - description: Confirm a previously created payment intent that was not auto-confirmed. + operationId: logout + summary: Log out + description: Revoke the current session and clear the refresh-token cookie. tags: - - Payments + - Auth security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - description: Payment transaction ID (UUID) - schema: - type: string - format: uuid + - accessTokenAuth: [] responses: '200': - description: Payment confirmed + description: Logged out + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + /api/auth/me: + get: + operationId: getCurrentUser + summary: Get the current user + description: | + Return the authenticated user's profile, roles, 2FA state, and merchant + link (including their per-merchant role, the source merchant-route + authorization enforces — so clients derive what to show from the same + truth the backend checks). + tags: + - Auth + security: + - accessTokenAuth: [] + responses: + '200': + description: Current user content: application/json: schema: @@ -686,46 +695,72 @@ paths: - $ref: '#/components/schemas/ApiResponse' - type: object properties: - msg: - example: Transaction is in Progress! data: - $ref: '#/components/schemas/PaymentResponse' - '400': - description: Invalid state for confirmation - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorResponse' + type: object + properties: + id: + type: string + description: User UID + email: + type: string + format: email + roles: + type: array + description: Global roles resolved from the database + items: + type: string + twoFactorEnabled: + type: boolean + description: Whether the user has completed TOTP enrollment + mfaSetupRequired: + type: boolean + description: | + True when this deployment requires the caller to enroll 2FA + before admin functions will work — i.e. MFA enforcement is on, + the caller holds a CS role, and they have not enrolled. Always + false for platform-side users, for enrolled CS staff, and where + enforcement is off. Clients use it to route to enrollment + instead of walking into a 403. + + Point-in-time and scoped to the MFA gate: an admin 2FA reset can + flip it mid-session (the caller's access token outlives the + reset), and the admin IP allowlist can deny independently of it. + Treat it as a hint and still handle a 403 whose failure reason is + `mfa_required`. + merchant: + type: object + nullable: true + description: Merchant link, or null for CS staff with no merchant + properties: + id: + type: integer + role: + type: string + nullable: true + description: The caller's role on this merchant (merchant_users.role) + is_owner: + type: boolean + description: Whether the caller owns this merchant '401': $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' '404': - description: Transaction not found - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorResponse' - /api/v1/payments/{id}/capture: - post: - operationId: capturePayment - summary: Capture a payment - description: Capture a previously authorized payment (for manual capture_method payments). + $ref: '#/components/responses/NotFoundError' + /api/auth/roles: + get: + operationId: listRoleCatalog + summary: List the role catalog + description: | + Role metadata catalog — the canonical labels and descriptions for + every CS and platform role, as rendered by dashboards. Static per + backend version, so adding or renaming a role propagates without a + frontend release. tags: - - Payments + - Auth security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - description: Payment transaction ID (UUID) - schema: - type: string - format: uuid + - accessTokenAuth: [] responses: '200': - description: Payment captured + description: Role catalog content: application/json: schema: @@ -733,38 +768,44 @@ paths: - $ref: '#/components/schemas/ApiResponse' - type: object properties: - msg: - example: Payment captured successfully data: - $ref: '#/components/schemas/PaymentResponse' - '400': - description: Error capturing payment - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorResponse' - /api/v1/payments/{id}/cancel: + type: array + items: + type: object + properties: + value: + type: string + description: Role identifier (e.g. ROLE_ADMIN) + scope: + type: string + enum: + - cs + - platform + description: Which side of the wire the role belongs to + label: + type: string + description: In-context display name + qualifiedLabel: + type: string + description: Disambiguated name for cross-side contexts + description: + type: string + '401': + $ref: '#/components/responses/UnauthorizedError' + /api/auth/me/2fa/enroll: post: - operationId: cancelPayment - summary: Cancel a payment + operationId: enroll2fa + summary: Begin 2FA enrollment description: | - Cancel a payment that is in INITIATED status. Only PAYMENT type transactions - can be cancelled; other transaction types will return an error. + Start TOTP enrollment and return the shared secret and otpauth URL. + 2FA is not active until confirmed via `POST /api/auth/me/2fa/confirm`. tags: - - Payments + - Auth security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - description: Payment transaction ID (UUID) - schema: - type: string - format: uuid + - accessTokenAuth: [] responses: '200': - description: Payment cancelled + description: Enrollment started content: application/json: schema: @@ -772,68 +813,51 @@ paths: - $ref: '#/components/schemas/ApiResponse' - type: object properties: - msg: - example: Transaction cancellation successful data: - $ref: '#/components/schemas/PaymentResponse' - '400': - description: Invalid state or wrong transaction type - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorResponse' + type: object + properties: + secret: + type: string + description: Base32 TOTP secret (shown once) + otpauthUrl: + type: string + description: otpauth:// URL for QR rendering '401': $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' '404': - description: Transaction not found + $ref: '#/components/responses/NotFoundError' + '409': + description: 2FA already enabled content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponse' - /api/v1/payments/{id}/refund: + /api/auth/me/2fa/confirm: post: - operationId: refundPayment - summary: Refund a payment + operationId: confirm2fa + summary: Confirm 2FA enrollment description: | - Create a refund for a completed payment. Partial refunds are supported by - specifying an amount less than the original. Total refund amount (including - previous refunds) cannot exceed the original transaction amount. + Verify a TOTP code to activate 2FA and receive single-use backup codes + (shown exactly once). tags: - - Payments + - Auth security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - description: Payment transaction ID (UUID) - schema: - type: string - format: uuid + - accessTokenAuth: [] requestBody: - required: false + required: true content: application/json: schema: type: object + required: + - code properties: - amount: - type: integer - minimum: 1 - description: Refund amount in smallest currency unit. Omit for full refund. - metadata: - type: object - description: Additional metadata for the refund - additionalProperties: true - example: - amount: 2500 - metadata: - reason: Customer request + code: + type: string + description: TOTP code from the authenticator app responses: '200': - description: Refund initiated + description: 2FA activated content: application/json: schema: @@ -841,35 +865,73 @@ paths: - $ref: '#/components/schemas/ApiResponse' - type: object properties: - msg: - example: Transaction was Initiated Successfully! data: - $ref: '#/components/schemas/PaymentResponse' + type: object + properties: + backupCodes: + type: array + items: + type: string '400': - description: Refund amount exceeds original or invalid state + description: No enrollment in progress content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponse' '401': $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' - '404': - description: Transaction not found + '422': + $ref: '#/components/responses/ValidationError' + /api/auth/me/2fa: + delete: + operationId: disable2fa + summary: Disable 2FA + description: | + Disable 2FA after re-confirming the account password. Not permitted for + the CS Owner. + tags: + - Auth + security: + - accessTokenAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - password + properties: + password: + type: string + responses: + '200': + description: 2FA disabled + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + description: Owner must keep 2FA enabled content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponse' + '404': + $ref: '#/components/responses/NotFoundError' '422': $ref: '#/components/responses/ValidationError' - /api/v1/merchant: + /api/auth/step-up: post: - operationId: createMerchant - summary: Create a merchant account - description: Create a new merchant with the provided business details. + operationId: stepUp + summary: Obtain a step-up token + description: | + Re-authenticate with the account password (and TOTP code when 2FA is + enabled) to obtain a short-lived step-up token for sensitive operations. tags: - - Merchants + - Auth security: - accessTokenAuth: [] requestBody: @@ -877,15 +939,18 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/CreateMerchantRequest' - example: - legal_name: Acme Corp - address: 123 Business St, Suite 100 - website: https://acme.example.com - tax_number: '12345678901' + type: object + required: + - password + properties: + password: + type: string + code: + type: string + description: TOTP code, required when 2FA is enabled responses: '200': - description: Merchant created successfully + description: Step-up granted content: application/json: schema: @@ -894,27 +959,28 @@ paths: - type: object properties: data: - $ref: '#/components/schemas/MerchantResponse' - '400': - description: Validation error or merchant already exists - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorResponse' + type: object + properties: + stepUpToken: + type: string + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' '422': $ref: '#/components/responses/ValidationError' - /api/v1/merchant/key/create: + /api/auth/me/sessions: get: - operationId: createMerchantKey - summary: Create merchant API keys - description: Generate API key pair (public + secret) for a merchant. The secret key is only shown once. + operationId: listSessions + summary: List active sessions + description: Return the user's active refresh-token sessions. tags: - - Merchants + - Auth security: - accessTokenAuth: [] responses: '200': - description: Keys generated successfully + description: Sessions retrieved content: application/json: schema: @@ -923,66 +989,111 @@ paths: - type: object properties: data: - type: object - properties: - secretKey: - type: string - description: Secret key (only shown once, store securely) - publicKey: - type: string - description: Public key - '400': - description: Error generating keys + type: array + items: + type: object + properties: + id: + type: integer + createdAt: + type: string + format: date-time + expiresAt: + type: string + format: date-time + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' + /api/auth/me/sessions/{id}: + parameters: + - name: id + in: path + required: true + description: Session ID to revoke + schema: + type: integer + delete: + operationId: revokeSession + summary: Revoke a session + description: Revoke a specific refresh-token session owned by the user. + tags: + - Auth + security: + - accessTokenAuth: [] + responses: + '200': + description: Session revoked content: application/json: schema: - $ref: '#/components/schemas/ApiErrorResponse' - /api/v1/merchant/token/grant: + $ref: '#/components/schemas/ApiResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' + /api/auth/me/email/change/request: post: - operationId: grantMerchantToken - summary: Grant merchant token + operationId: requestEmailChange + summary: Request an email change description: | - Authenticate using client credentials to obtain a Bearer token for API access. - This is the primary authentication method for merchant API calls. + Request changing the account email. Sends a verification link to the new + address. The token is returned in the response body only in DEV/TEST. tags: - - Merchants - security: [] + - Auth + security: + - accessTokenAuth: [] requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/GrantTokenRequest' - example: - client_id: app_abc123 - client_secret: sk_live_xyz789 - grant_type: client_credentials + type: object + required: + - newEmail + - password + properties: + newEmail: + type: string + format: email + password: + type: string responses: '200': - description: Token granted + description: Email-change requested content: application/json: schema: - $ref: '#/components/schemas/MerchantTokenResponse' - example: - access_token: eyJhbGciOiJIUzI1NiIs... - token_type: Bearer - expires_in: 3600000 + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + type: object + nullable: true + properties: + token: + type: string + description: Confirmation token — only returned in DEV/TEST + '401': + $ref: '#/components/responses/UnauthorizedError' '404': - description: Merchant not found or invalid credentials + $ref: '#/components/responses/NotFoundError' + '409': + description: Email already registered to another account content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponse' '422': $ref: '#/components/responses/ValidationError' - /api/v1/merchant/token/refresh: + /api/auth/me/email/change/confirm: post: - operationId: refreshMerchantToken - summary: Refresh merchant token - description: Generate new access and refresh tokens using a valid refresh token. + operationId: confirmEmailChange + summary: Confirm an email change + description: Public endpoint. Redeem the email-change token to update the address. tags: - - Merchants + - Auth security: [] requestBody: required: true @@ -991,14 +1102,13 @@ paths: schema: type: object required: - - refresh_token + - token properties: - refresh_token: + token: type: string - description: Refresh token from previous grant or refresh responses: '200': - description: Tokens refreshed + description: Email updated content: application/json: schema: @@ -1009,71 +1119,103 @@ paths: data: type: object properties: - accessToken: - type: string - refreshToken: + email: type: string + format: email '400': - description: Token expired or invalid + description: Invalid or expired token content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponse' '404': - description: Merchant not found + $ref: '#/components/responses/NotFoundError' + '409': + description: Email already registered to another account content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponse' - /api/v1/merchant/util/transfer-date: + /api/auth/password/change: post: - operationId: calculateTransferDate - summary: Calculate transfer date - description: Calculate the next available transfer date based on settlement date, region, and holiday configuration. + operationId: changePassword + summary: Change password + description: Change the account password after verifying the current password. tags: - - Merchants + - Auth security: - - bearerAuth: [] + - accessTokenAuth: [] requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/TransferDateRequest' + type: object + required: + - currentPassword + - newPassword + properties: + currentPassword: + type: string + newPassword: + type: string responses: '200': - description: Transfer date calculated - content: - application/json: - schema: - allOf: - - $ref: '#/components/schemas/ApiResponse' - - type: object - properties: - data: - type: object - properties: - transferDate: - type: string - format: date-time - '400': - description: Validation error + description: Password changed content: application/json: schema: - $ref: '#/components/schemas/ApiErrorResponse' - /api/v1/merchant/webhooks: - get: - operationId: listWebhooks - summary: List webhooks - description: List all registered webhooks for the authenticated merchant. + $ref: '#/components/schemas/ApiResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' + '422': + $ref: '#/components/responses/ValidationError' + /api/v1/payments: + post: + operationId: createPayment + summary: Create a payment + description: | + Create a payment intent with the provided details. Supports multiple payment providers + (Stripe) and payment methods (Card). + + The request body structure varies by provider and payment method type. + Field casing note: `provider`, `currency`, `payment_method.type`, and `capture_method` + are case-insensitive on input (automatically uppercased internally). tags: - - Webhooks + - Payments security: - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/RefAppKeyHeader' + - $ref: '#/components/parameters/PaymentTypeHeader' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreatePaymentRequest' + examples: + stripeCard: + summary: Stripe card payment + value: + provider: stripe + source: + amount: 5000 + currency: usd + customer: + id: 550e8400-e29b-41d4-a716-446655440000 + payment_method: + type: card + id: 660e8400-e29b-41d4-a716-446655440000 + capture_method: automatic + confirm: true + metadata: + order_id: ORD-12345 responses: '200': - description: Webhook list + description: Payment created successfully content: application/json: schema: @@ -1081,30 +1223,49 @@ paths: - $ref: '#/components/schemas/ApiResponse' - type: object properties: + msg: + example: Transaction was Initiated Successfully! data: - type: array - items: - $ref: '#/components/schemas/WebhookResponse' + $ref: '#/components/schemas/PaymentResponse' + '400': + description: Validation error or invalid state + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + description: Customer not found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '409': + $ref: '#/components/responses/ConflictError' + '422': + $ref: '#/components/responses/ValidationError' + /api/v1/payments/{id}/confirm: post: - operationId: registerWebhook - summary: Register a webhook - description: Register a new webhook endpoint for the merchant. Returns a signing secret for payload verification. + operationId: confirmPayment + summary: Confirm a payment + description: Confirm a previously created payment intent that was not auto-confirmed. tags: - - Webhooks + - Payments security: - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/RegisterWebhookRequest' - example: - url: https://example.com/webhooks/crowdsplit - description: Production webhook endpoint + parameters: + - name: id + in: path + required: true + description: Payment transaction ID — `pay_` or bare UUID (both accepted) + schema: + $ref: '#/components/schemas/PaymentId' responses: '200': - description: Webhook registered. Secret is only shown once. + description: Payment confirmed content: application/json: schema: @@ -1112,37 +1273,45 @@ paths: - $ref: '#/components/schemas/ApiResponse' - type: object properties: + msg: + example: Transaction is in Progress! data: - type: object - properties: - secret: - type: string - description: Signing secret for HMAC-SHA256 verification + $ref: '#/components/schemas/PaymentResponse' '400': - description: Validation error + description: Invalid state for confirmation content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponse' - /api/v1/merchant/webhooks/{id}: - get: - operationId: getWebhook - summary: Get webhook details - description: Get details of a specific webhook by ID. + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + description: Transaction not found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + /api/v1/payments/{id}/capture: + post: + operationId: capturePayment + summary: Capture a payment + description: Capture a previously authorized payment (for manual capture_method payments). tags: - - Webhooks + - Payments security: - bearerAuth: [] parameters: - name: id in: path required: true + description: Payment transaction ID — `pay_` or bare UUID (both accepted) schema: - type: string - format: uuid + $ref: '#/components/schemas/PaymentId' responses: '200': - description: Webhook details + description: Payment captured content: application/json: schema: @@ -1150,34 +1319,37 @@ paths: - $ref: '#/components/schemas/ApiResponse' - type: object properties: + msg: + example: Payment captured successfully data: - $ref: '#/components/schemas/WebhookResponse' - '404': - $ref: '#/components/responses/NotFoundError' - put: - operationId: updateWebhook - summary: Update a webhook - description: Update the URL or description of an existing webhook. + $ref: '#/components/schemas/PaymentResponse' + '400': + description: Error capturing payment + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + /api/v1/payments/{id}/cancel: + post: + operationId: cancelPayment + summary: Cancel a payment + description: | + Cancel a payment that is in INITIATED status. Only PAYMENT type transactions + can be cancelled; other transaction types will return an error. tags: - - Webhooks + - Payments security: - bearerAuth: [] parameters: - name: id in: path required: true + description: Payment transaction ID — `pay_` or bare UUID (both accepted) schema: - type: string - format: uuid - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateWebhookRequest' + $ref: '#/components/schemas/PaymentId' responses: '200': - description: Webhook updated + description: Payment cancelled content: application/json: schema: @@ -1185,68 +1357,119 @@ paths: - $ref: '#/components/schemas/ApiResponse' - type: object properties: + msg: + example: Transaction cancellation successful data: - $ref: '#/components/schemas/WebhookResponse' + $ref: '#/components/schemas/PaymentResponse' + '400': + description: Invalid state or wrong transaction type + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' '404': - $ref: '#/components/responses/NotFoundError' - delete: - operationId: deleteWebhook - summary: Delete a webhook + description: Transaction not found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + /api/v1/payments/{id}/refund: + post: + operationId: refundPayment + summary: Refund a payment description: | - Remove a registered webhook URL. New events for this merchant - will no longer be dispatched to the deleted URL. Notifications - already queued on the broker at the moment of deletion continue - through their retry schedule until acknowledged or exhausted — - the delete does not purge in-flight deliveries. + Create a refund for a completed payment. Partial refunds are supported by + specifying an amount less than the original. Total refund amount (including + previous refunds) cannot exceed the original transaction amount. tags: - - Webhooks + - Payments security: - bearerAuth: [] parameters: - name: id in: path required: true + description: Payment transaction ID — `pay_` or bare UUID (both accepted) schema: - type: string - format: uuid + $ref: '#/components/schemas/PaymentId' + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + amount: + type: integer + minimum: 1 + description: Refund amount in smallest currency unit. Omit for full refund. + metadata: + type: object + description: Additional metadata for the refund + additionalProperties: true + example: + amount: 2500 + metadata: + reason: Customer request responses: '200': - description: Webhook deleted + description: Refund initiated content: application/json: schema: - $ref: '#/components/schemas/ApiResponse' + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + msg: + example: Transaction was Initiated Successfully! + data: + $ref: '#/components/schemas/PaymentResponse' + '400': + description: Refund amount exceeds original or invalid state + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' '401': $ref: '#/components/responses/UnauthorizedError' '403': $ref: '#/components/responses/ForbiddenError' '404': - $ref: '#/components/responses/NotFoundError' - /api/v1/merchant/webhooks/{id}/toggle: - patch: - operationId: toggleWebhookStatus - summary: Toggle webhook active status - description: | - Flip a registered webhook between active and inactive states - without deleting it. Useful for planned merchant-side maintenance - windows or temporarily silencing a noisy endpoint. Inactive - webhooks receive no new dispatches; deliveries already queued - before the toggle may still fire once before the state takes - effect. + description: Transaction not found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '422': + $ref: '#/components/responses/ValidationError' + /api/v1/merchant: + post: + operationId: createMerchant + summary: Create a merchant account + description: Create a new merchant with the provided business details. tags: - - Webhooks + - Merchants security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid + - accessTokenAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateMerchantRequest' + example: + legal_name: Acme Corp + address: 123 Business St, Suite 100 + website: https://acme.example.com + tax_number: '12345678901' responses: '200': - description: Webhook status toggled + description: Merchant created successfully content: application/json: schema: @@ -1255,24 +1478,28 @@ paths: - type: object properties: data: - $ref: '#/components/schemas/WebhookResponse' - '404': - $ref: '#/components/responses/NotFoundError' - /api/v1/merchant/webhooks/notifications: + $ref: '#/components/schemas/MerchantResponse' + '400': + description: Validation error or merchant already exists + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '422': + $ref: '#/components/responses/ValidationError' get: - operationId: listWebhookNotifications - summary: List webhook notifications - description: List all webhook notification events for the merchant with pagination. + operationId: listMerchants + summary: List the caller's merchants + description: | + Return the merchants the authenticated user is a member of. A Platform + Admin owns at most one merchant, so this is typically a 0- or 1-item list. tags: - - Webhooks + - Merchants security: - - bearerAuth: [] - parameters: - - $ref: '#/components/parameters/LimitQuery' - - $ref: '#/components/parameters/OffsetQuery' + - accessTokenAuth: [] responses: '200': - description: Notification list + description: Merchants retrieved content: application/json: schema: @@ -1281,34 +1508,23 @@ paths: - type: object properties: data: - type: object - properties: - count: - type: integer - description: Total notification count - notification_list: - type: array - items: - $ref: '#/components/schemas/WebhookNotification' - /api/v1/merchant/webhooks/notifications/{id}: + type: array + items: + $ref: '#/components/schemas/MerchantResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + /api/v1/merchant/key/create: get: - operationId: getWebhookNotification - summary: Get webhook notification details - description: Get details of a specific webhook notification event. + operationId: createMerchantKey + summary: Create merchant API keys + description: Generate API key pair (public + secret) for a merchant. The secret key is only shown once. tags: - - Webhooks + - Merchants security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid + - accessTokenAuth: [] responses: '200': - description: Notification details + description: Keys generated successfully content: application/json: schema: @@ -1317,129 +1533,82 @@ paths: - type: object properties: data: - $ref: '#/components/schemas/WebhookNotification' - '404': - $ref: '#/components/responses/NotFoundError' - /api/v1/customers: + type: object + properties: + secretKey: + type: string + description: Secret key (only shown once, store securely) + publicKey: + type: string + description: Public key + '400': + description: Error generating keys + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + /api/v1/merchant/token/grant: post: - operationId: createCustomer - summary: Create a customer + operationId: grantMerchantToken + summary: Grant merchant token description: | - Register a new customer. Fields document_type, country_code, and gender - are case-insensitive (uppercased internally, returned lowercased). + Authenticate using client credentials to obtain a Bearer token for API access. + This is the primary authentication method for merchant API calls. tags: - - Customers - security: - - bearerAuth: [] + - Merchants + security: [] requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/CreateCustomerRequest' + $ref: '#/components/schemas/GrantTokenRequest' example: - document_number: '12345678901' - document_type: cpf - email: customer@example.com - first_name: John - last_name: Doe - dob: '1990-01-15' - phone_country_code: '+55' - phone_area_code: '11' - phone_number: '999999999' - country_code: br + client_id: app_abc123 + client_secret: sk_live_xyz789 + grant_type: client_credentials responses: '200': - description: Customer created + description: Token granted content: application/json: schema: - allOf: - - $ref: '#/components/schemas/ApiResponse' - - type: object - properties: - data: - $ref: '#/components/schemas/CustomerResponse' - '400': - description: Validation error or customer already exists + $ref: '#/components/schemas/MerchantTokenResponse' + example: + access_token: eyJhbGciOiJIUzI1NiIs... + token_type: Bearer + expires_in: 3600000 + '404': + description: Merchant not found or invalid credentials content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' '422': $ref: '#/components/responses/ValidationError' - get: - operationId: listCustomers - summary: List all customers - description: | - Retrieve a paginated list of customers for the authenticated merchant. - Query parameters for filtering are case-insensitive (uppercased internally). + /api/v1/merchant/token/refresh: + post: + operationId: refreshMerchantToken + summary: Refresh merchant token + description: Generate new access and refresh tokens using a valid refresh token. tags: - - Customers - security: - - bearerAuth: [] - parameters: - - $ref: '#/components/parameters/LimitQuery' - - $ref: '#/components/parameters/OffsetQuery' - - name: target_role - in: query - description: Comma-separated roles to filter by (case-insensitive) - schema: - type: string - - name: provider_registration_status - in: query - description: Comma-separated provider registration statuses (case-insensitive) - schema: - type: string - - name: provider - in: query - description: Comma-separated provider names (case-insensitive) - schema: - type: string - - name: email - in: query - description: Comma-separated email addresses to filter by - schema: - type: string - - name: document_type - in: query - description: Comma-separated document types (case-insensitive) - schema: - type: string - - name: country_code - in: query - description: Comma-separated country codes (case-insensitive) - schema: - type: string - - name: strict - in: query - description: Enforce limit restrictions - schema: - type: boolean + - Merchants + security: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - refresh_token + properties: + refresh_token: + type: string + description: Refresh token from previous grant or refresh responses: '200': - description: Customer list - headers: - X-Limit-Requested: - description: Requested limit value - schema: - type: integer - X-Limit-Applied: - description: Applied limit value - schema: - type: integer - X-Offset-Requested: - description: Requested offset value - schema: - type: integer - X-Offset-Applied: - description: Applied offset value - schema: - type: integer + description: Tokens refreshed content: application/json: schema: @@ -1450,67 +1619,40 @@ paths: data: type: object properties: - count: - type: integer - description: Total customer count - customer_list: - type: array - items: - $ref: '#/components/schemas/CustomerResponse' - /api/v1/customers/{id}: - get: - operationId: getCustomer - summary: Get customer details - description: Retrieve details for a specific customer. - tags: - - Customers - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid - responses: - '200': - description: Customer details + accessToken: + type: string + refreshToken: + type: string + '400': + description: Token expired or invalid content: application/json: schema: - allOf: - - $ref: '#/components/schemas/ApiResponse' - - type: object - properties: - data: - $ref: '#/components/schemas/CustomerResponse' + $ref: '#/components/schemas/ApiErrorResponse' '404': - $ref: '#/components/responses/NotFoundError' - put: - operationId: updateCustomer - summary: Update a customer - description: Update customer details. All fields are optional. + description: Merchant not found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + /api/v1/merchant/util/transfer-date: + post: + operationId: calculateTransferDate + summary: Calculate transfer date + description: Calculate the next available transfer date based on settlement date, region, and holiday configuration. tags: - - Customers + - Merchants security: - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/CreateCustomerRequest' + $ref: '#/components/schemas/TransferDateRequest' responses: '200': - description: Customer updated + description: Transfer date calculated content: application/json: schema: @@ -1519,98 +1661,171 @@ paths: - type: object properties: data: - $ref: '#/components/schemas/CustomerResponse' + type: object + properties: + transferDate: + type: string + format: date-time '400': description: Validation error content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' - '404': - $ref: '#/components/responses/NotFoundError' - /api/v1/customers/{id}/sync: - post: - operationId: syncCustomer - summary: Sync customer data - description: Trigger a sync of specified fields with an external provider. + /api/v1/merchant/webhooks: + get: + operationId: listWebhooks + summary: List webhooks + description: | + List registered webhooks for the authenticated merchant. + Cursor-paginated (§2.7); items key `webhook_list`. `count` is never + included (this endpoint has no non-time filters). Offset/page + parameters are rejected. tags: - - Customers + - Webhooks security: - bearerAuth: [] parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid + - $ref: '#/components/parameters/LimitQuery' + - $ref: '#/components/parameters/StartingAfterQuery' + - $ref: '#/components/parameters/EndingBeforeQuery' + - $ref: '#/components/parameters/CreatedAtGteQuery' + - $ref: '#/components/parameters/CreatedAtLteQuery' + - $ref: '#/components/parameters/UpdatedAtGteQuery' + - $ref: '#/components/parameters/UpdatedAtLteQuery' + responses: + '200': + description: Webhook list + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + type: object + required: + - has_more + - webhook_list + properties: + has_more: + type: boolean + description: | + True if more records exist beyond this page in the + requested direction. + webhook_list: + type: array + items: + $ref: '#/components/schemas/WebhookResponse' + post: + operationId: registerWebhook + summary: Register a webhook + description: Register a new webhook endpoint for the merchant. Returns a signing secret for payload verification. + tags: + - Webhooks + security: + - bearerAuth: [] requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/SyncCustomerRequest' + $ref: '#/components/schemas/RegisterWebhookRequest' example: - providers: - - stripe - fields: - - email - - phone + url: https://example.com/webhooks/crowdsplit + description: Production webhook endpoint responses: '200': - description: Sync scheduled + description: Webhook registered. Secret is only shown once. content: application/json: schema: - $ref: '#/components/schemas/ApiResponse' - example: - msg: Sync scheduled - data: null + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + allOf: + - $ref: '#/components/schemas/WebhookResponse' + - type: object + required: + - secret + properties: + secret: + type: string + description: | + Signing secret for HMAC-SHA256 verification. + Returned ONLY here — it is not retrievable + from any later read (§2.9.2). Store it now. '400': - description: Invalid provider or fields + description: Validation error content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' - '404': - $ref: '#/components/responses/NotFoundError' - /api/v1/customers/{customer_id}/balances: + /api/v1/merchant/webhooks/event-types: get: - operationId: getCustomerBalances - summary: Get customer balances - description: Retrieve available and pending balances for a customer across providers. + operationId: listWebhookEventTypes + summary: List webhook event types + description: | + Reference catalog of every webhook event CrowdSplit can deliver, + grouped by category. Use the returned event values in a webhook's + `event_types` subscription list; an endpoint registered without + `event_types` receives every event. tags: - - Customers + - Webhooks + security: + - bearerAuth: [] + responses: + '200': + description: Webhook event catalog + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + type: array + items: + type: object + properties: + category: + type: string + description: Event group (e.g. payment, payout) + events: + type: array + items: + type: string + description: Wire event values in this category + example: + msg: Webhook Event Types + data: + - category: payment + events: + - payment.created + - payment.refund.created + /api/v1/merchant/webhooks/{id}: + get: + operationId: getWebhook + summary: Get webhook details + description: Get details of a specific webhook by ID. + tags: + - Webhooks security: - bearerAuth: [] parameters: - - name: customer_id + - name: id in: path required: true schema: type: string format: uuid - - name: provider - in: query - description: Comma-separated provider names (case-insensitive) - schema: - type: string - - name: role - in: query - description: Comma-separated roles (case-insensitive) - schema: - type: string responses: '200': - description: Balance information + description: Webhook details content: application/json: schema: @@ -1619,30 +1834,19 @@ paths: - type: object properties: data: - $ref: '#/components/schemas/BalanceResponse' - '400': - description: Invalid customer ID - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' + $ref: '#/components/schemas/WebhookResponse' '404': $ref: '#/components/responses/NotFoundError' - /api/v1/customers/{customer_id}/files: - post: - operationId: uploadCustomerFiles - summary: Upload customer files - description: Upload identity or address verification documents for a customer. + put: + operationId: updateWebhook + summary: Update a webhook + description: Update the URL or description of an existing webhook. tags: - - Customers + - Webhooks security: - bearerAuth: [] parameters: - - name: customer_id + - name: id in: path required: true schema: @@ -1651,55 +1855,74 @@ paths: requestBody: required: true content: - multipart/form-data: + application/json: schema: - type: object - properties: - file: - type: string - format: binary - description: Document file (max 5MB) - file_type: - type: string - description: Document type - enum: - - SSN_BACK - - SSN_FRONT - - CPF_DOCUMENT - - PASSPORT - - ADDRESS_PROOF + $ref: '#/components/schemas/UpdateWebhookRequest' responses: - '201': - description: File uploaded successfully + '200': + description: Webhook updated content: application/json: schema: - $ref: '#/components/schemas/ApiResponse' - example: - msg: file upload successful - data: null - '400': - description: Invalid file or missing data + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/WebhookResponse' + '404': + $ref: '#/components/responses/NotFoundError' + delete: + operationId: deleteWebhook + summary: Delete a webhook + description: | + Remove a registered webhook URL. Deliveries to the deleted URL stop + immediately (§2.9.9): pending and retrying deliveries are cancelled, + and replays never target the deleted endpoint. Cancelled deliveries + remain visible on their notifications' delivery history for audit. + Merchants migrating endpoints should register the new webhook FIRST, + run both in parallel, and delete the old one only after cutover. + tags: + - Webhooks + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Webhook deleted content: application/json: schema: - $ref: '#/components/schemas/ApiErrorResponse' + $ref: '#/components/schemas/ApiResponse' '401': $ref: '#/components/responses/UnauthorizedError' '403': $ref: '#/components/responses/ForbiddenError' '404': $ref: '#/components/responses/NotFoundError' - get: - operationId: getCustomerFiles - summary: Get customer files - description: Retrieve uploaded files for a customer with presigned download URLs. + /api/v1/merchant/webhooks/{id}/toggle: + patch: + operationId: toggleWebhookStatus + summary: Toggle webhook active status + description: | + Flip a registered webhook between active and inactive states + without deleting it. Useful for planned merchant-side maintenance + windows or temporarily silencing a noisy endpoint. Inactive + webhooks receive no new dispatches; deliveries already queued + before the toggle may still fire once before the state takes + effect. tags: - - Customers + - Webhooks security: - bearerAuth: [] parameters: - - name: customer_id + - name: id in: path required: true schema: @@ -1707,7 +1930,7 @@ paths: format: uuid responses: '200': - description: File list + description: Webhook status toggled content: application/json: schema: @@ -1716,115 +1939,70 @@ paths: - type: object properties: data: - type: array - items: - type: object - properties: - fileType: - type: string - url: - type: string - description: Presigned S3 URL (temporary) + $ref: '#/components/schemas/WebhookResponse' '404': $ref: '#/components/responses/NotFoundError' - /api/v1/customers/{customer_id}/payment_methods: - post: - operationId: createPaymentMethod - summary: Add a payment method - description: | - Create a payment method for a customer. Fields type, provider, currency, - chain, and bank_account_type are case-insensitive. + /api/v1/merchant/webhooks/notifications: + get: + operationId: listWebhookNotifications + summary: List webhook notifications + description: List all webhook notification events for the merchant with pagination. tags: - - Payment Methods + - Webhooks security: - bearerAuth: [] parameters: - - name: customer_id - in: path - required: true + - $ref: '#/components/parameters/LimitQuery' + - $ref: '#/components/parameters/StartingAfterQuery' + - $ref: '#/components/parameters/EndingBeforeQuery' + - $ref: '#/components/parameters/CreatedAtGteQuery' + - $ref: '#/components/parameters/CreatedAtLteQuery' + - $ref: '#/components/parameters/UpdatedAtGteQuery' + - $ref: '#/components/parameters/UpdatedAtLteQuery' + - name: reference_id + in: query + description: Filter to notifications about one resource id schema: type: string - format: uuid - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreatePaymentMethodRequest' - examples: - bankAccount: - summary: Bank account payment method - value: - type: bank - provider: crowd_split - bank_details: - account_number: '12345678' - account_name: John Doe - bank_name: Banco do Brasil - branch_code: '0001' - account_type: checking - ispb: '00000000' - responses: - '200': - description: Payment method created - content: - application/json: - schema: - type: object - properties: - msg: - type: string - data: - $ref: '#/components/schemas/PaymentMethodResponse' - provider_message: - type: string - '400': - description: Validation error - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' - '404': - $ref: '#/components/responses/NotFoundError' - '422': - $ref: '#/components/responses/ValidationError' - get: - operationId: listPaymentMethods - summary: List payment methods - description: List all payment methods for a customer. - tags: - - Payment Methods - security: - - bearerAuth: [] - parameters: - - name: customer_id - in: path - required: true + - name: category + in: query + description: | + Filter by webhook category (e.g. `payment_lifecycle`). Unknown + values are rejected. `reference_module` is the legacy alias. schema: type: string - format: uuid - - name: type + - name: reference_module in: query - description: Filter by payment method type (case-insensitive) + description: Legacy alias of `category`. + schema: + type: string + - name: event + in: query + description: | + Filter by exact wire event value (e.g. `payment.succeeded`). + Unknown values are rejected — see GET /merchant/webhooks/event-types. schema: type: string - name: status in: query - description: Filter by status (case-insensitive) + description: | + Filter by aggregate delivery status. `dead_lettered` is the §2.9.6 + Path A recovery listing (replay via the replay endpoint). schema: type: string - - name: provider + enum: + - pending + - delivered + - dead_lettered + - name: webhook_id in: query - description: Filter by provider (case-insensitive) + description: Filter to notifications addressed to one webhook registration. schema: type: string + format: uuid responses: '200': - description: Payment method list + description: Notification list content: application/json: schema: @@ -1833,34 +2011,46 @@ paths: - type: object properties: data: - type: array - items: - $ref: '#/components/schemas/PaymentMethodResponse' - /api/v1/customers/{customer_id}/payment_methods/{id}: + type: object + required: + - has_more + - notification_list + properties: + has_more: + type: boolean + description: | + True if more records exist beyond this page in the + requested direction. + count: + type: integer + description: | + Total records matching the active filters. Present + only when at least one non-time filter is supplied; + omitted on broad time-windowed scans. + notification_list: + type: array + items: + $ref: '#/components/schemas/WebhookNotification' + /api/v1/merchant/webhooks/notifications/{id}: get: - operationId: getPaymentMethod - summary: Get payment method details - description: Get details of a specific payment method. + operationId: getWebhookNotification + summary: Get webhook notification details + description: Get details of a specific webhook notification event. tags: - - Payment Methods + - Webhooks security: - bearerAuth: [] parameters: - - name: customer_id - in: path - required: true - schema: - type: string - format: uuid - name: id in: path required: true + description: Notification id — `evt_` or bare `` (dual-accepted). schema: type: string - format: uuid + pattern: ^(evt_)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ responses: '200': - description: Payment method details + description: Notification details content: application/json: schema: @@ -1869,44 +2059,35 @@ paths: - type: object properties: data: - $ref: '#/components/schemas/PaymentMethodResponse' + $ref: '#/components/schemas/WebhookNotification' '404': $ref: '#/components/responses/NotFoundError' - put: - operationId: updatePaymentMethod - summary: Update a payment method + /api/v1/merchant/webhooks/notifications/{id}/replay: + post: + operationId: replayWebhookNotification + summary: Replay a webhook notification description: | - Replace mutable fields on a saved payment method — typically - customer-visible label, default-flag, or billing-address metadata. - Provider-issued identifiers such as `card_token` and on-chain - addresses are immutable; delete the payment method and re-create - it to change those. + Queue a fresh delivery of a stored notification to its recorded + receivers (§2.9.9). Async: the 202 reports acceptance; delivery + outcomes surface in the notification's `delivery_attempts[]` with + `is_replay: true`. The envelope `id` is unchanged (merchants dedupe + on it); a fresh `CrowdSplit-Timestamp` + `CrowdSplit-Signature` is + computed at dispatch. Does not re-enter the automatic retry schedule. tags: - - Payment Methods + - Webhooks security: - bearerAuth: [] parameters: - - name: customer_id - in: path - required: true - schema: - type: string - format: uuid - name: id in: path required: true + description: Notification id — `evt_` or bare `` (dual-accepted). schema: type: string - format: uuid - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreatePaymentMethodRequest' + pattern: ^(evt_)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ responses: - '200': - description: Payment method updated + '202': + description: Notification replay queued content: application/json: schema: @@ -1915,200 +2096,127 @@ paths: - type: object properties: data: - $ref: '#/components/schemas/PaymentMethodResponse' - '400': - description: Validation error + type: object + required: + - notification_id + - replay_attempt_at + properties: + notification_id: + type: string + pattern: ^(evt_)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ + description: Envelope id (`evt_`); dual-accepted on input. + replay_attempt_at: + type: string + format: date-time + description: When the replay was accepted/queued. + '404': + $ref: '#/components/responses/NotFoundError' + '422': + description: The notification has no delivery targets to replay to content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' - '404': - $ref: '#/components/responses/NotFoundError' - delete: - operationId: deletePaymentMethod - summary: Delete a payment method - description: Delete a payment method from a customer. + /api/v1/merchant/{id}: + parameters: + - name: id + in: path + required: true + description: Public merchant UID + schema: + type: string + format: uuid + get: + operationId: getMerchant + summary: Get a merchant + description: Retrieve a single merchant the authenticated user is a member of. tags: - - Payment Methods + - Merchants security: - - bearerAuth: [] - parameters: - - name: customer_id - in: path - required: true - schema: - type: string - format: uuid - - name: id - in: path - required: true - schema: - type: string - format: uuid + - accessTokenAuth: [] responses: '200': - description: Payment method deleted + description: Merchant retrieved content: application/json: schema: - $ref: '#/components/schemas/ApiResponse' + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/MerchantResponse' '401': $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' '404': $ref: '#/components/responses/NotFoundError' - /api/v1/customers/{customer_id}/platforms: - post: - operationId: populateKycData - summary: Submit platform registration data - description: Submit KYC/provider registration data for a customer on a specific provider platform. + patch: + operationId: updateMerchant + summary: Update merchant settings + description: | + Update business details on a merchant. Requires the `merchant:settings` + permission. tags: - - Provider Registration + - Merchants security: - - bearerAuth: [] - parameters: - - name: customer_id - in: path - required: true - schema: - type: string - format: uuid + - accessTokenAuth: [] requestBody: required: true content: application/json: schema: type: object - required: - - provider - - target_role properties: - provider: + legal_name: type: string - description: Provider name (case-insensitive) - target_role: + address: + type: string + website: + type: string + tax_number: type: string - description: Target role for the provider registration - additionalProperties: true responses: '200': - description: Platform data submitted - content: - application/json: - schema: - type: object - properties: - msg: - type: string - data: - type: array - description: | - Registration status for this customer across every - provider they are registered on, one entry per - `(provider, target_role)` pair. Same shape as - `GET /api/v1/provider-registration/{customer_id}/status`. - items: - type: object - properties: - provider: - type: string - description: Provider name (lowercased). - target_role: - type: string - description: Target role on this provider (lowercased). - status: - type: string - description: Current registration status (lowercased). - readiness: - type: object - nullable: true - description: Provider-specific readiness flags (JSONB pass-through). - additionalProperties: true - rejection_reason: - type: string - nullable: true - description: Human-readable rejection reason when rejection-like. - additionalProperties: true - '400': - description: Validation error + description: Merchant updated content: application/json: schema: - $ref: '#/components/schemas/ApiErrorResponse' + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/MerchantResponse' '401': $ref: '#/components/responses/UnauthorizedError' '403': $ref: '#/components/responses/ForbiddenError' '404': $ref: '#/components/responses/NotFoundError' - '422': - $ref: '#/components/responses/ValidationError' - /api/v1/transactions: - get: - operationId: listTransactions - summary: List transactions + /api/v1/merchant/{id}/audit-log: + parameters: + - name: id + in: path + required: true + description: Public merchant UID + schema: + type: string + format: uuid + - $ref: '#/components/parameters/LimitQuery' + - $ref: '#/components/parameters/OffsetQuery' + get: + operationId: listMerchantAuditLog + summary: List merchant audit-log entries description: | - Retrieve a paginated list of transactions for the merchant. - Query filter values are case-insensitive (uppercased internally). - Response fields status, type, provider, currency, payment_method are returned lowercased. + Paginated audit trail of mutations performed within the merchant. + Requires the `merchant:settings` permission. tags: - - Transactions + - Merchants security: - - bearerAuth: [] - parameters: - - $ref: '#/components/parameters/LimitQuery' - - $ref: '#/components/parameters/OffsetQuery' - - name: type_list - in: query - description: Comma-separated transaction types (case-insensitive) - schema: - type: string - - name: status - in: query - description: Filter by status (case-insensitive) - schema: - type: string - - name: payment_method - in: query - description: Filter by payment method type (case-insensitive) - schema: - type: string - - name: source_currency - in: query - description: Filter by source currency (case-insensitive) - schema: - type: string - - name: destination_currency - in: query - description: Filter by destination currency (case-insensitive) - schema: - type: string - - name: provider - in: query - description: Filter by provider (case-insensitive) - schema: - type: string + - accessTokenAuth: [] responses: '200': - description: Transaction list - headers: - X-Limit-Requested: - schema: - type: integer - X-Limit-Applied: - schema: - type: integer - X-Offset-Requested: - schema: - type: integer - X-Offset-Applied: - schema: - type: integer + description: Audit-log page content: application/json: schema: @@ -2119,31 +2227,65 @@ paths: data: type: object properties: - count: - type: integer - transaction_list: + items: type: array items: - $ref: '#/components/schemas/TransactionResponse' - /api/v1/transactions/{id}: + type: object + properties: + action: + type: string + actorUserId: + type: string + nullable: true + targetType: + type: string + nullable: true + targetId: + type: string + nullable: true + metadata: + type: object + additionalProperties: true + nullable: true + ipAddress: + type: string + nullable: true + userAgent: + type: string + nullable: true + createdAt: + type: string + format: date-time + total: + type: integer + limit: + type: integer + offset: + type: integer + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + /api/v1/merchant/{id}/api-keys: + parameters: + - name: id + in: path + required: true + description: Public merchant UID + schema: + type: string + format: uuid get: - operationId: getTransaction - summary: Get transaction details - description: Retrieve details of a single transaction. + operationId: listMerchantApiKeys + summary: List merchant API keys + description: Return API-key metadata (no secrets) for the merchant. tags: - - Transactions + - Merchants security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid + - accessTokenAuth: [] responses: '200': - description: Transaction details + description: API keys retrieved content: application/json: schema: @@ -2152,31 +2294,25 @@ paths: - type: object properties: data: - $ref: '#/components/schemas/TransactionResponse' + type: array + items: + $ref: '#/components/schemas/MerchantApiKey' + '401': + $ref: '#/components/responses/UnauthorizedError' '404': $ref: '#/components/responses/NotFoundError' - /api/v1/transactions/{id}/settle: - patch: - operationId: settleTransaction - summary: Settle a transaction - description: Mark a transaction as settled. Requires app-key authentication. + post: + operationId: createMerchantApiKey + summary: Create a merchant API key + description: | + Mint a new API key pair. Allowed only when the merchant is `approved` + and both STRIPE and BRIDGE provider configs are saved; otherwise 403. + The secret key is returned exactly once in this response. + Requires the `api_key:manage` permission. tags: - - Transactions - security: [] - parameters: - - name: id - in: path - required: true - description: Transaction UID - schema: - type: string - format: uuid - - name: app-key - in: header - required: true - description: Application key for settlement authorization - schema: - type: string + - Merchants + security: + - accessTokenAuth: [] requestBody: required: true content: @@ -2184,27 +2320,14 @@ paths: schema: type: object required: - - data + - label properties: - data: - type: object - required: - - charge_id - - amount - - status - properties: - charge_id: - type: string - description: Provider charge identifier - amount: - type: integer - description: Settlement amount in smallest currency unit - status: - type: string - example: SETTLED + label: + type: string + description: Human-readable label for the key responses: - '200': - description: Transaction settled + '201': + description: API key created content: application/json: schema: @@ -2213,26 +2336,72 @@ paths: - type: object properties: data: - $ref: '#/components/schemas/TransactionResponse' - '400': - description: Settlement failed or invalid request + allOf: + - $ref: '#/components/schemas/MerchantApiKey' + - type: object + properties: + secretKey: + type: string + description: Secret key — shown only once + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + description: Merchant not approved, or provider configs incomplete content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponse' '404': $ref: '#/components/responses/NotFoundError' - '422': - $ref: '#/components/responses/ValidationError' - /api/v1/outbound_payments: - post: - operationId: createPayout - summary: Create an outbound payment - description: Initiate a payout/outbound payment to a customer's payment method. + /api/v1/merchant/{id}/api-keys/{key_id}: + parameters: + - name: id + in: path + required: true + description: Public merchant UID + schema: + type: string + format: uuid + - name: key_id + in: path + required: true + description: Public API-key UID + schema: + type: string + format: uuid + delete: + operationId: revokeMerchantApiKey + summary: Revoke a merchant API key + description: | + Permanently revoke a specific API key. Requires the `api_key:manage` + permission. tags: - - Payouts + - Merchants security: - - bearerAuth: [] + - accessTokenAuth: [] + responses: + '200': + description: API key revoked + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + /api/v1/merchant/team-invitations/accept: + post: + operationId: acceptTeamInvitation + summary: Accept a team invitation + description: | + Public endpoint. Redeem a signed invitation token to create the invited + user account, assign the invited role, and join the merchant team. + tags: + - Merchants + security: [] requestBody: required: true content: @@ -2240,41 +2409,18 @@ paths: schema: type: object required: - - payment_method_id - - amount - - currency - - customer_id + - token + - password properties: - payment_method_id: - type: string - format: uuid - description: Target payment method ID - amount: - type: integer - minimum: 1 - description: Amount in smallest currency unit - currency: + token: type: string - enum: - - USD - - BRL - description: Currency code - customer_id: + description: Signed invitation token from the invite email/link + password: type: string - format: uuid - description: Customer ID - metadata: - type: object - description: Custom metadata - additionalProperties: true - example: - payment_method_id: 660e8400-e29b-41d4-a716-446655440000 - amount: 50000 - currency: BRL - customer_id: 550e8400-e29b-41d4-a716-446655440000 + description: Password to set on the newly created account responses: '200': - description: Payout initiated + description: Invitation accepted content: application/json: schema: @@ -2283,99 +2429,104 @@ paths: - type: object properties: data: - $ref: '#/components/schemas/TransactionResponse' + type: object + properties: + email: + type: string + format: email + role: + type: string '400': - description: Invalid request or payment method not found + description: Invalid or expired invitation token content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' - '404': - description: Customer or payment method not found + '409': + description: An account already exists for this email content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponse' '422': $ref: '#/components/responses/ValidationError' - /api/v1/transfer: - post: - operationId: createTransfer - summary: Create a transfer - description: | - Initiate a transfer between accounts. Supports single-provider and inter-platform transfers. - Fields provider, currency, payment_method.type are case-insensitive. + /api/v1/merchant/{id}/members: + parameters: + - name: id + in: path + required: true + description: Public merchant UID + schema: + type: string + format: uuid + get: + operationId: listMerchantMembers + summary: List merchant team members + description: Return active members of the merchant team with their roles. tags: - - Transfers + - Merchants security: - - bearerAuth: [] - requestBody: + - accessTokenAuth: [] + responses: + '200': + description: Members retrieved + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + type: array + items: + type: object + properties: + userId: + type: string + nullable: true + email: + type: string + format: email + nullable: true + roles: + type: array + items: + type: string + joinedAt: + type: string + format: date-time + '401': + $ref: '#/components/responses/UnauthorizedError' + /api/v1/merchant/{id}/members/{memberId}: + parameters: + - name: id + in: path required: true - content: - application/json: - schema: - type: object - required: - - source - - destination - properties: - provider: - type: string - description: Provider for single-provider transfers (case-insensitive) - source: - type: object - description: Source account details - properties: - provider: - type: string - description: Source provider (for inter-platform transfers) - currency: - type: string - customer: - type: object - properties: - id: - type: string - format: uuid - amount: - type: integer - minimum: 1 - destination: - type: object - description: Destination account details - properties: - provider: - type: string - description: Destination provider (for inter-platform transfers) - customer: - type: object - properties: - id: - type: string - format: uuid - payment_method: - type: object - properties: - type: - type: string - id: - type: string - format: uuid - currency: - type: string - chain: - type: string - metadata: - type: object - description: Custom metadata - additionalProperties: true + description: Public merchant UID + schema: + type: string + format: uuid + - name: memberId + in: path + required: true + description: Public user UID of the member to remove + schema: + type: string + format: uuid + delete: + operationId: removeMerchantMember + summary: Remove a team member + description: | + Deactivate a member's merchant membership. Cannot remove the last + Platform Admin (422). Requires the `team:invite` permission. + tags: + - Merchants + security: + - accessTokenAuth: [] responses: '200': - description: Transfer initiated + description: Member removed content: application/json: schema: @@ -2384,13 +2535,10 @@ paths: - type: object properties: data: - $ref: '#/components/schemas/TransactionResponse' - '400': - description: Invalid request or platform error - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorResponse' + type: object + properties: + userId: + type: string '401': $ref: '#/components/responses/UnauthorizedError' '403': @@ -2398,16 +2546,66 @@ paths: '404': $ref: '#/components/responses/NotFoundError' '422': - $ref: '#/components/responses/ValidationError' - /api/v1/transfer/webhook: + description: Cannot remove the last Platform Admin + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + /api/v1/merchant/{id}/invitations: + parameters: + - name: id + in: path + required: true + description: Public merchant UID + schema: + type: string + format: uuid + get: + operationId: listMerchantInvitations + summary: List team invitations + description: List invitations for the merchant, optionally filtered by status. + tags: + - Merchants + security: + - accessTokenAuth: [] + parameters: + - name: status + in: query + required: false + description: Filter by invitation status + schema: + type: string + enum: + - PENDING + - ACCEPTED + - CANCELLED + responses: + '200': + description: Invitations retrieved + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/MerchantInvitation' + '401': + $ref: '#/components/responses/UnauthorizedError' post: - operationId: sendTransferWebhook - summary: Send transfer webhook (dev) - description: Manually trigger a webhook for a transfer. Intended for development/testing. + operationId: createMerchantInvitation + summary: Invite a team member + description: | + Invite a new member by email. Role defaults to Platform User when + omitted. Returns 409 (generic) if the email already has an account or a + pending invitation. Requires the `team:invite` permission. tags: - - Transfers + - Merchants security: - - bearerAuth: [] + - accessTokenAuth: [] requestBody: required: true content: @@ -2415,19 +2613,70 @@ paths: schema: type: object required: - - transfer_id - - status + - email properties: - transfer_id: + email: type: string - format: uuid - description: Transfer transaction UUID - status: + format: email + role: type: string - description: Status to set + description: Role to grant on acceptance (defaults to Platform User) + responses: + '201': + description: Invitation created + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + allOf: + - $ref: '#/components/schemas/MerchantInvitation' + - type: object + properties: + token: + type: string + description: Invitation token — only returned in DEV/TEST + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '409': + description: Email already has an account or a pending invitation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + /api/v1/merchant/{id}/invitations/{invitationId}: + parameters: + - name: id + in: path + required: true + description: Public merchant UID + schema: + type: string + format: uuid + - name: invitationId + in: path + required: true + description: Public invitation UID + schema: + type: string + format: uuid + delete: + operationId: cancelMerchantInvitation + summary: Cancel a team invitation + description: | + Cancel a pending invitation. Requires the `team:invite` permission. + tags: + - Merchants + security: + - accessTokenAuth: [] responses: '200': - description: Webhook sent + description: Invitation cancelled content: application/json: schema: @@ -2435,31 +2684,144 @@ paths: - $ref: '#/components/schemas/ApiResponse' - type: object properties: - msg: - example: webhook sent successfully data: - type: 'null' - '400': - description: Error sending webhook + $ref: '#/components/schemas/MerchantInvitation' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + '409': + description: Invitation is not in a cancellable (pending) state content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponse' + /api/v1/merchant/{id}/activation: + parameters: + - name: id + in: path + required: true + description: Public merchant UID + schema: + type: string + format: uuid + get: + operationId: getMerchantActivation + summary: Get activation status and history + description: Return the merchant's current activation status and review history. + tags: + - Merchants + security: + - accessTokenAuth: [] + responses: + '200': + description: Activation status retrieved + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + type: object + properties: + activationStatus: + type: string + history: + type: array + items: + type: object + additionalProperties: true + '401': + $ref: '#/components/responses/UnauthorizedError' + post: + operationId: submitMerchantActivation + summary: Submit (or resubmit) for activation review + description: | + Resubmit the merchant for CS review after a rejection or change request. + Requires the `activation:submit` permission. + tags: + - Merchants + security: + - accessTokenAuth: [] + responses: + '200': + description: Activation submitted + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + type: object + properties: + activationStatus: + type: string + submittedAt: + type: string + format: date-time '401': $ref: '#/components/responses/UnauthorizedError' '403': $ref: '#/components/responses/ForbiddenError' - /api/v1/buy: + '409': + description: Cannot submit from the current activation state + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + /api/v1/merchant/{id}/platform-configs: + parameters: + - name: id + in: path + required: true + description: Public merchant UID + schema: + type: string + format: uuid + get: + operationId: listMerchantPlatformConfigs + summary: List provider configs + description: List the merchant's saved provider (STRIPE/BRIDGE) configurations. + tags: + - Merchants + security: + - accessTokenAuth: [] + responses: + '200': + description: Provider configs retrieved + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/MerchantProviderConfig' + '401': + $ref: '#/components/responses/UnauthorizedError' post: - operationId: createBuy - summary: Create a buy transaction + operationId: createMerchantPlatformConfig + summary: Save a provider config description: | - Initiate a cryptocurrency/asset buy transaction. - Fields provider, currency, payment_method.type are case-insensitive. + Save provider credentials for STRIPE or BRIDGE. Allowed only after the + merchant is approved. The backend registers the provider's webhook + endpoint(s) using the supplied credentials, captures the signing + secret(s), stores them encrypted, and returns them once in this + response. The request does not include any webhook secret. Requires the + `provider_config:write` permission. tags: - - Buy / Sell + - Merchants security: - - bearerAuth: [] + - accessTokenAuth: [] requestBody: required: true content: @@ -2467,53 +2829,1848 @@ paths: schema: type: object required: - - provider - - source + - platform + - config properties: - provider: + platform: type: string - description: Provider name (case-insensitive) - source: - type: object - properties: - amount: - type: integer - minimum: 1 - currency: - type: string - customer: - type: object - properties: - id: - type: string - format: uuid - payment_method: - type: object - properties: - type: - type: string - destination: + enum: + - STRIPE + - BRIDGE + config: type: object - properties: - currency: - type: string - payment_method: - type: object - properties: - type: - type: string - chain: - type: string - metadata: + description: Provider credentials (shape varies by platform) + additionalProperties: true + responses: + '200': + description: Provider config saved + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/MerchantProviderConfigSaveResult' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + description: Merchant not approved, or missing permission + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '404': + $ref: '#/components/responses/NotFoundError' + /api/v1/merchant/{id}/platform-configs/{platform}: + parameters: + - name: id + in: path + required: true + description: Public merchant UID + schema: + type: string + format: uuid + - name: platform + in: path + required: true + description: Provider identifier + schema: + type: string + enum: + - STRIPE + - BRIDGE + get: + operationId: getMerchantPlatformConfig + summary: Get a provider config + description: | + Retrieve a single provider config. Provider credentials are redacted. + tags: + - Merchants + security: + - accessTokenAuth: [] + responses: + '200': + description: Provider config retrieved + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/MerchantProviderConfig' + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' + patch: + operationId: updateMerchantPlatformConfig + summary: Update a provider config + description: | + Update provider credentials. Re-registers webhooks and returns the new + signing secret(s) once. Requires the `provider_config:write` permission. + tags: + - Merchants + security: + - accessTokenAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - config + properties: + config: type: object - description: | - Merchant-supplied arbitrary metadata, stored with the - transaction and echoed back on derived webhook deliveries. - Shape is defined by the merchant at request time. additionalProperties: true responses: - '200': - description: Buy transaction initiated + '200': + description: Provider config updated + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/MerchantProviderConfigSaveResult' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + delete: + operationId: deleteMerchantPlatformConfig + summary: Delete a provider config + description: | + Remove a provider config. Requires the `provider_config:write` permission. + tags: + - Merchants + security: + - accessTokenAuth: [] + responses: + '200': + description: Provider config deleted + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + /api/v1/merchant/{id}/platform-configs/{platform}/test: + parameters: + - name: id + in: path + required: true + description: Public merchant UID + schema: + type: string + format: uuid + - name: platform + in: path + required: true + description: Provider identifier + schema: + type: string + enum: + - STRIPE + - BRIDGE + post: + operationId: testMerchantPlatformConfig + summary: Test a provider config + description: | + Validate the saved provider config against the provider's API. Requires + the `provider_config:write` permission. + tags: + - Merchants + security: + - accessTokenAuth: [] + responses: + '200': + description: Provider config is valid + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '400': + description: Provider validation failed + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' + /api/v1/customers: + post: + operationId: createCustomer + summary: Create a customer + description: | + Register a new customer. Fields document_type, country_code, and gender + are case-insensitive (uppercased internally, returned lowercased). + tags: + - Customers + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateCustomerRequest' + example: + document_number: '12345678901' + document_type: cpf + email: customer@example.com + first_name: John + last_name: Doe + dob: '1990-01-15' + phone_country_code: '+55' + phone_area_code: '11' + phone_number: '999999999' + country_code: br + responses: + '200': + description: Customer created + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/CustomerResponse' + '400': + description: Validation error or customer already exists + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '422': + $ref: '#/components/responses/ValidationError' + get: + operationId: listCustomers + summary: List all customers + description: | + Retrieve a paginated list of customers for the authenticated merchant. + Query parameters for filtering are case-insensitive (uppercased internally). + tags: + - Customers + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/LimitQuery' + - $ref: '#/components/parameters/StartingAfterQuery' + - $ref: '#/components/parameters/EndingBeforeQuery' + - $ref: '#/components/parameters/CreatedAtGteQuery' + - $ref: '#/components/parameters/CreatedAtLteQuery' + - $ref: '#/components/parameters/UpdatedAtGteQuery' + - $ref: '#/components/parameters/UpdatedAtLteQuery' + - name: target_role + in: query + description: Comma-separated roles to filter by (case-insensitive) + schema: + type: string + - name: provider_registration_status + in: query + description: Comma-separated provider registration statuses (case-insensitive) + schema: + type: string + - name: provider + in: query + description: Comma-separated provider names (case-insensitive) + schema: + type: string + - name: email + in: query + description: Comma-separated email addresses to filter by + schema: + type: string + - name: document_type + in: query + description: Comma-separated document types (case-insensitive) + schema: + type: string + - name: country_code + in: query + description: Comma-separated country codes (case-insensitive) + schema: + type: string + responses: + '200': + description: Customer list + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + type: object + required: + - has_more + - customer_list + properties: + has_more: + type: boolean + description: | + True if more records exist beyond this page in the + requested direction. + count: + type: integer + description: | + Total records matching the active filters. Present + only when at least one non-time filter is supplied; + omitted on broad time-windowed scans. + customer_list: + type: array + items: + $ref: '#/components/schemas/CustomerResponse' + /api/v1/customers/{id}: + get: + operationId: getCustomer + summary: Get customer details + description: Retrieve details for a specific customer. + tags: + - Customers + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + description: Customer ID — `cus_` or bare UUID (both accepted) + schema: + $ref: '#/components/schemas/CustomerId' + responses: + '200': + description: Customer details + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/CustomerResponse' + '404': + $ref: '#/components/responses/NotFoundError' + put: + operationId: updateCustomer + summary: Update a customer + description: Update customer details. All fields are optional. + tags: + - Customers + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + description: Customer ID — `cus_` or bare UUID (both accepted) + schema: + $ref: '#/components/schemas/CustomerId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateCustomerRequest' + responses: + '200': + description: Customer updated + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/CustomerResponse' + '400': + description: Validation error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + /api/v1/customers/{id}/sync: + post: + operationId: syncCustomer + summary: Sync customer data + description: Trigger a sync of specified fields with an external provider. + tags: + - Customers + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + description: Customer ID — `cus_` or bare UUID (both accepted) + schema: + $ref: '#/components/schemas/CustomerId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SyncCustomerRequest' + example: + providers: + - stripe + fields: + - email + - phone + responses: + '200': + description: Sync scheduled + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + example: + msg: Sync scheduled + data: null + '400': + description: Invalid provider or fields + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + /api/v1/customers/{customer_id}/balances: + get: + operationId: getCustomerBalances + summary: Get customer balances + description: Retrieve available and pending balances for a customer across providers. + tags: + - Customers + security: + - bearerAuth: [] + parameters: + - name: customer_id + in: path + required: true + description: Customer ID — `cus_` or bare UUID (both accepted) + schema: + $ref: '#/components/schemas/CustomerId' + - name: provider + in: query + description: Comma-separated provider names (case-insensitive) + schema: + type: string + - name: role + in: query + description: Comma-separated roles (case-insensitive) + schema: + type: string + responses: + '200': + description: Balance information + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/BalanceResponse' + '400': + description: Invalid customer ID + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + /api/v1/customers/{customer_id}/files: + post: + operationId: uploadCustomerFiles + summary: Upload customer files + description: Upload identity or address verification documents for a customer. + tags: + - Customers + security: + - bearerAuth: [] + parameters: + - name: customer_id + in: path + required: true + description: Customer ID — `cus_` or bare UUID (both accepted) + schema: + $ref: '#/components/schemas/CustomerId' + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + description: Document file (max 5MB) + file_type: + type: string + description: Document type + enum: + - SSN_BACK + - SSN_FRONT + - CPF_DOCUMENT + - PASSPORT + - ADDRESS_PROOF + responses: + '201': + description: File uploaded successfully + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + example: + msg: file upload successful + data: null + '400': + description: Invalid file or missing data + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + get: + operationId: getCustomerFiles + summary: Get customer files + description: Retrieve uploaded files for a customer with presigned download URLs. + tags: + - Customers + security: + - bearerAuth: [] + parameters: + - name: customer_id + in: path + required: true + description: Customer ID — `cus_` or bare UUID (both accepted) + schema: + $ref: '#/components/schemas/CustomerId' + responses: + '200': + description: File list + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + type: array + items: + type: object + properties: + fileType: + type: string + url: + type: string + description: Presigned S3 URL (temporary) + '404': + $ref: '#/components/responses/NotFoundError' + /api/v1/customers/{customer_id}/payment_methods: + post: + operationId: createPaymentMethod + summary: Add a payment method + description: | + Create a payment method for a customer. Fields type, provider, currency, + chain, and bank_account_type are case-insensitive. + tags: + - Payment Methods + security: + - bearerAuth: [] + parameters: + - name: customer_id + in: path + required: true + description: Customer ID — `cus_` or bare UUID (both accepted) + schema: + $ref: '#/components/schemas/CustomerId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreatePaymentMethodRequest' + examples: + bankAccount: + summary: Bank account payment method + value: + type: bank + provider: crowd_split + bank_details: + account_number: '12345678' + account_name: John Doe + bank_name: Banco do Brasil + branch_code: '0001' + account_type: checking + ispb: '00000000' + responses: + '200': + description: Payment method created + content: + application/json: + schema: + type: object + properties: + msg: + type: string + data: + $ref: '#/components/schemas/PaymentMethodResponse' + provider_message: + type: string + '400': + description: Validation error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + '422': + $ref: '#/components/responses/ValidationError' + get: + operationId: listPaymentMethods + summary: List payment methods + description: List all payment methods for a customer. + tags: + - Payment Methods + security: + - bearerAuth: [] + parameters: + - name: customer_id + in: path + required: true + description: Customer ID — `cus_` or bare UUID (both accepted) + schema: + $ref: '#/components/schemas/CustomerId' + - name: type + in: query + description: Filter by payment method type (case-insensitive) + schema: + type: string + - name: status + in: query + description: Filter by status (case-insensitive) + schema: + type: string + - name: provider + in: query + description: Filter by provider (case-insensitive) + schema: + type: string + responses: + '200': + description: Payment method list + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/PaymentMethodResponse' + /api/v1/customers/{customer_id}/payment_methods/{id}: + get: + operationId: getPaymentMethod + summary: Get payment method details + description: Get details of a specific payment method. + tags: + - Payment Methods + security: + - bearerAuth: [] + parameters: + - name: customer_id + in: path + required: true + description: Customer ID — `cus_` or bare UUID (both accepted) + schema: + $ref: '#/components/schemas/CustomerId' + - name: id + in: path + required: true + description: Payment method ID — `pm_` or bare UUID (both accepted) + schema: + $ref: '#/components/schemas/PaymentMethodId' + responses: + '200': + description: Payment method details + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/PaymentMethodResponse' + '404': + $ref: '#/components/responses/NotFoundError' + put: + operationId: updatePaymentMethod + summary: Update a payment method + description: | + Replace mutable fields on a saved payment method — typically + customer-visible label, default-flag, or billing-address metadata. + Provider-issued identifiers such as `card_token` and on-chain + addresses are immutable; delete the payment method and re-create + it to change those. + tags: + - Payment Methods + security: + - bearerAuth: [] + parameters: + - name: customer_id + in: path + required: true + description: Customer ID — `cus_` or bare UUID (both accepted) + schema: + $ref: '#/components/schemas/CustomerId' + - name: id + in: path + required: true + description: Payment method ID — `pm_` or bare UUID (both accepted) + schema: + $ref: '#/components/schemas/PaymentMethodId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreatePaymentMethodRequest' + responses: + '200': + description: Payment method updated + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/PaymentMethodResponse' + '400': + description: Validation error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + delete: + operationId: deletePaymentMethod + summary: Delete a payment method + description: Delete a payment method from a customer. + tags: + - Payment Methods + security: + - bearerAuth: [] + parameters: + - name: customer_id + in: path + required: true + description: Customer ID — `cus_` or bare UUID (both accepted) + schema: + $ref: '#/components/schemas/CustomerId' + - name: id + in: path + required: true + description: Payment method ID — `pm_` or bare UUID (both accepted) + schema: + $ref: '#/components/schemas/PaymentMethodId' + responses: + '200': + description: Payment method deleted + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + /api/v1/customers/{customer_id}/platforms: + post: + operationId: populateKycData + summary: Submit platform registration data + description: Submit KYC/provider registration data for a customer on a specific provider platform. + tags: + - Provider Registration + security: + - bearerAuth: [] + parameters: + - name: customer_id + in: path + required: true + description: Customer ID — `cus_` or bare UUID (both accepted) + schema: + $ref: '#/components/schemas/CustomerId' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - provider + - target_role + properties: + provider: + type: string + description: Provider name (case-insensitive) + target_role: + type: string + description: Target role for the provider registration + additionalProperties: true + responses: + '200': + description: Platform data submitted + content: + application/json: + schema: + type: object + properties: + msg: + type: string + data: + type: array + description: | + Registration status for this customer across every + provider they are registered on, one entry per + `(provider, target_role)` pair. Same shape as + `GET /api/v1/provider-registration/{customer_id}/status`. + items: + type: object + properties: + provider: + type: string + description: Provider name (lowercased). + target_role: + type: string + description: Target role on this provider (lowercased). + status: + type: string + description: Current registration status (lowercased). + readiness: + type: object + nullable: true + description: Provider-specific readiness flags (JSONB pass-through). + additionalProperties: true + rejection_reason: + type: string + nullable: true + description: Human-readable rejection reason when rejection-like. + additionalProperties: true + '400': + description: Validation error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + '422': + $ref: '#/components/responses/ValidationError' + /api/v1/transactions: + get: + operationId: listTransactions + summary: List transactions + description: | + Retrieve a paginated list of transactions for the merchant. + Query filter values are case-insensitive (uppercased internally). + Response fields status, type, provider, currency, payment_method are returned lowercased. + tags: + - Transactions + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/LimitQuery' + - $ref: '#/components/parameters/StartingAfterQuery' + - $ref: '#/components/parameters/EndingBeforeQuery' + - $ref: '#/components/parameters/CreatedAtGteQuery' + - $ref: '#/components/parameters/CreatedAtLteQuery' + - $ref: '#/components/parameters/UpdatedAtGteQuery' + - $ref: '#/components/parameters/UpdatedAtLteQuery' + - name: type_list + in: query + description: Comma-separated transaction types (case-insensitive) + schema: + type: string + - name: status + in: query + description: Filter by status (case-insensitive) + schema: + type: string + - name: payment_method + in: query + description: Filter by payment method type (case-insensitive) + schema: + type: string + - name: source_currency + in: query + description: Filter by source currency (case-insensitive) + schema: + type: string + - name: destination_currency + in: query + description: Filter by destination currency (case-insensitive) + schema: + type: string + - name: provider + in: query + description: Filter by provider (case-insensitive) + schema: + type: string + responses: + '200': + description: Transaction list + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + type: object + required: + - has_more + - transaction_list + properties: + has_more: + type: boolean + description: | + True if more records exist beyond this page in the + requested direction. + count: + type: integer + description: | + Total records matching the active filters. Present + only when at least one non-time filter is supplied; + omitted on broad time-windowed scans. + transaction_list: + type: array + items: + $ref: '#/components/schemas/TransactionResponse' + /api/v1/transactions/{id}: + get: + operationId: getTransaction + summary: Get transaction details + description: Retrieve details of a single transaction. + tags: + - Transactions + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + description: | + Transaction ID. Accepts the prefixed form (`pay_`/`ref_`/`tr_`/ + `po_`/`buy_` + UUID) or the bare UUID; a prefix of a different + resource type is rejected with 422. + schema: + $ref: '#/components/schemas/TransactionId' + responses: + '200': + description: Transaction details + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/TransactionResponse' + '404': + $ref: '#/components/responses/NotFoundError' + /api/v1/transactions/{id}/settle: + patch: + operationId: settleTransaction + summary: Settle a transaction + description: Mark a transaction as settled. Requires app-key authentication. + tags: + - Transactions + security: [] + parameters: + - name: id + in: path + required: true + description: | + Transaction UID. Accepts the prefixed form (`pay_`/`ref_`/`tr_`/ + `po_`/`buy_` + UUID) or the bare UUID. + schema: + $ref: '#/components/schemas/TransactionId' + - name: app-key + in: header + required: true + description: Application key for settlement authorization + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + type: object + required: + - charge_id + - amount + - status + properties: + charge_id: + type: string + description: Provider charge identifier + amount: + type: integer + description: Settlement amount in smallest currency unit + status: + type: string + example: SETTLED + responses: + '200': + description: Transaction settled + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/TransactionResponse' + '400': + description: Settlement failed or invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '404': + $ref: '#/components/responses/NotFoundError' + '422': + $ref: '#/components/responses/ValidationError' + /api/v1/outbound_payments: + post: + operationId: createPayout + summary: Create an outbound payment + description: Initiate a payout/outbound payment to a customer's payment method. + tags: + - Payouts + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - payment_method_id + - amount + - currency + - customer_id + properties: + payment_method_id: + $ref: '#/components/schemas/PaymentMethodId' + description: Target payment method ID (`pm_` or bare UUID — both accepted) + amount: + type: integer + minimum: 1 + description: Amount in smallest currency unit + currency: + type: string + enum: + - USD + - BRL + description: Currency code + customer_id: + $ref: '#/components/schemas/CustomerId' + description: Customer ID (`cus_` or bare UUID — both accepted) + metadata: + type: object + description: Custom metadata + additionalProperties: true + example: + payment_method_id: 660e8400-e29b-41d4-a716-446655440000 + amount: 50000 + currency: BRL + customer_id: 550e8400-e29b-41d4-a716-446655440000 + responses: + '200': + description: Payout initiated + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/TransactionResponse' + '400': + description: Invalid request or payment method not found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + description: Customer or payment method not found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '422': + $ref: '#/components/responses/ValidationError' + /api/v1/transfer: + post: + operationId: createTransfer + summary: Create a transfer + description: | + Initiate a transfer between accounts. Supports single-provider and inter-platform transfers. + Fields provider, currency, payment_method.type are case-insensitive. + tags: + - Transfers + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - source + - destination + properties: + provider: + type: string + description: Provider for single-provider transfers (case-insensitive) + source: + type: object + description: Source account details + properties: + provider: + type: string + description: Source provider (for inter-platform transfers) + currency: + type: string + customer: + type: object + properties: + id: + $ref: '#/components/schemas/CustomerId' + description: Source customer ID (`cus_` or bare UUID — both accepted) + amount: + type: integer + minimum: 1 + destination: + type: object + description: Destination account details + properties: + provider: + type: string + description: Destination provider (for inter-platform transfers) + customer: + type: object + properties: + id: + $ref: '#/components/schemas/CustomerId' + description: Destination customer ID (`cus_` or bare UUID — both accepted) + payment_method: + type: object + properties: + type: + type: string + id: + $ref: '#/components/schemas/PaymentMethodId' + description: Destination payment method ID (`pm_` or bare UUID — both accepted) + currency: + type: string + chain: + type: string + metadata: + type: object + description: Custom metadata + additionalProperties: true + responses: + '200': + description: Transfer initiated + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/TransactionResponse' + '400': + description: Invalid request or platform error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + '409': + $ref: '#/components/responses/ConflictError' + '422': + $ref: '#/components/responses/ValidationError' + /api/v1/transfer/webhook: + post: + operationId: sendTransferWebhook + summary: Send transfer webhook (dev) + description: Manually trigger a webhook for a transfer. Intended for development/testing. + tags: + - Transfers + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - transfer_id + - status + properties: + transfer_id: + $ref: '#/components/schemas/TransferId' + description: Transfer transaction ID (`tr_` or bare UUID — both accepted) + status: + type: string + description: Status to set + responses: + '200': + description: Webhook sent + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + msg: + example: webhook sent successfully + data: + type: 'null' + '400': + description: Error sending webhook + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + /api/v1/buy: + post: + operationId: createBuy + summary: Create a buy transaction + description: | + Initiate a cryptocurrency/asset buy transaction. + Fields provider, currency, payment_method.type are case-insensitive. + tags: + - Buy / Sell + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - provider + - source + properties: + provider: + type: string + description: Provider name (case-insensitive) + source: + type: object + properties: + amount: + type: integer + minimum: 1 + currency: + type: string + customer: + type: object + properties: + id: + $ref: '#/components/schemas/CustomerId' + description: Customer ID (`cus_` or bare UUID — both accepted) + payment_method: + type: object + properties: + type: + type: string + destination: + type: object + properties: + currency: + type: string + payment_method: + type: object + properties: + type: + type: string + chain: + type: string + metadata: + type: object + description: | + Merchant-supplied arbitrary metadata, stored with the + transaction and echoed back on derived webhook deliveries. + Shape is defined by the merchant at request time. + additionalProperties: true + responses: + '200': + description: Buy transaction initiated + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/TransactionResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '409': + $ref: '#/components/responses/ConflictError' + '422': + $ref: '#/components/responses/ValidationError' + /api/v1/wallets/{customer_id}/balance: + get: + operationId: getWalletBalance + summary: Get trading wallet balance + description: Retrieve the trading wallet balance for a customer. + tags: + - Wallets + security: + - bearerAuth: [] + parameters: + - name: customer_id + in: path + required: true + description: Customer ID — `cus_` or bare UUID (both accepted) + schema: + $ref: '#/components/schemas/CustomerId' + responses: + '200': + description: Wallet balance + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + type: object + description: Trading wallet balance information + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + /api/v1/wallets/trades/buy: + post: + operationId: createWalletBuy + summary: Create a wallet buy trade + description: Initiate a buy trade through the wallet interface. + tags: + - Wallets + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - amount + - provider + - payment_method + - currency + - customer_id + properties: + amount: + type: integer + minimum: 1 + provider: + type: string + payment_method: + type: string + currency: + type: string + customer_id: + $ref: '#/components/schemas/CustomerId' + description: Customer ID (`cus_` or bare UUID — both accepted) + metadata: + type: object + description: | + Merchant-supplied arbitrary metadata, stored with the + transaction and echoed back on derived webhook deliveries. + Shape is defined by the merchant at request time. + additionalProperties: true + responses: + '200': + description: Buy trade initiated + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/TransactionResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '422': + $ref: '#/components/responses/ValidationError' + /api/v1/disputes: + get: + operationId: listDisputes + summary: List disputes + description: Retrieve a paginated list of disputes for the merchant. + tags: + - Disputes + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/LimitQuery' + - $ref: '#/components/parameters/StartingAfterQuery' + - $ref: '#/components/parameters/EndingBeforeQuery' + - $ref: '#/components/parameters/CreatedAtGteQuery' + - $ref: '#/components/parameters/CreatedAtLteQuery' + - $ref: '#/components/parameters/UpdatedAtGteQuery' + - $ref: '#/components/parameters/UpdatedAtLteQuery' + - name: id + in: query + description: Comma-separated dispute ids to filter by + schema: + type: string + - name: status + in: query + description: Comma-separated dispute statuses (case-insensitive) + schema: + type: string + - name: date_from + in: query + description: Only disputes created on or after this date (YYYY-MM-DD) + schema: + type: string + format: date + - name: date_to + in: query + description: Only disputes created on or before this date (YYYY-MM-DD) + schema: + type: string + format: date + - name: evidence_due_from + in: query + description: Only disputes with evidence due on or after this date (YYYY-MM-DD) + schema: + type: string + format: date + - name: evidence_due_to + in: query + description: Only disputes with evidence due on or before this date (YYYY-MM-DD) + schema: + type: string + format: date + responses: + '200': + description: Dispute list + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + type: object + required: + - has_more + - dispute_list + properties: + has_more: + type: boolean + description: | + True if more records exist beyond this page in the + requested direction. + count: + type: integer + description: | + Total records matching the active filters. Present + only when at least one non-time filter is supplied; + omitted on broad time-windowed scans. + dispute_list: + type: array + items: + $ref: '#/components/schemas/DisputeResponse' + /api/v1/disputes/{dispute_id}/evidence: + put: + operationId: updateDisputeEvidence + summary: Upload dispute evidence + description: Upload file and/or text evidence for a dispute. At least one of file_evidences or text_evidences is required. + tags: + - Disputes + security: + - bearerAuth: [] + parameters: + - name: dispute_id + in: path + required: true + description: Dispute ID — `dis_` or bare UUID (both accepted) + schema: + $ref: '#/components/schemas/DisputeId' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + file_evidences: + type: array + items: + type: object + required: + - id + - type + properties: + id: + $ref: '#/components/schemas/FileId' + description: File ID (`file_` or bare UUID — both accepted) + type: + type: string + enum: + - receipt + - customer_signature + - shipping_documentation + - service_documentation + - refund_policy + - cancellation_policy + - uncategorized_file + text_evidences: + type: array + items: + type: object + required: + - key + - value + properties: + key: + type: string + enum: + - customer_name + - customer_email_address + - customer_purchase_ip + - product_description + - duplicate_charge_id + - enhanced_evidence + - customer_communication + - refund_policy + - refund_policy_disclosure + - refund_refusal_explanation + - service_date + - shipping_address + - shipping_carrier + - shipping_date + - shipping_tracking_number + - shipping_tracking_url + - duplicate_charge_explanation + - cancellation_policy + - cancellation_rebuttal + - uncategorized_text + value: + type: + - string + - 'null' + responses: + '200': + description: Evidence uploaded + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/DisputeResponse' + '400': + description: Invalid evidence data + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + '422': + $ref: '#/components/responses/ValidationError' + /api/v1/disputes/{dispute_id}/submit: + put: + operationId: submitDispute + summary: Submit dispute for review + description: Mark the dispute as final and submit it to the provider for review. + tags: + - Disputes + security: + - bearerAuth: [] + parameters: + - name: dispute_id + in: path + required: true + description: Dispute ID — `dis_` or bare UUID (both accepted) + schema: + $ref: '#/components/schemas/DisputeId' + responses: + '200': + description: Dispute submitted + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/DisputeResponse' + '404': + $ref: '#/components/responses/NotFoundError' + /api/v1/disputes/{dispute_id}/close: + put: + operationId: closeDispute + summary: Close a dispute + description: | + Close the dispute by accepting the provider's outcome. Used when + the merchant chooses not to contest (or to stop contesting). Once + closed the dispute state is terminal — subsequent evidence uploads + are no-ops. For the final funds-movement state, listen for the + `payment.dispute.funds_withdrawn` or + `payment.dispute.funds_reinstated` webhook event. + tags: + - Disputes + security: + - bearerAuth: [] + parameters: + - name: dispute_id + in: path + required: true + description: Dispute ID — `dis_` or bare UUID (both accepted) + schema: + $ref: '#/components/schemas/DisputeId' + responses: + '200': + description: Dispute closed + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/DisputeResponse' + '404': + $ref: '#/components/responses/NotFoundError' + /api/v1/subscription/plans: + get: + operationId: listPlans + summary: List subscription plans + description: Retrieve all subscription plans for the merchant. + tags: + - Subscriptions + security: + - bearerAuth: [] + responses: + '200': + description: Plan list + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + msg: + example: Plans fetched successfully! + data: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/PlanResponse' + total: + type: integer + example: + msg: Plans fetched successfully! + data: + data: + - id: 550e8400-e29b-41d4-a716-446655440000 + name: Monthly Pro + description: Monthly professional plan + billing_cycle: month + billing_interval: 1 + price: 2999 + currency: USD + is_active: true + is_auto_renewable: true + allow_amount_override: false + start_time: '2025-01-01T00:00:00.000Z' + end_time: '2035-01-01T00:00:00.000Z' + created_by: admin@example.com + updated_by: null + created_at: '2025-01-01T00:00:00.000Z' + updated_at: '2025-01-01T00:00:00.000Z' + total: 1 + post: + operationId: createPlan + summary: Create a subscription plan + description: | + Define a recurring-payment plan template. Plans are created as + inactive and must be published via + `PATCH /api/v1/subscription/plans/{planId}/publish` before they + accept subscriptions. + + `billing_cycle` (`day`, `month`, `year`) combined with + `billing_interval` defines the billing period — e.g. + `{ billing_cycle: "month", billing_interval: 3 }` = quarterly. + tags: + - Subscriptions + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreatePlanRequest' + example: + name: Monthly Pro + description: Monthly professional plan + billing_cycle: month + billing_interval: 1 + price: 2999 + currency: USD + is_auto_renewable: true + allow_amount_override: false + start_date: '2025-01-01T00:00:00.000Z' + created_by: admin@example.com + responses: + '201': + description: Plan created content: application/json: schema: @@ -2521,39 +4678,55 @@ paths: - $ref: '#/components/schemas/ApiResponse' - type: object properties: + msg: + example: Plan created successfully! data: - $ref: '#/components/schemas/TransactionResponse' + $ref: '#/components/schemas/PlanResponse' + example: + msg: Plan created successfully! + data: + id: 550e8400-e29b-41d4-a716-446655440000 + name: Monthly Pro + description: Monthly professional plan + billing_cycle: month + billing_interval: 1 + price: 2999 + currency: USD + is_active: false + is_auto_renewable: true + allow_amount_override: false + start_time: '2025-01-01T00:00:00.000Z' + end_time: '2035-01-01T00:00:00.000Z' + created_by: admin@example.com + updated_by: null + created_at: '2025-01-01T00:00:00.000Z' + updated_at: '2025-01-01T00:00:00.000Z' '400': - description: Invalid request + description: Validation error content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' - '422': - $ref: '#/components/responses/ValidationError' - /api/v1/wallets/{customer_id}/balance: + /api/v1/subscription/plans/{planId}: get: - operationId: getWalletBalance - summary: Get trading wallet balance - description: Retrieve the trading wallet balance for a customer. + operationId: getPlan + summary: Get plan details + description: Retrieve details of a specific subscription plan. tags: - - Wallets + - Subscriptions security: - bearerAuth: [] parameters: - - name: customer_id + - name: planId in: path required: true + description: Plan UUID schema: type: string format: uuid responses: '200': - description: Wallet balance + description: Plan details content: application/json: schema: @@ -2561,59 +4734,63 @@ paths: - $ref: '#/components/schemas/ApiResponse' - type: object properties: + msg: + example: Plan fetched successfully! data: - type: object - description: Trading wallet balance information - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorResponse' - /api/v1/wallets/trades/buy: - post: - operationId: createWalletBuy - summary: Create a wallet buy trade - description: Initiate a buy trade through the wallet interface. + $ref: '#/components/schemas/PlanResponse' + example: + msg: Plan fetched successfully! + data: + id: 550e8400-e29b-41d4-a716-446655440000 + name: Monthly Pro + description: Monthly professional plan + billing_cycle: month + billing_interval: 1 + price: 2999 + currency: USD + is_active: true + is_auto_renewable: true + allow_amount_override: false + start_time: '2025-01-01T00:00:00.000Z' + end_time: '2035-01-01T00:00:00.000Z' + created_by: admin@example.com + updated_by: null + created_at: '2025-01-01T00:00:00.000Z' + updated_at: '2025-01-01T00:00:00.000Z' + '404': + $ref: '#/components/responses/NotFoundError' + patch: + operationId: updatePlan + summary: Update a plan + description: | + Update plan properties. All fields are optional. When the plan is + active, the following fields are immutable: `price`, `start_date`, + `currency`, `is_auto_renewable`. tags: - - Wallets + - Subscriptions security: - bearerAuth: [] + parameters: + - name: planId + in: path + required: true + description: Plan UUID + schema: + type: string + format: uuid requestBody: required: true content: application/json: schema: - type: object - required: - - amount - - provider - - payment_method - - currency - - customer_id - properties: - amount: - type: integer - minimum: 1 - provider: - type: string - payment_method: - type: string - currency: - type: string - customer_id: - type: string - format: uuid - metadata: - type: object - description: | - Merchant-supplied arbitrary metadata, stored with the - transaction and echoed back on derived webhook deliveries. - Shape is defined by the merchant at request time. - additionalProperties: true + $ref: '#/components/schemas/UpdatePlanRequest' + example: + name: Updated Plan Name + description: Updated description + updated_by: admin@example.com responses: '200': - description: Buy trade initiated + description: Plan updated content: application/json: schema: @@ -2621,53 +4798,59 @@ paths: - $ref: '#/components/schemas/ApiResponse' - type: object properties: + msg: + example: Plan updated successfully! data: - $ref: '#/components/schemas/TransactionResponse' - '400': - description: Invalid request + $ref: '#/components/schemas/PlanResponse' + example: + msg: Plan updated successfully! + data: + id: 550e8400-e29b-41d4-a716-446655440000 + name: Updated Plan Name + description: Updated description + billing_cycle: month + billing_interval: 1 + price: 2999 + currency: USD + is_active: true + is_auto_renewable: true + allow_amount_override: false + start_time: '2025-01-01T00:00:00.000Z' + end_time: '2035-01-01T00:00:00.000Z' + created_by: admin@example.com + updated_by: admin@example.com + created_at: '2025-01-01T00:00:00.000Z' + updated_at: '2025-06-15T10:30:00.000Z' + '404': + $ref: '#/components/responses/NotFoundError' + '422': + description: Attempted to modify immutable fields on an active plan content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' - '422': - $ref: '#/components/responses/ValidationError' - /api/v1/disputes: - get: - operationId: listDisputes - summary: List disputes - description: Retrieve a paginated list of disputes for the merchant. + delete: + operationId: deletePlan + summary: Delete a plan + description: | + Soft-delete a subscription plan. Existing subscribers continue + through their current billing cycle; no new subscriptions can + reference the plan after deletion. tags: - - Disputes + - Subscriptions security: - bearerAuth: [] parameters: - - $ref: '#/components/parameters/LimitQuery' - - $ref: '#/components/parameters/OffsetQuery' - - name: strict - in: query - description: Enforce limit restrictions + - name: planId + in: path + required: true + description: Plan UUID schema: - type: boolean + type: string + format: uuid responses: '200': - description: Dispute list - headers: - X-Limit-Requested: - schema: - type: integer - X-Limit-Applied: - schema: - type: integer - X-Offset-Requested: - schema: - type: integer - X-Offset-Applied: - schema: - type: integer + description: Plan deleted content: application/json: schema: @@ -2675,98 +4858,36 @@ paths: - $ref: '#/components/schemas/ApiResponse' - type: object properties: - data: - type: object - properties: - count: - type: integer - dispute_list: - type: array - items: - $ref: '#/components/schemas/DisputeResponse' - /api/v1/disputes/{dispute_id}/evidence: - put: - operationId: updateDisputeEvidence - summary: Upload dispute evidence - description: Upload file and/or text evidence for a dispute. At least one of file_evidences or text_evidences is required. + msg: + example: Plan deleted successfully! + example: + msg: Plan deleted successfully! + data: null + '404': + $ref: '#/components/responses/NotFoundError' + /api/v1/subscription/plans/{planId}/publish: + patch: + operationId: publishPlan + summary: Publish a plan + description: | + Activate a plan so it can accept subscriptions. Once published, + immutable fields (`price`, `start_date`, `currency`, + `is_auto_renewable`) can no longer be modified. tags: - - Disputes + - Subscriptions security: - bearerAuth: [] parameters: - - name: dispute_id + - name: planId in: path required: true - schema: - type: string - format: uuid - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - file_evidences: - type: array - items: - type: object - required: - - id - - type - properties: - id: - type: string - format: uuid - description: File ID - type: - type: string - enum: - - receipt - - customer_signature - - shipping_documentation - - service_documentation - - refund_policy - - cancellation_policy - - uncategorized_file - text_evidences: - type: array - items: - type: object - required: - - key - - value - properties: - key: - type: string - enum: - - customer_name - - customer_email_address - - customer_purchase_ip - - product_description - - duplicate_charge_id - - enhanced_evidence - - customer_communication - - refund_policy - - refund_policy_disclosure - - refund_refusal_explanation - - service_date - - shipping_address - - shipping_carrier - - shipping_date - - shipping_tracking_number - - shipping_tracking_url - - duplicate_charge_explanation - - cancellation_policy - - cancellation_rebuttal - - uncategorized_text - value: - type: - - string - - 'null' + description: Plan UUID + schema: + type: string + format: uuid responses: '200': - description: Evidence uploaded + description: Plan published content: application/json: schema: @@ -2774,41 +4895,214 @@ paths: - $ref: '#/components/schemas/ApiResponse' - type: object properties: + msg: + example: Plan published successfully! data: - $ref: '#/components/schemas/DisputeResponse' + $ref: '#/components/schemas/PlanResponse' + example: + msg: Plan published successfully! + data: + id: 550e8400-e29b-41d4-a716-446655440000 + name: Monthly Pro + description: Monthly professional plan + billing_cycle: month + billing_interval: 1 + price: 2999 + currency: USD + is_active: true + is_auto_renewable: true + allow_amount_override: false + start_time: '2025-01-01T00:00:00.000Z' + end_time: '2035-01-01T00:00:00.000Z' + created_by: admin@example.com + updated_by: null + created_at: '2025-01-01T00:00:00.000Z' + updated_at: '2025-06-15T10:30:00.000Z' + '404': + $ref: '#/components/responses/NotFoundError' + /api/v1/subscription/subscribe: + post: + operationId: subscribe + summary: Subscribe to a plan + description: | + Create a subscription for a customer to a published plan. The plan + must be active (published). + + **Immediate vs future start:** If `start_date` is omitted or in the + past, the subscription starts immediately and the first payment is + initiated. If `start_date` is in the future, the subscription is + queued and payment begins when the start date arrives. + + **Reactivation:** If a recently canceled subscription exists for the + same customer and plan (still within its billing period), it is + reactivated instead of creating a new one. + + **Price override:** If the plan has `allow_amount_override: true`, + `source.amount` can be provided to override the plan price. + + **Flow:** Controls Stripe payment routing. `platform` (default) + routes the charge through the platform account; `destination` + charges the connected account directly. + + **Fee:** `fee.bearer` controls who absorbs the Stripe processing + fee — `platform` (default) or `connected_account`. + + **Webhook events:** + - `subscription.created` — new subscription created + - `subscription.activated` — reactivated from canceled state + tags: + - Subscriptions + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateSubscriptionRequest' + example: + plan_id: 550e8400-e29b-41d4-a716-446655440000 + provider: stripe + flow: platform + source: + customer: + id: ba6fd3cc-1234-5678-9012-abcdef123456 + payment_method: + type: card + id: de8gf5ee-3456-7890-1234-cdef12345678 + destination: + customer: + id: cd7fe4dd-2345-6789-0123-bcdef1234567 + fee: + bearer: connected_account + responses: + '200': + description: Existing canceled subscription reactivated + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + msg: + example: Subscription reactivated successfully! + data: + $ref: '#/components/schemas/SubscriptionResponse' + example: + msg: Subscription reactivated successfully! + data: + id: a1b2c3d4-e5f6-7890-abcd-ef1234567890 + plan_id: 550e8400-e29b-41d4-a716-446655440000 + status: active + auto_renew: true + provider: stripe + flow: platform + source: + customer: + id: ba6fd3cc-1234-5678-9012-abcdef123456 + payment_method: + type: card + id: de8gf5ee-3456-7890-1234-cdef12345678 + destination: + customer: + id: cd7fe4dd-2345-6789-0123-bcdef1234567 + fee: + bearer: connected_account + original_billing_day: 15 + start_time: '2025-06-15T00:00:00.000Z' + end_time: '2025-07-15T00:00:00.000Z' + created_at: '2025-06-15T00:00:00.000Z' + updated_at: '2025-06-15T10:30:00.000Z' + '201': + description: Subscription created + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + msg: + example: Subscription created successfully! + data: + $ref: '#/components/schemas/SubscriptionResponse' + example: + msg: Subscription created successfully! + data: + id: a1b2c3d4-e5f6-7890-abcd-ef1234567890 + plan_id: 550e8400-e29b-41d4-a716-446655440000 + status: pending_activation + auto_renew: true + provider: stripe + flow: platform + source: + customer: + id: ba6fd3cc-1234-5678-9012-abcdef123456 + payment_method: + type: card + id: de8gf5ee-3456-7890-1234-cdef12345678 + destination: + customer: + id: cd7fe4dd-2345-6789-0123-bcdef1234567 + fee: + bearer: connected_account + original_billing_day: 15 + start_time: '2025-06-15T00:00:00.000Z' + end_time: '2025-07-15T00:00:00.000Z' + created_at: '2025-06-15T00:00:00.000Z' + updated_at: '2025-06-15T00:00:00.000Z' '400': - description: Invalid evidence data + description: Validation error content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' '404': - $ref: '#/components/responses/NotFoundError' + description: Plan not found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '409': + description: Active subscription already exists for this customer and plan + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' '422': - $ref: '#/components/responses/ValidationError' - /api/v1/disputes/{dispute_id}/submit: - put: - operationId: submitDispute - summary: Submit dispute for review - description: Mark the dispute as final and submit it to the provider for review. + description: Plan not active or amount override not allowed + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + /api/v1/subscription/subscriptions/{subscriptionId}/cancel: + patch: + operationId: cancelSubscription + summary: Cancel a subscription + description: | + Cancel an active subscription. Auto-renew is disabled and the + subscription status is set to `canceled`. Future billing cycles + are skipped; already-captured payments are preserved and must be + refunded separately via `POST /api/v1/payments/{id}/refund` if + required. + + **Webhook event:** `subscription.canceled` tags: - - Disputes + - Subscriptions security: - bearerAuth: [] parameters: - - name: dispute_id + - name: subscriptionId in: path required: true + description: Subscription UUID schema: type: string format: uuid responses: '200': - description: Dispute submitted + description: Subscription cancelled content: application/json: schema: @@ -2816,35 +5110,76 @@ paths: - $ref: '#/components/schemas/ApiResponse' - type: object properties: + msg: + example: Subscription canceled successfully! data: - $ref: '#/components/schemas/DisputeResponse' + $ref: '#/components/schemas/SubscriptionResponse' + example: + msg: Subscription canceled successfully! + data: + id: a1b2c3d4-e5f6-7890-abcd-ef1234567890 + plan_id: 550e8400-e29b-41d4-a716-446655440000 + status: canceled + auto_renew: false + provider: stripe + flow: platform + source: + customer: + id: ba6fd3cc-1234-5678-9012-abcdef123456 + payment_method: + type: card + id: de8gf5ee-3456-7890-1234-cdef12345678 + destination: + customer: + id: cd7fe4dd-2345-6789-0123-bcdef1234567 + fee: + bearer: connected_account + original_billing_day: 15 + start_time: '2025-06-15T00:00:00.000Z' + end_time: '2025-07-15T00:00:00.000Z' + created_at: '2025-06-15T00:00:00.000Z' + updated_at: '2025-06-20T14:00:00.000Z' '404': $ref: '#/components/responses/NotFoundError' - /api/v1/disputes/{dispute_id}/close: - put: - operationId: closeDispute - summary: Close a dispute + /api/v1/subscription/list: + get: + operationId: listSubscriptions + summary: List subscriptions description: | - Close the dispute by accepting the provider's outcome. Used when - the merchant chooses not to contest (or to stop contesting). Once - closed the dispute state is terminal — subsequent evidence uploads - are no-ops. For the final funds-movement state, listen for the - `payment.dispute.funds_withdrawn` or - `payment.dispute.funds_reinstated` webhook event. + List subscriptions for the merchant with optional filtering and + pagination. tags: - - Disputes + - Subscriptions security: - bearerAuth: [] parameters: - - name: dispute_id - in: path - required: true + - name: customer_id + in: query + description: Filter by source customer ID schema: type: string - format: uuid + - name: status + in: query + description: Filter by status (comma-separated — active, pending_activation, canceled, expired, queued, failed) + schema: + type: string + - name: per_page + in: query + description: Records per page (max 100) + schema: + type: integer + maximum: 100 + default: 10 + - name: page_no + in: query + description: Page number (1-based) + schema: + type: integer + minimum: 1 + default: 1 responses: '200': - description: Dispute closed + description: Subscription list content: application/json: schema: @@ -2852,22 +5187,64 @@ paths: - $ref: '#/components/schemas/ApiResponse' - type: object properties: + msg: + example: Subscriptions fetched successfully! data: - $ref: '#/components/schemas/DisputeResponse' - '404': - $ref: '#/components/responses/NotFoundError' - /api/v1/subscription/plans: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/SubscriptionResponse' + total: + type: integer + example: + msg: Subscriptions fetched successfully! + data: + data: + - id: a1b2c3d4-e5f6-7890-abcd-ef1234567890 + plan_id: 550e8400-e29b-41d4-a716-446655440000 + status: active + auto_renew: true + provider: stripe + flow: platform + source: + customer: + id: ba6fd3cc-1234-5678-9012-abcdef123456 + payment_method: + type: card + id: de8gf5ee-3456-7890-1234-cdef12345678 + destination: + customer: + id: cd7fe4dd-2345-6789-0123-bcdef1234567 + fee: + bearer: connected_account + original_billing_day: 15 + start_time: '2025-06-15T00:00:00.000Z' + end_time: '2025-07-15T00:00:00.000Z' + created_at: '2025-06-15T00:00:00.000Z' + updated_at: '2025-06-15T00:00:00.000Z' + total: 1 + /api/v1/subscription/{subscriptionId}: get: - operationId: listPlans - summary: List subscription plans - description: Retrieve all subscription plans for the merchant. + operationId: getSubscription + summary: Get subscription details + description: Retrieve details of a specific subscription. tags: - Subscriptions security: - bearerAuth: [] + parameters: + - name: subscriptionId + in: path + required: true + description: Subscription UUID + schema: + type: string + format: uuid responses: '200': - description: Plan list + description: Subscription details content: application/json: schema: @@ -2875,38 +5252,76 @@ paths: - $ref: '#/components/schemas/ApiResponse' - type: object properties: + msg: + example: Subscription fetched successfully! data: - type: array - items: - $ref: '#/components/schemas/PlanResponse' + $ref: '#/components/schemas/SubscriptionResponse' + example: + msg: Subscription fetched successfully! + data: + id: a1b2c3d4-e5f6-7890-abcd-ef1234567890 + plan_id: 550e8400-e29b-41d4-a716-446655440000 + status: active + auto_renew: true + provider: stripe + flow: platform + source: + customer: + id: ba6fd3cc-1234-5678-9012-abcdef123456 + payment_method: + type: card + id: de8gf5ee-3456-7890-1234-cdef12345678 + destination: + customer: + id: cd7fe4dd-2345-6789-0123-bcdef1234567 + fee: + bearer: connected_account + original_billing_day: 15 + start_time: '2025-06-15T00:00:00.000Z' + end_time: '2025-07-15T00:00:00.000Z' + created_at: '2025-06-15T00:00:00.000Z' + updated_at: '2025-06-15T00:00:00.000Z' + '404': + $ref: '#/components/responses/NotFoundError' + /api/v1/subscription/{subscriptionId}/payment: post: - operationId: createPlan - summary: Create a subscription plan + operationId: retrySubscriptionPayment + summary: Retry subscription payment description: | - Define a recurring-payment plan template. `price` must be between - 100 and 10_000 and `currency` must be `USD`. Plans are not eligible - for subscriptions until they are published via - `PATCH /api/v1/subscription/plans/{planId}/publish`. + Manually retry a payment for a subscription that is in + `pending_activation` status. Optionally update the payment method + used for this and future billing cycles. + + The retry is rejected if the subscription has already reached + the maximum retry count (configured via + `SUBSCRIPTION_PAYMENT_MAX_RETRIES`). tags: - Subscriptions security: - bearerAuth: [] + parameters: + - name: subscriptionId + in: path + required: true + description: Subscription UUID + schema: + type: string + format: uuid requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/CreatePlanRequest' + $ref: '#/components/schemas/RetryPaymentRequest' example: - name: Monthly Pro - description: Monthly professional plan - frequency: 30 - price: 2999 - currency: USD - created_by: admin@example.com + provider: stripe + source: + payment_method: + type: card + id: de8gf5ee-3456-7890-1234-cdef12345678 responses: - '201': - description: Plan created + '200': + description: Payment retry initiated content: application/json: schema: @@ -2915,28 +5330,60 @@ paths: - type: object properties: msg: - example: plan created + example: Payment retry initiated data: - type: string - description: Plan hash ID - /api/v1/subscription/plans/{planId}: - get: - operationId: getPlan - summary: Get plan details - description: Retrieve details of a specific subscription plan. + $ref: '#/components/schemas/SubscriptionResponse' + example: + msg: Payment retry initiated + data: + id: a1b2c3d4-e5f6-7890-abcd-ef1234567890 + plan_id: 550e8400-e29b-41d4-a716-446655440000 + status: pending_activation + auto_renew: true + provider: stripe + flow: platform + source: + customer: + id: ba6fd3cc-1234-5678-9012-abcdef123456 + payment_method: + type: card + id: de8gf5ee-3456-7890-1234-cdef12345678 + destination: + customer: + id: cd7fe4dd-2345-6789-0123-bcdef1234567 + fee: + bearer: connected_account + original_billing_day: 15 + start_time: '2025-06-15T00:00:00.000Z' + end_time: '2025-07-15T00:00:00.000Z' + created_at: '2025-06-15T00:00:00.000Z' + updated_at: '2025-06-20T14:00:00.000Z' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '404': + $ref: '#/components/responses/NotFoundError' + '422': + description: Subscription not in pending_activation status or max retries exceeded + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + /api/v1/provider-registration/schema: + get: + operationId: getKycSchema + summary: Get KYC schema + description: Retrieve the provider registration schema definition. tags: - - Subscriptions + - Provider Registration security: - bearerAuth: [] - parameters: - - name: planId - in: path - required: true - schema: - type: string responses: '200': - description: Plan details + description: KYC schema content: application/json: schema: @@ -2945,32 +5392,46 @@ paths: - type: object properties: data: - $ref: '#/components/schemas/PlanResponse' - '404': - $ref: '#/components/responses/NotFoundError' - patch: - operationId: updatePlan - summary: Update a plan - description: Update plan properties. All fields are optional. + type: object + description: Provider-specific KYC schema definition + /api/v1/provider-registration/{customer_id}/submit: + post: + operationId: submitKyc + summary: Submit provider registration + description: | + Submit KYC/provider registration for a customer. The request body varies + by provider. Fields provider and target_role are case-insensitive. tags: - - Subscriptions + - Provider Registration security: - bearerAuth: [] parameters: - - name: planId + - name: customer_id in: path required: true + description: Customer ID — `cus_` or bare UUID (both accepted) schema: - type: string + $ref: '#/components/schemas/CustomerId' requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/CreatePlanRequest' + type: object + required: + - provider + - target_role + properties: + provider: + type: string + description: Provider name (case-insensitive) + target_role: + type: string + description: Target role for registration + additionalProperties: true responses: '200': - description: Plan updated + description: Registration submitted content: application/json: schema: @@ -2978,33 +5439,43 @@ paths: - $ref: '#/components/schemas/ApiResponse' - type: object properties: - msg: - example: plan updated data: - type: string - description: Plan hash ID - delete: - operationId: deletePlan - summary: Delete a plan + type: object + '400': + description: Validation error or submission failed + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + '422': + $ref: '#/components/responses/ValidationError' + /api/v1/provider-registration/{customer_id}/status: + get: + operationId: getKycStatus + summary: Check provider registration status description: | - Soft-delete a subscription plan. The record is retained so that - existing subscribers continue through their current billing cycle - with the plan's snapshotted terms; no new subscriptions can - reference the plan after deletion. Hard removal requires ops - intervention. + Check the KYC/provider registration status for a customer across platforms. + Response fields provider, target_role, and status are returned lowercased. tags: - - Subscriptions + - Provider Registration security: - bearerAuth: [] parameters: - - name: planId + - name: customer_id in: path required: true + description: Customer ID — `cus_` or bare UUID (both accepted) schema: - type: string + $ref: '#/components/schemas/CustomerId' responses: '200': - description: Plan deleted + description: Registration status content: application/json: schema: @@ -3012,90 +5483,97 @@ paths: - $ref: '#/components/schemas/ApiResponse' - type: object properties: - msg: - example: plan deleted data: - type: string + type: object + description: Provider registration status per platform '404': $ref: '#/components/responses/NotFoundError' - /api/v1/subscription/plans/{planId}/publish: - patch: - operationId: publishPlan - summary: Publish a plan - description: Activate a plan so it can accept subscriptions. + /api/v1/taxes/calculate: + post: + operationId: calculateTaxes + summary: Calculate taxes + description: | + Calculate applicable taxes for a transaction. + Field provider is case-insensitive. tags: - - Subscriptions + - Tax security: - bearerAuth: [] - parameters: - - name: planId - in: path - required: true - schema: - type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - provider + properties: + provider: + type: string + description: Provider name (case-insensitive) + additionalProperties: true responses: '200': - description: Plan published + description: Tax calculation result content: application/json: schema: - allOf: - - $ref: '#/components/schemas/ApiResponse' - - type: object + type: object + properties: + msg: + type: string + data: + type: object + description: | + Tax calculation result. Echoes the request body fields + and adds a `provider_response` object containing the + raw tax-provider reply. Exact keys inside + `provider_response` vary per provider. properties: - msg: - example: plan published - data: + provider: type: string - /api/v1/subscription/subscribe: + description: Provider that computed the tax (lowercased). + provider_response: + type: object + description: Raw upstream tax-provider response. Shape varies. + additionalProperties: true + additionalProperties: true + '400': + description: Calculation error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + /api/v1/files: post: - operationId: subscribe - summary: Subscribe to a plan - description: Create a subscription for a customer to a plan. + operationId: uploadFiles + summary: Upload files + description: | + Upload one or more files for the merchant. Requests are + `multipart/form-data`; size and MIME-type limits follow the + server's upload configuration. tags: - - Subscriptions + - Files security: - bearerAuth: [] requestBody: required: true content: - application/json: + multipart/form-data: schema: type: object - required: - - plan_id - - source_customer_id - - destination_customer_id - - payment_method_id - - payment_method_type - - payment_method_provider - - fee_bearer properties: - plan_id: - type: string - description: Plan hash ID - source_customer_id: - type: string - format: uuid - destination_customer_id: - type: string - format: uuid - payment_method_id: - type: string - format: uuid - payment_method_type: - type: string - enum: - - CARD - payment_method_provider: - type: string - fee_bearer: + file: type: string - enum: - - connected_account + format: binary + description: File to upload (use repeated `file` fields to upload multiple). responses: '200': - description: Subscription created + description: Files uploaded content: application/json: schema: @@ -3103,99 +5581,40 @@ paths: - $ref: '#/components/schemas/ApiResponse' - type: object properties: - msg: - example: Subscription created successfully data: type: object - properties: - id: - type: string - description: Subscription hash ID - status: - type: string - example: pending_activation - sub_status: - type: string - example: payment_init '400': - description: Invalid request + description: Upload error (size / MIME-type violation, storage failure). content: application/json: schema: $ref: '#/components/schemas/ApiErrorResponse' - /api/v1/subscription/subscriptions/{subscriptionId}/cancel: - patch: - operationId: cancelSubscription - summary: Cancel a subscription - description: | - Cancel an active subscription. Future billing cycles - are skipped; already-captured payments are preserved and must be - refunded separately via `POST /api/v1/payments/{id}/refund` if a - refund is required. - tags: - - Subscriptions - security: - - bearerAuth: [] - parameters: - - name: subscriptionId - in: path - required: true - schema: - type: string - responses: - '200': - description: Subscription cancelled - content: - application/json: - schema: - type: object - properties: - msg: - type: string - example: Subscription cancelled successfully - data: - type: object - description: | - Pass-through response from the upstream subscription - service. Shape is not contractually stable; treat as - opaque acknowledgement. - additionalProperties: true - '404': - $ref: '#/components/responses/NotFoundError' - /api/v1/subscription/list: + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' get: - operationId: listSubscriptions - summary: List subscriptions - description: List subscriptions for a customer with pagination. + operationId: listFiles + summary: List files + description: | + Retrieve a paginated list of uploaded files for the merchant. This + endpoint has no non-time filters, so `count` is never included; iterate + with `has_more`. tags: - - Subscriptions + - Files security: - bearerAuth: [] parameters: - - name: customer_id - in: query - required: true - schema: - type: string - format: uuid - - name: status - in: query - description: Comma-separated statuses (active, pending_activation, canceled, expired, queued) - schema: - type: string - - name: per_page - in: query - schema: - type: integer - maximum: 100 - - name: page_no - in: query - schema: - type: integer - minimum: 1 + - $ref: '#/components/parameters/LimitQuery' + - $ref: '#/components/parameters/StartingAfterQuery' + - $ref: '#/components/parameters/EndingBeforeQuery' + - $ref: '#/components/parameters/CreatedAtGteQuery' + - $ref: '#/components/parameters/CreatedAtLteQuery' + - $ref: '#/components/parameters/UpdatedAtGteQuery' + - $ref: '#/components/parameters/UpdatedAtLteQuery' responses: '200': - description: Subscription list + description: File list content: application/json: schema: @@ -3205,44 +5624,47 @@ paths: properties: data: type: object + required: + - has_more + - file_list properties: - data: + has_more: + type: boolean + description: | + True if more records exist beyond this page in the + requested direction. + file_list: type: array items: - $ref: '#/components/schemas/SubscriptionResponse' - pagination: - type: object - properties: - per_page: - type: integer - page_no: - type: integer - total: - type: integer - '400': - description: Validation error - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorResponse' - /api/v1/subscription/{subscriptionId}: + type: object + properties: + id: + type: string + format: uuid + original_name: + type: string + created_at: + type: string + format: date-time + /api/v1/files/{file_id}: get: - operationId: getSubscription - summary: Get subscription details - description: Retrieve details of a specific subscription. + operationId: getFile + summary: Get file details + description: Retrieve details or download URL for a specific file. tags: - - Subscriptions + - Files security: - bearerAuth: [] parameters: - - name: subscriptionId + - name: file_id in: path required: true + description: File ID — `file_` or bare UUID (both accepted) schema: - type: string + $ref: '#/components/schemas/FileId' responses: '200': - description: Subscription details + description: File details content: application/json: schema: @@ -3251,28 +5673,141 @@ paths: - type: object properties: data: - $ref: '#/components/schemas/SubscriptionResponse' + type: object '404': $ref: '#/components/responses/NotFoundError' - /api/v1/subscription/{subscriptionId}/payment: + delete: + operationId: deleteFile + summary: Delete a file + description: | + Permanently delete a merchant-owned file, including its underlying + object-storage artifact. Irreversible; pre-signed URLs previously + generated against this file stop resolving once the object is + removed. + tags: + - Files + security: + - bearerAuth: [] + parameters: + - name: file_id + in: path + required: true + description: File ID — `file_` or bare UUID (both accepted) + schema: + $ref: '#/components/schemas/FileId' + responses: + '200': + description: File deleted + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + /api/v1/providers/{provider}/proxy: post: - operationId: initiateSubscriptionPayment - summary: Initiate subscription payment + operationId: genericProviderProxy + summary: Generic provider proxy call description: | - Manually initiate a payment for a subscription. + Proxy a request to a specific payment provider's API. + The endpoint and request body must be whitelisted in the merchant's configuration. - **Authentication.** Merchant `bearerAuth` only. + Transport-vs-semantic split (§10f.3): a HTTP 200 is returned whenever the + proxy call itself succeeded — including when the upstream provider + semantically rejected the request (a provider 4xx). In that case the + response carries `data.provider_status` with the upstream 4xx and + `data.provider_response` with the provider body. SDK callers MUST inspect + `provider_status` before treating an HTTP 200 as success. Provider 5xx / + unreachable / timeout return the canonical error envelope with a + 502/503/504 status. tags: - - Subscriptions + - Generic security: - bearerAuth: [] parameters: - - name: subscriptionId + - name: provider in: path required: true - description: Subscription hash ID. + description: Provider name schema: type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + description: Provider-specific request body (varies by provider and endpoint) + additionalProperties: true + responses: + '200': + description: Proxy call succeeded. `data.provider_status` carries the upstream provider HTTP status — 2xx on success, or a 4xx when the provider semantically rejected the request. Inspect `provider_status` before treating the response as success. + content: + application/json: + schema: + $ref: '#/components/schemas/ProviderProxyResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + '413': + $ref: '#/components/responses/PayloadTooLargeError' + '429': + $ref: '#/components/responses/RateLimitedError' + '500': + $ref: '#/components/responses/InternalServerError' + '502': + $ref: '#/components/responses/ProviderError' + '503': + $ref: '#/components/responses/ProviderUnavailableError' + '504': + $ref: '#/components/responses/ProviderUnavailableError' + /api/admin/statuspage/maintenances: + get: + operationId: listStatuspageMaintenances + summary: List scheduled maintenances + description: | + Returns all scheduled maintenances from Statuspage. Requires CS Admin + or Owner role with the `statuspage:manage` permission. + tags: + - Statuspage + security: + - accessTokenAuth: [] + responses: + '200': + description: Maintenances fetched + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/MaintenanceResponse' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + post: + operationId: createStatuspageMaintenance + summary: Schedule a maintenance window + description: | + Creates a scheduled maintenance on Statuspage for the specified + components. Notifies subscribers automatically. Requires CS Admin + or Owner role with the `statuspage:manage` permission. + tags: + - Statuspage + security: + - accessTokenAuth: [] requestBody: required: true content: @@ -3280,26 +5815,47 @@ paths: schema: type: object required: - - customer_id - - payment_method_id - - payment_method_type - - payment_method_provider + - name + - body + - scheduledFor + - scheduledUntil + - components properties: - customer_id: + name: type: string - format: uuid - payment_method_id: + description: Short title for the maintenance window. + example: Scheduled DB migration + body: type: string - format: uuid - payment_method_type: + description: Detailed message shown to status-page subscribers. + example: Brief downtime expected during schema migration. + scheduledFor: type: string - enum: - - CARD - payment_method_provider: + format: date-time + description: ISO 8601 start time of the maintenance window. + example: '2026-07-20T02:00:00Z' + scheduledUntil: type: string + format: date-time + description: ISO 8601 end time of the maintenance window. + example: '2026-07-20T03:00:00Z' + components: + type: array + description: Component keys to mark under maintenance. + items: + type: string + enum: + - api + - database + - messageQueue + - webhookDelivery + - stripe + - bridge + example: + - database responses: - '200': - description: Payment initiated + '201': + description: Maintenance scheduled content: application/json: schema: @@ -3308,38 +5864,103 @@ paths: - type: object properties: data: - type: object - properties: - id: - type: string - status: - type: string - sub_status: - type: string + $ref: '#/components/schemas/MaintenanceResponse' '400': - description: Invalid request + $ref: '#/components/responses/ValidationError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + /api/admin/statuspage/maintenances/{id}: + patch: + operationId: completeStatuspageMaintenance + summary: Complete a maintenance window + description: | + Marks a scheduled maintenance as completed on Statuspage. Requires + CS Admin or Owner role with the `statuspage:manage` permission. + tags: + - Statuspage + security: + - accessTokenAuth: [] + parameters: + - name: id + in: path + required: true + description: Statuspage maintenance ID. + schema: + type: string + example: abc123xyz + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - status + properties: + status: + type: string + enum: + - completed + example: completed + responses: + '200': + description: Maintenance completed content: application/json: schema: - $ref: '#/components/schemas/ApiErrorResponse' + $ref: '#/components/schemas/ApiResponse' + '400': + $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/UnauthorizedError' '403': $ref: '#/components/responses/ForbiddenError' - '404': - $ref: '#/components/responses/NotFoundError' - /api/v1/provider-registration/schema: + /api/health: get: - operationId: getKycSchema - summary: Get KYC schema - description: Retrieve the provider registration schema definition. + operationId: healthCheck + summary: Health check + description: Check API server health including database and message queue connectivity. tags: - - Provider Registration - security: - - bearerAuth: [] + - System + security: [] responses: '200': - description: KYC schema + description: All systems healthy + '503': + description: One or more systems unhealthy + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: error + message: + type: string + example: RabbitMQ is not available + /api/v1/countries: + get: + operationId: listCountries + summary: List countries + description: | + Return the list of countries currently eligible for KYC onboarding + (mutual Stripe ∩ Bridge support — Stripe Connect card-payments + capability + Bridge allowed). Each entry is tagged with a + `kyc_supported` boolean (always `true` in the current response). The + list is open — no authentication required. + + The list is maintained in the database and managed by CrowdSplit + staff through the dashboard; countries can be enabled or disabled + without a deployment. Changes propagate within about a minute. + tags: + - System + security: [] + responses: + '200': + description: Country list content: application/json: schema: @@ -3349,45 +5970,38 @@ paths: properties: data: type: object - description: Provider-specific KYC schema definition - /api/v1/provider-registration/{customer_id}/submit: - post: - operationId: submitKyc - summary: Submit provider registration - description: | - Submit KYC/provider registration for a customer. The request body varies - by provider. Fields provider and target_role are case-insensitive. + required: + - countries + properties: + countries: + type: array + items: + $ref: '#/components/schemas/CountryResponse' + /api/v1/payments/{id}/provider-snapshot/{version}: + get: + operationId: getPaymentProviderSnapshot + summary: Fetch a payment's untruncated provider snapshot + description: Resolves the `full_resource_url` embedded in a §2.9.3-truncated payment webhook payload. tags: - - Provider Registration + - Provider Snapshots security: - bearerAuth: [] parameters: - - name: customer_id + - name: id in: path required: true schema: - type: string - format: uuid - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - provider - - target_role - properties: - provider: - type: string - description: Provider name (case-insensitive) - target_role: - type: string - description: Target role for registration - additionalProperties: true + $ref: '#/components/schemas/PaymentId' + - name: version + in: path + required: true + description: The resource `version` (§2.6) the truncated webhook payload referenced — take it from the summary's own `version` field / `full_resource_url`, not from the payload's top-level `version` (which may be higher). + schema: + type: integer + minimum: 1 responses: '200': - description: Registration submitted + description: Full untruncated provider snapshot for this (id, version). content: application/json: schema: @@ -3396,42 +6010,53 @@ paths: - type: object properties: data: - type: object - '400': - description: Validation error or submission failed - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorResponse' + $ref: '#/components/schemas/ProviderSnapshotData' + example: + msg: Provider snapshot fetched successfully! + data: + provider_response: + stripe_payment_intent_id: pi_3TestExample001 + balance_transaction_id: txn_1NbA1B2C3D4E5F6 + fee_details: + - type: stripe_fee + amount: 250 + currency: usd + description: Stripe processing fees + version: 5 + retrieved_at: '2026-04-25T14:00:00.000Z' '401': $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' '404': $ref: '#/components/responses/NotFoundError' - '422': - $ref: '#/components/responses/ValidationError' - /api/v1/provider-registration/{customer_id}/status: + '410': + $ref: '#/components/responses/GoneError' + '429': + $ref: '#/components/responses/RateLimitedError' + /api/v1/refunds/{id}/provider-snapshot/{version}: get: - operationId: getKycStatus - summary: Check provider registration status - description: | - Check the KYC/provider registration status for a customer across platforms. - Response fields provider, target_role, and status are returned lowercased. + operationId: getRefundProviderSnapshot + summary: Fetch a refund's untruncated provider snapshot + description: Resolves the `full_resource_url` embedded in a §2.9.3-truncated refund webhook payload. tags: - - Provider Registration + - Provider Snapshots security: - bearerAuth: [] parameters: - - name: customer_id + - name: id in: path required: true schema: - type: string - format: uuid + $ref: '#/components/schemas/RefundId' + - name: version + in: path + required: true + description: The resource `version` (§2.6) the truncated webhook payload referenced — take it from the summary's own `version` field / `full_resource_url`, not from the payload's top-level `version` (which may be higher). + schema: + type: integer + minimum: 1 responses: '200': - description: Registration status + description: Full untruncated provider snapshot for this (id, version). content: application/json: schema: @@ -3440,96 +6065,108 @@ paths: - type: object properties: data: - type: object - description: Provider registration status per platform + $ref: '#/components/schemas/ProviderSnapshotData' + example: + msg: Provider snapshot fetched successfully! + data: + provider_response: + stripe_payment_intent_id: pi_3TestExample001 + balance_transaction_id: txn_1NbA1B2C3D4E5F6 + fee_details: + - type: stripe_fee + amount: 250 + currency: usd + description: Stripe processing fees + version: 5 + retrieved_at: '2026-04-25T14:00:00.000Z' + '401': + $ref: '#/components/responses/UnauthorizedError' '404': $ref: '#/components/responses/NotFoundError' - /api/v1/taxes/calculate: - post: - operationId: calculateTaxes - summary: Calculate taxes - description: | - Calculate applicable taxes for a transaction. - Field provider is case-insensitive. + '410': + $ref: '#/components/responses/GoneError' + '429': + $ref: '#/components/responses/RateLimitedError' + /api/v1/transfers/{id}/provider-snapshot/{version}: + get: + operationId: getTransferProviderSnapshot + summary: Fetch a transfer's untruncated provider snapshot + description: Payout-typed rows are transfer-family and surface under this segment — `po_…` ids are accepted alongside `tr_…`. tags: - - Tax + - Provider Snapshots security: - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - provider - properties: - provider: - type: string - description: Provider name (case-insensitive) - additionalProperties: true + parameters: + - name: id + in: path + required: true + schema: + $ref: '#/components/schemas/TransferId' + - name: version + in: path + required: true + description: The resource `version` (§2.6) the truncated webhook payload referenced — take it from the summary's own `version` field / `full_resource_url`, not from the payload's top-level `version` (which may be higher). + schema: + type: integer + minimum: 1 responses: '200': - description: Tax calculation result + description: Full untruncated provider snapshot for this (id, version). content: application/json: schema: - type: object - properties: - msg: - type: string - data: - type: object - description: | - Tax calculation result. Echoes the request body fields - and adds a `provider_response` object containing the - raw tax-provider reply. Exact keys inside - `provider_response` vary per provider. + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object properties: - provider: - type: string - description: Provider that computed the tax (lowercased). - provider_response: - type: object - description: Raw upstream tax-provider response. Shape varies. - additionalProperties: true - additionalProperties: true - '400': - description: Calculation error - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorResponse' + data: + $ref: '#/components/schemas/ProviderSnapshotData' + example: + msg: Provider snapshot fetched successfully! + data: + provider_response: + stripe_payment_intent_id: pi_3TestExample001 + balance_transaction_id: txn_1NbA1B2C3D4E5F6 + fee_details: + - type: stripe_fee + amount: 250 + currency: usd + description: Stripe processing fees + version: 5 + retrieved_at: '2026-04-25T14:00:00.000Z' '401': $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' - /api/v1/files: - post: - operationId: uploadFiles - summary: Upload files - description: | - Upload one or more files for the merchant. Requests are - `multipart/form-data`; size and MIME-type limits follow the - server's upload configuration. + '404': + $ref: '#/components/responses/NotFoundError' + '410': + $ref: '#/components/responses/GoneError' + '429': + $ref: '#/components/responses/RateLimitedError' + /api/v1/buys/{id}/provider-snapshot/{version}: + get: + operationId: getBuyProviderSnapshot + summary: Fetch a buy's untruncated provider snapshot + description: Resolves the `full_resource_url` embedded in a §2.9.3-truncated buy webhook payload. tags: - - Files + - Provider Snapshots security: - bearerAuth: [] - requestBody: - required: true - content: - multipart/form-data: - schema: - type: object - properties: - file: - type: string - format: binary - description: File to upload (use repeated `file` fields to upload multiple). + parameters: + - name: id + in: path + required: true + schema: + $ref: '#/components/schemas/BuyId' + - name: version + in: path + required: true + description: The resource `version` (§2.6) the truncated webhook payload referenced — take it from the summary's own `version` field / `full_resource_url`, not from the payload's top-level `version` (which may be higher). + schema: + type: integer + minimum: 1 responses: '200': - description: Files uploaded + description: Full untruncated provider snapshot for this (id, version). content: application/json: schema: @@ -3538,28 +6175,53 @@ paths: - type: object properties: data: - type: object - '400': - description: Upload error (size / MIME-type violation, storage failure). - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorResponse' + $ref: '#/components/schemas/ProviderSnapshotData' + example: + msg: Provider snapshot fetched successfully! + data: + provider_response: + stripe_payment_intent_id: pi_3TestExample001 + balance_transaction_id: txn_1NbA1B2C3D4E5F6 + fee_details: + - type: stripe_fee + amount: 250 + currency: usd + description: Stripe processing fees + version: 5 + retrieved_at: '2026-04-25T14:00:00.000Z' '401': $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + '410': + $ref: '#/components/responses/GoneError' + '429': + $ref: '#/components/responses/RateLimitedError' + /api/v1/sweeps/{id}/provider-snapshot/{version}: get: - operationId: listFiles - summary: List files - description: Retrieve a list of uploaded files for the merchant. + operationId: getSweepProviderSnapshot + summary: Fetch a sweep's untruncated provider snapshot + description: Contract-complete but inert in v1 — sweeps are not yet implemented as resources, so this always returns 404. tags: - - Files + - Provider Snapshots security: - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + $ref: '#/components/schemas/ResourceId' + - name: version + in: path + required: true + description: The resource `version` (§2.6) the truncated webhook payload referenced — take it from the summary's own `version` field / `full_resource_url`, not from the payload's top-level `version` (which may be higher). + schema: + type: integer + minimum: 1 responses: '200': - description: File list + description: Full untruncated provider snapshot for this (id, version). content: application/json: schema: @@ -3568,27 +6230,53 @@ paths: - type: object properties: data: - type: array - items: - type: object - /api/v1/files/{file_id}: + $ref: '#/components/schemas/ProviderSnapshotData' + example: + msg: Provider snapshot fetched successfully! + data: + provider_response: + stripe_payment_intent_id: pi_3TestExample001 + balance_transaction_id: txn_1NbA1B2C3D4E5F6 + fee_details: + - type: stripe_fee + amount: 250 + currency: usd + description: Stripe processing fees + version: 5 + retrieved_at: '2026-04-25T14:00:00.000Z' + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' + '410': + $ref: '#/components/responses/GoneError' + '429': + $ref: '#/components/responses/RateLimitedError' + /api/v1/disputes/{id}/provider-snapshot/{version}: get: - operationId: getFile - summary: Get file details - description: Retrieve details or download URL for a specific file. + operationId: getDisputeProviderSnapshot + summary: Fetch a dispute's untruncated provider snapshot + description: Note that oversized DISPUTE payloads usually truncate the nested payment snapshot, whose `full_resource_url` points at the payments segment, not this one. tags: - - Files + - Provider Snapshots security: - bearerAuth: [] parameters: - - name: file_id + - name: id in: path required: true schema: - type: string + $ref: '#/components/schemas/DisputeId' + - name: version + in: path + required: true + description: The resource `version` (§2.6) the truncated webhook payload referenced — take it from the summary's own `version` field / `full_resource_url`, not from the payload's top-level `version` (which may be higher). + schema: + type: integer + minimum: 1 responses: '200': - description: File details + description: Full untruncated provider snapshot for this (id, version). content: application/json: schema: @@ -3597,131 +6285,163 @@ paths: - type: object properties: data: - type: object + $ref: '#/components/schemas/ProviderSnapshotData' + example: + msg: Provider snapshot fetched successfully! + data: + provider_response: + stripe_payment_intent_id: pi_3TestExample001 + balance_transaction_id: txn_1NbA1B2C3D4E5F6 + fee_details: + - type: stripe_fee + amount: 250 + currency: usd + description: Stripe processing fees + version: 5 + retrieved_at: '2026-04-25T14:00:00.000Z' + '401': + $ref: '#/components/responses/UnauthorizedError' '404': $ref: '#/components/responses/NotFoundError' - delete: - operationId: deleteFile - summary: Delete a file - description: | - Permanently delete a merchant-owned file, including its underlying - object-storage artifact. Irreversible; pre-signed URLs previously - generated against this file stop resolving once the object is - removed. + '410': + $ref: '#/components/responses/GoneError' + '429': + $ref: '#/components/responses/RateLimitedError' + /api/v1/provider-registration/{id}/provider-snapshot/{version}: + get: + operationId: getProviderRegistrationProviderSnapshot + summary: Fetch a provider registration's untruncated provider snapshot + description: Registrations are keyed by the customer they register — `{id}` is the customer id (`cus_…`). tags: - - Files + - Provider Snapshots security: - bearerAuth: [] parameters: - - name: file_id + - name: id in: path required: true schema: - type: string + $ref: '#/components/schemas/CustomerId' + - name: version + in: path + required: true + description: The resource `version` (§2.6) the truncated webhook payload referenced — take it from the summary's own `version` field / `full_resource_url`, not from the payload's top-level `version` (which may be higher). + schema: + type: integer + minimum: 1 responses: '200': - description: File deleted + description: Full untruncated provider snapshot for this (id, version). content: application/json: schema: - $ref: '#/components/schemas/ApiResponse' + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/ProviderSnapshotData' + example: + msg: Provider snapshot fetched successfully! + data: + provider_response: + stripe_payment_intent_id: pi_3TestExample001 + balance_transaction_id: txn_1NbA1B2C3D4E5F6 + fee_details: + - type: stripe_fee + amount: 250 + currency: usd + description: Stripe processing fees + version: 5 + retrieved_at: '2026-04-25T14:00:00.000Z' '401': $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' '404': $ref: '#/components/responses/NotFoundError' - /api/v1/providers/{provider}/proxy: - post: - operationId: genericProviderProxy - summary: Generic provider proxy call - description: | - Proxy a request to a specific payment provider's API. - The endpoint and request body must be whitelisted in the merchant's configuration. + '410': + $ref: '#/components/responses/GoneError' + '429': + $ref: '#/components/responses/RateLimitedError' + /api/v1/customer-syncs/{id}/provider-snapshot/{version}: + get: + operationId: getCustomerSyncProviderSnapshot + summary: Fetch a customer sync's untruncated provider snapshot + description: Sync events are keyed by the customer they sync — `{id}` is the customer id (`cus_…`). tags: - - Generic + - Provider Snapshots security: - bearerAuth: [] parameters: - - name: provider + - name: id in: path required: true - description: Provider name schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - type: object - description: Provider-specific request body (varies by provider and endpoint) - additionalProperties: true - responses: - '200': - description: Provider response (pass-through) - content: - application/json: - schema: - type: object - description: | - Provider-specific response, passed through verbatim from - the upstream provider's API. Shape varies by `provider` - and by the specific endpoint being proxied — consult the - provider's own API documentation for the concrete shape. - additionalProperties: true - '400': - description: Request not whitelisted or provider error + $ref: '#/components/schemas/CustomerId' + - name: version + in: path + required: true + description: The resource `version` (§2.6) the truncated webhook payload referenced — take it from the summary's own `version` field / `full_resource_url`, not from the payload's top-level `version` (which may be higher). + schema: + type: integer + minimum: 1 + responses: + '200': + description: Full untruncated provider snapshot for this (id, version). content: application/json: schema: - $ref: '#/components/schemas/ApiErrorResponse' + allOf: + - $ref: '#/components/schemas/ApiResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/ProviderSnapshotData' + example: + msg: Provider snapshot fetched successfully! + data: + provider_response: + stripe_payment_intent_id: pi_3TestExample001 + balance_transaction_id: txn_1NbA1B2C3D4E5F6 + fee_details: + - type: stripe_fee + amount: 250 + currency: usd + description: Stripe processing fees + version: 5 + retrieved_at: '2026-04-25T14:00:00.000Z' '401': $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' '404': $ref: '#/components/responses/NotFoundError' - /api/health: - get: - operationId: healthCheck - summary: Health check - description: Check API server health including database and message queue connectivity. - tags: - - System - security: [] - responses: - '200': - description: All systems healthy - '503': - description: One or more systems unhealthy - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: error - message: - type: string - example: RabbitMQ is not available - /api/v1/countries: + '410': + $ref: '#/components/responses/GoneError' + '429': + $ref: '#/components/responses/RateLimitedError' + /api/v1/payment-methods/{id}/provider-snapshot/{version}: get: - operationId: listCountries - summary: List countries - description: | - Return the hardcoded list of countries currently eligible for KYC - onboarding (mutual Stripe ∩ Bridge support — Stripe Connect card-payments - capability + Bridge allowed). Each entry is tagged with a - `kyc_supported` boolean (always `true` in the current response). The list - is open — no authentication required. Source list: RCS-453. + operationId: getPaymentMethodProviderSnapshot + summary: Fetch a payment method's untruncated provider snapshot + description: Resolves the `full_resource_url` embedded in a §2.9.3-truncated payment-method webhook payload. tags: - - System - security: [] + - Provider Snapshots + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + $ref: '#/components/schemas/PaymentMethodId' + - name: version + in: path + required: true + description: The resource `version` (§2.6) the truncated webhook payload referenced — take it from the summary's own `version` field / `full_resource_url`, not from the payload's top-level `version` (which may be higher). + schema: + type: integer + minimum: 1 responses: '200': - description: Country list + description: Full untruncated provider snapshot for this (id, version). content: application/json: schema: @@ -3730,14 +6450,28 @@ paths: - type: object properties: data: - type: object - required: - - countries - properties: - countries: - type: array - items: - $ref: '#/components/schemas/CountryResponse' + $ref: '#/components/schemas/ProviderSnapshotData' + example: + msg: Provider snapshot fetched successfully! + data: + provider_response: + stripe_payment_intent_id: pi_3TestExample001 + balance_transaction_id: txn_1NbA1B2C3D4E5F6 + fee_details: + - type: stripe_fee + amount: 250 + currency: usd + description: Stripe processing fees + version: 5 + retrieved_at: '2026-04-25T14:00:00.000Z' + '401': + $ref: '#/components/responses/UnauthorizedError' + '404': + $ref: '#/components/responses/NotFoundError' + '410': + $ref: '#/components/responses/GoneError' + '429': + $ref: '#/components/responses/RateLimitedError' webhooks: payment.awaiting_confirmation: post: @@ -3746,14 +6480,13 @@ webhooks: Payment awaiting confirmation. Emitted when a payment transitions to the `payment.awaiting_confirmation` state in its lifecycle. Payload is the full `WebhookTransactionData` for the payment. Retries preserve `data.id`; - deduplicate on `CrowdSplit-Notification-Id`. + deduplicate on the envelope `id`. operationId: webhookPaymentAwaitingConfirmation tags: - Webhook Events parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -3778,14 +6511,13 @@ webhooks: Payment cancelled. Emitted when a payment transitions to the `payment.cancelled` state in its lifecycle. Payload is the full `WebhookTransactionData` for the payment. Retries preserve `data.id`; - deduplicate on `CrowdSplit-Notification-Id`. + deduplicate on the envelope `id`. operationId: webhookPaymentCancelled tags: - Webhook Events parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -3810,14 +6542,13 @@ webhooks: Payment captured. Emitted when a payment transitions to the `payment.captured` state in its lifecycle. Payload is the full `WebhookTransactionData` for the payment. Retries preserve `data.id`; - deduplicate on `CrowdSplit-Notification-Id`. + deduplicate on the envelope `id`. operationId: webhookPaymentCaptured tags: - Webhook Events parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -3842,14 +6573,13 @@ webhooks: Payment failed. Emitted when a payment transitions to the `payment.failed` state in its lifecycle. Payload is the full `WebhookTransactionData` for the payment. Retries preserve `data.id`; deduplicate on - `CrowdSplit-Notification-Id`. + the envelope `id`. operationId: webhookPaymentFailed tags: - Webhook Events parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -3881,7 +6611,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -3913,7 +6642,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -3938,14 +6666,13 @@ webhooks: Payment is processing. Emitted when a payment transitions to the `payment.processing` state in its lifecycle. Payload is the full `WebhookTransactionData` for the payment. Retries preserve `data.id`; - deduplicate on `CrowdSplit-Notification-Id`. + deduplicate on the envelope `id`. operationId: webhookPaymentProcessing tags: - Webhook Events parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -3970,14 +6697,13 @@ webhooks: Payment refunded. Emitted when a payment transitions to the `payment.refunded` state in its lifecycle. Payload is the full `WebhookTransactionData` for the payment. Retries preserve `data.id`; - deduplicate on `CrowdSplit-Notification-Id`. + deduplicate on the envelope `id`. operationId: webhookPaymentRefunded tags: - Webhook Events parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4002,14 +6728,13 @@ webhooks: Payment succeeded. Emitted when a payment transitions to the `payment.succeeded` state in its lifecycle. Payload is the full `WebhookTransactionData` for the payment. Retries preserve `data.id`; - deduplicate on `CrowdSplit-Notification-Id`. + deduplicate on the envelope `id`. operationId: webhookPaymentSucceeded tags: - Webhook Events parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4034,14 +6759,13 @@ webhooks: Payment updated. Emitted when a payment transitions to the `payment.updated` state in its lifecycle. Payload is the full `WebhookTransactionData` for the payment. Retries preserve `data.id`; deduplicate on - `CrowdSplit-Notification-Id`. + the envelope `id`. operationId: webhookPaymentUpdated tags: - Webhook Events parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4066,14 +6790,13 @@ webhooks: Transaction settlement/checkpoint updated. Emitted when a transaction's settlement or checkpoint state changes (typically during reconciliation). Carries the latest `WebhookTransactionData`; `status` reflects the new state. - Retries share the same `CrowdSplit-Notification-Id`. + Retries share the same envelope `id`. operationId: webhookTransactionScUpdated tags: - Webhook Events parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4104,7 +6827,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4136,7 +6858,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4168,7 +6889,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4200,7 +6920,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4231,7 +6950,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4262,7 +6980,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4293,7 +7010,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4324,7 +7040,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4355,7 +7070,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4386,7 +7100,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4418,7 +7131,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4449,7 +7161,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4479,7 +7190,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4509,7 +7219,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4539,7 +7248,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4569,7 +7277,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4599,7 +7306,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4631,7 +7337,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4663,7 +7368,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4695,7 +7399,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4728,7 +7431,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4760,7 +7462,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4793,7 +7494,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4826,7 +7526,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4858,7 +7557,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4890,7 +7588,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4922,7 +7619,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4954,7 +7650,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -4986,7 +7681,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -5019,7 +7713,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -5051,7 +7744,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -5083,7 +7775,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -5115,7 +7806,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -5147,7 +7837,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -5179,7 +7868,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -5211,7 +7899,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -5243,7 +7930,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -5275,7 +7961,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -5307,7 +7992,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -5339,7 +8023,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -5370,7 +8053,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -5403,7 +8085,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -5435,7 +8116,6 @@ webhooks: parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -5447,27 +8127,211 @@ webhooks: event: const: buy.completed category: - const: buy_lifecycle + const: buy_lifecycle + data: + $ref: '#/components/schemas/WebhookTransactionData' + responses: + '200': + description: Webhook acknowledged + buy.succeeded: + post: + summary: Buy succeeded + description: | + Buy succeeded. Emitted during a buy (fiat → stablecoin) flow. Payload is a + `WebhookTransactionData` with `type: buy`; `data.source` is the fiat leg and + `data.destination` is the crypto leg. `buy.awaiting_confirmation` is the + intermediate state while waiting on on-chain confirmation. + operationId: webhookBuySucceeded + tags: + - Webhook Events + parameters: + - $ref: '#/components/parameters/CrowdSplitSignatureHeader' + - $ref: '#/components/parameters/CrowdSplitTimestampHeader' + requestBody: + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/WebhookEnvelope' + - type: object + properties: + event: + const: buy.succeeded + category: + const: buy_lifecycle + data: + $ref: '#/components/schemas/WebhookTransactionData' + responses: + '200': + description: Webhook acknowledged + sell.failed: + post: + summary: Sell failed + description: | + Sell failed. Emitted during a sell (stablecoin → fiat) flow. Payload is a + `WebhookTransactionData` with `type: sell`. Use `data.provider_response` to + access provider-specific status codes on failure. + operationId: webhookSellFailed + tags: + - Webhook Events + parameters: + - $ref: '#/components/parameters/CrowdSplitSignatureHeader' + - $ref: '#/components/parameters/CrowdSplitTimestampHeader' + requestBody: + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/WebhookEnvelope' + - type: object + properties: + event: + const: sell.failed + category: + const: sell_lifecycle + data: + $ref: '#/components/schemas/WebhookTransactionData' + responses: + '200': + description: Webhook acknowledged + sell.succeeded: + post: + summary: Sell succeeded + description: | + Sell succeeded. Emitted during a sell (stablecoin → fiat) flow. Payload is a + `WebhookTransactionData` with `type: sell`. Use `data.provider_response` to + access provider-specific status codes on failure. + operationId: webhookSellSucceeded + tags: + - Webhook Events + parameters: + - $ref: '#/components/parameters/CrowdSplitSignatureHeader' + - $ref: '#/components/parameters/CrowdSplitTimestampHeader' + requestBody: + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/WebhookEnvelope' + - type: object + properties: + event: + const: sell.succeeded + category: + const: sell_lifecycle + data: + $ref: '#/components/schemas/WebhookTransactionData' + responses: + '200': + description: Webhook acknowledged + external.deposit.awaiting_confirmation: + post: + summary: External deposit awaiting confirmation + description: | + External deposit awaiting confirmation. Emitted during the external-deposit + lifecycle (funds arriving from outside CrowdSplit, e.g., a bank wire or + on-chain transfer to a virtual account). Payload is a + `WebhookTransactionData` with `type: external_deposit`; reconcile against + bank or on-chain records. + operationId: webhookExternalDepositAwaitingConfirmation + tags: + - Webhook Events + parameters: + - $ref: '#/components/parameters/CrowdSplitSignatureHeader' + - $ref: '#/components/parameters/CrowdSplitTimestampHeader' + requestBody: + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/WebhookEnvelope' + - type: object + properties: + event: + const: external.deposit.awaiting_confirmation + category: + const: externally_lifecycle + data: + $ref: '#/components/schemas/WebhookTransactionData' + responses: + '200': + description: Webhook acknowledged + external.deposit.completed: + post: + summary: External deposit completed + description: | + External deposit completed. Emitted during the external-deposit lifecycle + (funds arriving from outside CrowdSplit, e.g., a bank wire or on-chain + transfer to a virtual account). Payload is a `WebhookTransactionData` with + `type: external_deposit`; reconcile against bank or on-chain records. + operationId: webhookExternalDepositCompleted + tags: + - Webhook Events + parameters: + - $ref: '#/components/parameters/CrowdSplitSignatureHeader' + - $ref: '#/components/parameters/CrowdSplitTimestampHeader' + requestBody: + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/WebhookEnvelope' + - type: object + properties: + event: + const: external.deposit.completed + category: + const: externally_lifecycle + data: + $ref: '#/components/schemas/WebhookTransactionData' + responses: + '200': + description: Webhook acknowledged + external.deposit.received: + post: + summary: External deposit received + description: | + External deposit received. Emitted during the external-deposit lifecycle + (funds arriving from outside CrowdSplit, e.g., a bank wire or on-chain + transfer to a virtual account). Payload is a `WebhookTransactionData` with + `type: external_deposit`; reconcile against bank or on-chain records. + operationId: webhookExternalDepositReceived + tags: + - Webhook Events + parameters: + - $ref: '#/components/parameters/CrowdSplitSignatureHeader' + - $ref: '#/components/parameters/CrowdSplitTimestampHeader' + requestBody: + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/WebhookEnvelope' + - type: object + properties: + event: + const: external.deposit.received + category: + const: externally_lifecycle data: $ref: '#/components/schemas/WebhookTransactionData' responses: '200': description: Webhook acknowledged - buy.succeeded: + external.deposit.succeeded: post: - summary: Buy succeeded + summary: External deposit succeeded description: | - Buy succeeded. Emitted during a buy (fiat → stablecoin) flow. Payload is a - `WebhookTransactionData` with `type: buy`; `data.source` is the fiat leg and - `data.destination` is the crypto leg. `buy.awaiting_confirmation` is the - intermediate state while waiting on on-chain confirmation. - operationId: webhookBuySucceeded + External deposit succeeded. Emitted during the external-deposit lifecycle + (funds arriving from outside CrowdSplit, e.g., a bank wire or on-chain + transfer to a virtual account). Payload is a `WebhookTransactionData` with + `type: external_deposit`; reconcile against bank or on-chain records. + operationId: webhookExternalDepositSucceeded tags: - Webhook Events parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -5477,28 +8341,29 @@ webhooks: - type: object properties: event: - const: buy.succeeded + const: external.deposit.succeeded category: - const: buy_lifecycle + const: externally_lifecycle data: $ref: '#/components/schemas/WebhookTransactionData' responses: '200': description: Webhook acknowledged - sell.failed: + external.virtual_account.funded: post: - summary: Sell failed + summary: External virtual account funded description: | - Sell failed. Emitted during a sell (stablecoin → fiat) flow. Payload is a - `WebhookTransactionData` with `type: sell`. Use `data.provider_response` to - access provider-specific status codes on failure. - operationId: webhookSellFailed + External virtual account funded. Emitted when a virtual account assigned to a + customer receives a deposit from outside the CrowdSplit platform. Payload + identifies the receiving customer and the funding amount. This does not by + itself credit a wallet — use `external.deposit.*` events for the subsequent + credit lifecycle. + operationId: webhookExternalVirtualAccountFunded tags: - Webhook Events parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -5508,28 +8373,28 @@ webhooks: - type: object properties: event: - const: sell.failed + const: external.virtual_account.funded category: - const: sell_lifecycle + const: externally_lifecycle data: $ref: '#/components/schemas/WebhookTransactionData' responses: '200': description: Webhook acknowledged - sell.succeeded: + subscription.activated: post: - summary: Sell succeeded + summary: Subscription activated description: | - Sell succeeded. Emitted during a sell (stablecoin → fiat) flow. Payload is a - `WebhookTransactionData` with `type: sell`. Use `data.provider_response` to - access provider-specific status codes on failure. - operationId: webhookSellSucceeded + Subscription activated. Emitted when a subscription transitions to the + `active` state — either on initial activation after successful payment or + when reactivating a previously canceled subscription. Payload is a + `WebhookSubscriptionData` with the current subscription state. + operationId: webhookSubscriptionActivated tags: - Webhook Events parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -5539,30 +8404,28 @@ webhooks: - type: object properties: event: - const: sell.succeeded + const: subscription.activated category: - const: sell_lifecycle + const: subscription_lifecycle data: - $ref: '#/components/schemas/WebhookTransactionData' + $ref: '#/components/schemas/WebhookSubscriptionData' responses: '200': description: Webhook acknowledged - external.deposit.awaiting_confirmation: + subscription.canceled: post: - summary: External deposit awaiting confirmation + summary: Subscription canceled description: | - External deposit awaiting confirmation. Emitted during the external-deposit - lifecycle (funds arriving from outside CrowdSplit, e.g., a bank wire or - on-chain transfer to a virtual account). Payload is a - `WebhookTransactionData` with `type: external_deposit`; reconcile against - bank or on-chain records. - operationId: webhookExternalDepositAwaitingConfirmation + Subscription canceled. Emitted when a merchant or customer cancels an + active subscription. The subscription remains accessible until its + `end_time`; auto-renewal is disabled. Payload is a + `WebhookSubscriptionData` with `status: canceled`. + operationId: webhookSubscriptionCanceled tags: - Webhook Events parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -5572,29 +8435,28 @@ webhooks: - type: object properties: event: - const: external.deposit.awaiting_confirmation + const: subscription.canceled category: - const: externally_lifecycle + const: subscription_lifecycle data: - $ref: '#/components/schemas/WebhookTransactionData' + $ref: '#/components/schemas/WebhookSubscriptionData' responses: '200': description: Webhook acknowledged - external.deposit.completed: + subscription.created: post: - summary: External deposit completed + summary: Subscription created description: | - External deposit completed. Emitted during the external-deposit lifecycle - (funds arriving from outside CrowdSplit, e.g., a bank wire or on-chain - transfer to a virtual account). Payload is a `WebhookTransactionData` with - `type: external_deposit`; reconcile against bank or on-chain records. - operationId: webhookExternalDepositCompleted + Subscription created. Emitted when a new subscription is created via the + subscribe endpoint. The subscription may be in `pending_activation` or + `queued` status depending on its start date. Payload is a + `WebhookSubscriptionData` with the initial subscription state. + operationId: webhookSubscriptionCreated tags: - Webhook Events parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -5604,29 +8466,28 @@ webhooks: - type: object properties: event: - const: external.deposit.completed + const: subscription.created category: - const: externally_lifecycle + const: subscription_lifecycle data: - $ref: '#/components/schemas/WebhookTransactionData' + $ref: '#/components/schemas/WebhookSubscriptionData' responses: '200': description: Webhook acknowledged - external.deposit.received: + subscription.expired: post: - summary: External deposit received + summary: Subscription expired description: | - External deposit received. Emitted during the external-deposit lifecycle - (funds arriving from outside CrowdSplit, e.g., a bank wire or on-chain - transfer to a virtual account). Payload is a `WebhookTransactionData` with - `type: external_deposit`; reconcile against bank or on-chain records. - operationId: webhookExternalDepositReceived + Subscription expired. Emitted when an active subscription reaches its + `end_time` during auto-renewal processing and a new billing period begins. + The old subscription transitions to `expired`; a new `subscription.renewed` + event follows for the replacement subscription. + operationId: webhookSubscriptionExpired tags: - Webhook Events parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -5636,29 +8497,28 @@ webhooks: - type: object properties: event: - const: external.deposit.received + const: subscription.expired category: - const: externally_lifecycle + const: subscription_lifecycle data: - $ref: '#/components/schemas/WebhookTransactionData' + $ref: '#/components/schemas/WebhookSubscriptionData' responses: '200': description: Webhook acknowledged - external.deposit.succeeded: + subscription.payment_failed: post: - summary: External deposit succeeded + summary: Subscription payment failed description: | - External deposit succeeded. Emitted during the external-deposit lifecycle - (funds arriving from outside CrowdSplit, e.g., a bank wire or on-chain - transfer to a virtual account). Payload is a `WebhookTransactionData` with - `type: external_deposit`; reconcile against bank or on-chain records. - operationId: webhookExternalDepositSucceeded + Subscription payment failed. Emitted when a payment attempt for a + subscription fails. The subscription may still be retried up to the + configured maximum attempts. If all retries are exhausted the subscription + transitions to `failed` status. + operationId: webhookSubscriptionPaymentFailed tags: - Webhook Events parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -5668,30 +8528,29 @@ webhooks: - type: object properties: event: - const: external.deposit.succeeded + const: subscription.payment_failed category: - const: externally_lifecycle + const: subscription_lifecycle data: - $ref: '#/components/schemas/WebhookTransactionData' + $ref: '#/components/schemas/WebhookSubscriptionData' responses: '200': description: Webhook acknowledged - external.virtual_account.funded: + subscription.renewed: post: - summary: External virtual account funded + summary: Subscription renewed description: | - External virtual account funded. Emitted when a virtual account assigned to a - customer receives a deposit from outside the CrowdSplit platform. Payload - identifies the receiving customer and the funding amount. This does not by - itself credit a wallet — use `external.deposit.*` events for the subsequent - credit lifecycle. - operationId: webhookExternalVirtualAccountFunded + Subscription renewed. Emitted when the auto-renewal cron creates a new + subscription period for an expiring subscription. This event fires for the + newly created subscription; the previous period receives a + `subscription.expired` event. Payment for the new period is initiated + immediately after renewal. + operationId: webhookSubscriptionRenewed tags: - Webhook Events parameters: - $ref: '#/components/parameters/CrowdSplitSignatureHeader' - $ref: '#/components/parameters/CrowdSplitTimestampHeader' - - $ref: '#/components/parameters/CrowdSplitNotificationIdHeader' requestBody: content: application/json: @@ -5701,11 +8560,11 @@ webhooks: - type: object properties: event: - const: external.virtual_account.funded + const: subscription.renewed category: - const: externally_lifecycle + const: subscription_lifecycle data: - $ref: '#/components/schemas/WebhookTransactionData' + $ref: '#/components/schemas/WebhookSubscriptionData' responses: '200': description: Webhook acknowledged @@ -5741,41 +8600,114 @@ components: description: Human-readable message describing the result data: description: Response payload (null on error) - required: - - msg - - data - ApiErrorResponse: - type: object - properties: - msg: - type: string - description: Human-readable error message - data: - type: 'null' - description: Always null for errors provider_message: type: string - description: Optional error message from the underlying payment provider + description: | + Optional message forwarded from the underlying payment provider + (Stripe, Bridge, …). Present when the operation surfaced a + provider-side error or warning. required: - msg - data - BillingAddress: + ErrorCode: + type: string + description: validation_error (400/413/422) · authentication_error (401) · authorization_error (403) · not_found (404) · conflict (409) · gone (410) · rate_limited (429) · internal_error (500) · provider_error (502) · provider_unavailable (503/504). + enum: + - validation_error + - authentication_error + - authorization_error + - not_found + - conflict + - gone + - rate_limited + - internal_error + - provider_error + - provider_unavailable + FailureReason: type: object properties: - house_number: + code: type: string - street_number: + description: Normalized, machine-stable reason identifier (small stable taxonomy, e.g. invalid_input, missing_required_information, card_declined). + message: type: string - street_name: + nullable: true + description: Human-readable, UI-safe description; null when none. + fields: + type: array + items: + type: string + description: Affected normalized field names; [] when none apply. + deadline: type: string - postal_code: + format: date-time + nullable: true + description: RFC 3339 hard-blocker deadline; null when none. + provider_code: type: string - city: + nullable: true + description: Raw provider code, verbatim, non-sensitive only (Stripe codes pass through; Bridge is always null). + requires_manual_review: + type: boolean + description: True when the merchant cannot self-remediate (contact support instead of resubmitting). + support_reference: type: string - state: + nullable: true + description: Opaque support case ID when requires_manual_review is true; else null. Never parse or pattern-match. + required: + - code + - message + - fields + - deadline + - provider_code + - requires_manual_review + - support_reference + ApiErrorResponse: + type: object + properties: + msg: type: string - country_code: + description: Human-readable summary, safe for merchant logs (not for end-user display — use failure_reasons[].message instead). + error_code: + $ref: '#/components/schemas/ErrorCode' + failure_reasons: + type: array + description: Detailed structured reasons (§2.2 stable shape). Always present, possibly empty. Merchants must default-handle unknown codes. + items: + $ref: '#/components/schemas/FailureReason' + data: + description: Null for most errors. For error_code provider_error / provider_unavailable, may carry `provider_response` with a curated upstream snapshot. + nullable: true + type: object + properties: + provider_response: + description: Raw provider-debug context (curated, non-sensitive). + provider_message: type: string + description: Legacy top-level mirror of provider detail kept for existing integrations (e.g. duplicate-payment-method detail). Additive; prefer failure_reasons. + required: + - msg + - error_code + - failure_reasons + - data + CustomerId: + type: string + description: | + Customer identifier (`cus_` + UUID). Emitted prefixed (bare UUID + possible while prefixed IDs roll out); both forms are accepted on + input, and a prefix of a different resource type is rejected + with 422. + pattern: ^(cus_)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ + example: cus_9c4f2a71-3e85-4b1a-9f27-6d5c8b3a2e10 + PaymentMethodId: + type: string + description: | + Payment method identifier (`pm_` + UUID). Emitted prefixed (bare + UUID possible while prefixed IDs roll out); both forms are accepted + on input, and a prefix of a different resource type is rejected + with 422. + pattern: ^(pm_)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ + example: pm_b2d84f6a-1c39-47e5-8a02-5e9d7c4b3f21 CreatePaymentRequest: type: object description: | @@ -5813,9 +8745,8 @@ components: type: object properties: id: - type: string - format: uuid - description: CrowdSplit customer ID + $ref: '#/components/schemas/CustomerId' + description: CrowdSplit customer ID (`cus_` or bare UUID — both accepted) payment_method: type: object required: @@ -5826,18 +8757,8 @@ components: enum: - card id: - type: string - format: uuid - description: Saved payment method ID (for card payments) - card_token: - type: string - description: One-time card token from provider - expiry_date: - type: string - format: date-time - description: PIX QR code expiry (required for PIX payments) - billing_address: - $ref: '#/components/schemas/BillingAddress' + $ref: '#/components/schemas/PaymentMethodId' + description: Saved payment method ID for card payments (`pm_` or bare UUID — both accepted) capture_method: type: string enum: @@ -5846,6 +8767,8 @@ components: description: Whether to capture immediately or manually later fraud_check: type: object + required: + - enabled properties: enabled: type: boolean @@ -5870,8 +8793,8 @@ components: type: object properties: id: - type: string - format: uuid + $ref: '#/components/schemas/CustomerId' + description: Destination customer ID (`cus_` or bare UUID — both accepted) fee: type: object description: Fee configuration for destination flow @@ -5882,6 +8805,18 @@ components: - platform - connected_account default: platform + application_fee_amount: + type: integer + minimum: 1 + description: | + Platform fee to collect from this charge, in the charge + currency's minor units, the same convention as `source.amount`. + Denominated in `source.currency` rather than the destination + currency, and must not exceed `source.amount`. + Requires `destination.customer.id`, since the fee is collected + from the destination connected account. Supported on both + `platform` and `destination` flows. Distinct from `fee.bearer`, + which allocates the provider's processing fee. flow: type: string enum: @@ -5912,10 +8847,6 @@ components: amount: type: integer minimum: 1 - total_installments: - type: integer - minimum: 1 - description: Total installment count confirm: type: boolean description: Whether to auto-confirm the payment @@ -5926,14 +8857,104 @@ components: transaction and echoed back on the response and on any derived webhook deliveries. Shape is defined by the merchant. additionalProperties: true + ResourceId: + type: string + description: | + Resource identifier, emitted as `_` where the prefix + encodes the resource type: payment `pay_`, refund `ref_`, transfer + `tr_`, payout `po_`, buy `buy_`, sweep `swp_`, dispute `dis_`, + customer `cus_`, payment method `pm_`, provider registration + `preg_`, file `file_`. + + The prefix is presentation-layer only — the underlying identifier + is the UUID, and while prefixed IDs roll out the bare UUID form may + still appear on output. On input both the prefixed and bare forms + are accepted; a prefix belonging to a different resource type is + rejected with 422. Treat IDs as opaque strings. + pattern: ^((pay|ref|tr|po|buy|swp|dis|cus|pm|preg|file)_)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ + example: pay_a4c7e1f2-9b53-4d62-b0d4-3f8e2c5a1b9d + PaymentId: + type: string + description: | + Payment identifier (`pay_` + UUID). Emitted prefixed (bare UUID + possible while prefixed IDs roll out); both forms are accepted on + input, and a prefix of a different resource type is rejected + with 422. + pattern: ^(pay_)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ + example: pay_a4c7e1f2-9b53-4d62-b0d4-3f8e2c5a1b9d + RefundId: + type: string + description: | + Refund identifier (`ref_` + UUID). Emitted prefixed (bare UUID + possible while prefixed IDs roll out); both forms are accepted on + input, and a prefix of a different resource type is rejected + with 422. + pattern: ^(ref_)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ + example: ref_c81d4e2b-0f67-49a3-b5c2-7a1e9d3f5b60 + TransferId: + type: string + description: | + Transfer identifier (`tr_` + UUID). Emitted prefixed (bare UUID + possible while prefixed IDs roll out); both forms are accepted on + input, and a prefix of a different resource type is rejected + with 422. + pattern: ^(tr_)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ + example: tr_5a2c7e91-4b38-4f6d-a1e0-8c3b9d5f2a74 + PayoutId: + type: string + description: | + Payout identifier. Payout list rows are transfer-typed + transactions, so a payout ID usually carries the `tr_` prefix; + `po_` appears on PAY_OUT-typed rows. Emitted prefixed (bare UUID + possible while prefixed IDs roll out); both forms are accepted on + input. + pattern: ^((tr|po)_)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ + example: po_d94b1f38-7c25-4a6e-9b02-3e5f8a1c7d96 + BuyId: + type: string + description: | + Buy transaction identifier (`buy_` + UUID). Emitted prefixed (bare + UUID possible while prefixed IDs roll out); both forms are accepted + on input, and a prefix of a different resource type is rejected + with 422. + pattern: ^(buy_)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ + example: buy_1f6e3a82-9d47-4c05-b8a1-2e7c5b9f4d13 + TransactionId: + description: | + Transaction identifier. Transactions are a family of resources, so + the prefix follows the transaction's own type: payment `pay_`, + refund `ref_`, transfer `tr_`, payout `po_`, buy `buy_`. Emitted + prefixed (bare UUID possible while prefixed IDs roll out); both + forms are accepted on input. + example: pay_a4c7e1f2-9b53-4d62-b0d4-3f8e2c5a1b9d + allOf: + - $ref: '#/components/schemas/ResourceId' + - anyOf: + - $ref: '#/components/schemas/PaymentId' + - $ref: '#/components/schemas/RefundId' + - $ref: '#/components/schemas/TransferId' + - $ref: '#/components/schemas/PayoutId' + - $ref: '#/components/schemas/BuyId' + ResourceVersion: + type: integer + minimum: 1 + description: 'Monotonically-increasing resource version (§2.6): 1 on creation, +1 on every state mutation that changes a wire-visible field. A given (id, version) pair always reflects the same resource state. Use it to order out-of-order webhook deliveries, ignore stale re-deliveries (version ≤ your cached value), and detect delivery gaps (backfill with a GET of the resource). A version bump without a corresponding webhook is possible (some wire-visible mutations don''t emit events) — treat a gap as "re-fetch", not "lost delivery".' + DisputeId: + type: string + description: | + Dispute identifier (`dis_` + UUID). Emitted prefixed (bare UUID + possible while prefixed IDs roll out); both forms are accepted on + input, and a prefix of a different resource type is rejected + with 422. + pattern: ^(dis_)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ + example: dis_7f3a9c25-8b41-4e6d-b590-2c8f5a7d1e34 PaymentResponse: type: object description: Payment transaction response object properties: id: - type: string - format: uuid - description: Transaction unique ID + $ref: '#/components/schemas/TransactionId' + description: Transaction unique ID (`pay_` for payments, `ref_` for refunds) status: type: string description: Current transaction status (lowercased in response) @@ -5969,12 +8990,16 @@ components: description: Payment method used on the source leg. Extra keys vary by provider. properties: id: - type: string - format: uuid + $ref: '#/components/schemas/PaymentMethodId' description: Payment method unique ID. type: type: string description: Payment method type (lowercased — card, pix, bank, …). + kind: + type: string + description: | + Payment method sub-type discriminator (lowercased). Canonical + alias of `type`; emitted alongside `type` with the same value. chain: type: string description: Blockchain network when the method is on-chain (lowercased). @@ -5983,9 +9008,26 @@ components: type: object properties: id: - type: string - format: uuid + $ref: '#/components/schemas/CustomerId' description: Customer (subject) unique ID. + fee: + type: object + description: | + Fee configuration, echoed back unchanged from the request body. + Present only when the merchant supplied one. + properties: + bearer: + type: string + description: Who bears the provider's processing fee (lowercased). + additionalProperties: true + application_fee_amount: + type: integer + description: | + Platform fee taken from the charge, in the charge currency's + minor units. Echoed back from the request body; present only + when the merchant supplied one. + version: + $ref: '#/components/schemas/ResourceVersion' created_at: type: string format: date-time @@ -6005,31 +9047,32 @@ components: the response and on any derived webhook deliveries. Shape is defined by the merchant at request time. additionalProperties: true - CreateMerchantRequest: - type: object - required: - - legal_name - - address - - website - - tax_number - properties: - legal_name: - type: string - description: Legal business name - address: - type: string - description: Business address - website: - type: string - description: Official website URL - tax_number: - type: string - description: Tax identification number + refund_ids: + type: array + description: | + Refund back-links for this payment. Always present on payment + responses (empty when none). Absent for non-payment types. + items: + $ref: '#/components/schemas/RefundId' + dispute_ids: + type: array + description: | + Dispute back-links for this payment. Always present on payment + responses (empty when none). Absent for non-payment types. + items: + $ref: '#/components/schemas/DisputeId' MerchantResponse: type: object + description: | + Merchant record returned by the dashboard merchant endpoints + (POST /merchant, GET /merchant, GET /merchant/:id, PATCH /merchant/:id). properties: id: - type: integer + type: string + format: uuid + description: | + Public, client-facing merchant identifier (the merchant uid). + The internal numeric primary key is never exposed on the wire. legalName: type: string address: @@ -6041,14 +9084,51 @@ components: appId: type: string description: Generated application ID - isApproved: - type: boolean + activationStatus: + type: string + description: | + Activation/KYB lifecycle and the single source of truth for live + access. "Can process live" == activationStatus === 'approved'. + enum: + - not_submitted + - submitted + - in_review + - approved + - changes_requested + - rejected + - suspended createdAt: type: string format: date-time - updatedAt: + required: + - id + - legalName + - address + - website + - taxNumber + - appId + - activationStatus + - createdAt + CreateMerchantRequest: + type: object + required: + - legal_name + - address + - website + - tax_number + properties: + legal_name: type: string - format: date-time + description: Legal business name + address: + type: string + description: Business address + website: + type: string + description: Official website URL + tax_number: + type: string + description: Tax identification number GrantTokenRequest: type: object required: @@ -6078,11 +9158,15 @@ components: - Bearer expires_in: type: integer - description: Token expiry time in milliseconds + description: Seconds until the access token expires (OAuth convention). + refresh_token: + type: string + description: Refresh token used to obtain a new access token via /merchant/token/refresh. required: - access_token - token_type - expires_in + - refresh_token TransferDateRequest: type: object required: @@ -6122,6 +9206,10 @@ components: description: Holiday provider name WebhookResponse: type: object + description: | + Webhook registration record (wire shape, snake_case). The signing + `secret` is NOT part of this shape — it is returned exactly once, at + registration (§2.9.2); update/toggle/list/get responses never echo it. properties: id: type: string @@ -6130,11 +9218,20 @@ components: type: string description: type: string - isActive: + nullable: true + is_active: type: boolean - secret: + event_types: + type: array + nullable: true + items: + type: string + description: | + Subscribed wire event values (see §2.9.7). `null` = subscribed to + all events. + created_at: type: string - description: Webhook signing secret (only returned on registration) + format: date-time RegisterWebhookRequest: type: object required: @@ -6148,6 +9245,14 @@ components: type: string maxLength: 255 description: Webhook description + event_types: + type: array + minItems: 1 + items: + type: string + description: | + Wire event values this endpoint subscribes to + (e.g. `payment.succeeded`). Omitted → subscribed to all events. UpdateWebhookRequest: type: object properties: @@ -6157,46 +9262,303 @@ components: description: type: string maxLength: 255 + event_types: + type: + - array + - 'null' + minItems: 1 + items: + type: string + description: | + Replaces the subscription list. Explicit `null` clears it back to + "all events"; omitted leaves it unchanged. + WebhookDeliveryAttempt: + type: object + description: One delivery attempt for a stored webhook notification (§2.9.9). + required: + - attempt + - url + - status_code + - duration_ms + - attempted_at + - is_replay + - replay_attempt_at + properties: + attempt: + type: integer + minimum: 1 + description: | + 1-indexed attempt number, contiguous per target URL; replays + continue the numbering after the last live attempt. + url: + type: string + description: | + Target receiver URL. Additive vs the contract's single-endpoint + model: one notification fans out to every subscribed endpoint. + status_code: + type: + - integer + - 'null' + description: | + HTTP status returned by the merchant endpoint; null when no + response was received (timeout, network failure). + duration_ms: + type: + - integer + - 'null' + description: Dispatch-to-response time in milliseconds; null on timeout. + attempted_at: + type: string + format: date-time + is_replay: + type: boolean + description: | + True for attempts triggered via the replay endpoint; false for + the original delivery and automatic retries. + replay_attempt_at: + type: + - string + - 'null' + format: date-time + description: When the replay was triggered; null on non-replay attempts. WebhookNotification: type: object + description: | + Stored webhook notification record returned by the merchant + notifications endpoints. Field names are snake_case on the wire. + required: + - id + - is_acknowledged + - status + - event + - category + - data properties: - notificationId: + id: + type: string + pattern: ^(evt_)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ + description: | + Notification ID (`evt_`; pre-cutover rows surface the bare + UUID). Inputs dual-accept both forms. + is_acknowledged: + type: boolean + description: Whether the merchant has acknowledged receipt + status: type: string + enum: + - pending + - delivered + - dead_lettered + description: | + Aggregate delivery status (§2.9.9): `pending` while any receiver is + still owed a delivery, `delivered` when every receiver got a 2xx, + `dead_lettered` when the retry schedule is exhausted (recover via + `?status=dead_lettered` + replay, §2.9.6 Path A). + webhook_id: + type: + - string + - 'null' format: uuid + description: | + The webhook registration this notification was addressed to, when it + fanned out to exactly one endpoint; null on multi-endpoint fan-out + (use `delivery_attempts[].url`) and on rows predating the link. + event: + type: + - string + - 'null' + description: | + Event type (e.g. `payment.succeeded`). Null when the notification + was recorded before the event taxonomy was applied. + category: + type: + - string + - 'null' + description: | + Webhook category (e.g. `payment`, `customer`). Null when not + classified yet. data: - type: object - properties: - type: - type: string - description: Event type (e.g. payment.succeeded) - id: - type: string - format: uuid - isAcknowledged: - type: boolean - CustomerResponse: + description: | + Original event payload as delivered to the merchant. Shape + depends on `event` and `category`; see the webhook events + section for the per-event payload shapes. + endpoints: + type: array + items: + type: string + description: Receiver URLs this notification fanned out to. + delivery_attempts: + type: array + items: + $ref: '#/components/schemas/WebhookDeliveryAttempt' + description: | + Every delivery attempt for this notification, oldest first + (§2.9.9). Present on merchant-surface reads. + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + MerchantApiKey: type: object + description: API-key metadata (the secret is never returned on reads). properties: id: type: string format: uuid - document_number: + description: Public API-key UID + label: type: string - tax_id: + publicKey: type: string - document_type: + isActive: + type: boolean + lastUsedAt: type: string - description: Returned lowercased + format: date-time + nullable: true + createdAt: + type: string + format: date-time + MerchantInvitation: + type: object + properties: + id: + type: string + format: uuid + description: Public invitation UID email: type: string format: email - social_name: + role: type: string - first_name: + status: + type: string + enum: + - PENDING + - ACCEPTED + - CANCELLED + expiresAt: + type: string + format: date-time + acceptedAt: + type: string + format: date-time + nullable: true + createdAt: + type: string + format: date-time + MerchantProviderConfig: + type: object + description: | + A saved provider configuration. Provider credentials are stored + encrypted and redacted on reads. + properties: + platform: + type: string + enum: + - STRIPE + - BRIDGE + isActive: + type: boolean + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + MerchantProviderConfigSaveResult: + type: object + description: | + Result of saving a provider config. The webhook signing secret(s) + generated during provider webhook registration are returned exactly + once here and never again. + properties: + platform: + type: string + enum: + - STRIPE + - BRIDGE + isActive: + type: boolean + webhookSecrets: + type: object + description: | + Provider-specific webhook signing secret(s). Shape depends on + the platform: + - For STRIPE, `{ main, connected }` — the two Stripe webhook + endpoint signing secrets. + - For BRIDGE, `{ webhookSecret }` — Bridge's webhook public + key string, keyed under `webhookSecret` for uniformity. + Additional keys may appear as new providers are added. + additionalProperties: + type: string + createdAt: + type: string + format: date-time + updatedAt: type: string + format: date-time + CustomerResponse: + type: object + description: | + Customer (subject) record returned by POST /customers, GET + /customers, GET /customers/:id, and PATCH /customers/:id. Shape + reflects the runtime emission in + `services/subjectService.getSubjectObjectForResponse`; fields on + the subject entity, its address/phone JSONB blobs, and its + additional_info JSONB blob are all serialized here. `merchant_*` + and `platforms` are conditional and only present on cross-merchant + (CS console) reads or list/detail reads that eagerly load those + relations. + required: + - id + properties: + id: + $ref: '#/components/schemas/CustomerId' + description: Public, client-facing customer identifier (subject uid). + document_number: + type: + - string + - 'null' + document_type: + type: + - string + - 'null' + description: Returned lowercased. + email: + type: + - string + - 'null' + format: email + first_name: + type: + - string + - 'null' last_name: + type: + - string + - 'null' + synced: + type: + - boolean + - 'null' + description: | + True when the customer's core profile has been sync'd to all + registered platforms; false when there is a pending drift; null + before the first sync attempt. + synced_at: + type: + - string + - 'null' + format: date-time + version: + $ref: '#/components/schemas/ResourceVersion' + created_at: type: string - dob: + format: date-time + updated_at: type: string format: date-time house_number: @@ -6223,32 +9585,105 @@ components: type: - string - 'null' + country_code: + type: + - string + - 'null' + description: Returned lowercased. + subdivision: + type: + - string + - 'null' phone_country_code: - type: string + type: + - string + - 'null' phone_area_code: - type: string + type: + - string + - 'null' phone_number: - type: string - monthly_net_income: type: - string - 'null' + dob: + type: + - string + - 'null' + description: Date of birth, as supplied on create/update (raw string). + mother_name: + type: + - string + - 'null' + monthly_net_income: + type: + - number + - 'null' gender: type: - string - 'null' - description: Returned lowercased - country_code: - type: string - description: Returned lowercased - roles: - type: array - items: - type: string - wallet_address: + description: Returned lowercased. + owner_legal_name: + type: + - string + - 'null' + owner_document_number: + type: + - string + - 'null' + owner_document_type: + type: + - string + - 'null' + company_name: + type: + - string + - 'null' + company_start_date: type: - string - 'null' + merchant_uid: + type: string + format: uuid + description: Present only on cross-merchant reads (CS console). + merchant_name: + type: string + description: Present only on cross-merchant reads (CS console). + platforms: + type: array + description: | + Per-provider registration/KYC status. Present when the caller + loaded the `subjectPlatforms` relation (list + detail reads). + items: + type: object + properties: + provider: + type: + - string + - 'null' + provider_customer_id: + type: + - string + - 'null' + description: The provider's own customer id (e.g. Stripe `cus_…`). + status: + type: string + target_role: + type: string + action_required: + type: boolean + readiness: + type: + - string + - 'null' + failure_reasons: + type: + - array + - 'null' + items: + type: object CreateCustomerRequest: type: object description: | @@ -6278,8 +9713,6 @@ components: type: string last_name: type: string - tax_id: - type: string dob: type: string format: date @@ -6318,7 +9751,7 @@ components: mother_name: type: string monthly_net_income: - type: string + type: number owner_legal_name: type: string owner_document_number: @@ -6329,8 +9762,6 @@ components: type: string company_start_date: type: string - wallet_address: - type: string SyncCustomerRequest: type: object required: @@ -6360,8 +9791,7 @@ components: type: object properties: customer_id: - type: string - format: uuid + $ref: '#/components/schemas/CustomerId' provider: type: string role: @@ -6385,8 +9815,7 @@ components: type: object properties: id: - type: string - format: uuid + $ref: '#/components/schemas/PaymentMethodId' type: type: string description: Returned lowercased @@ -6396,6 +9825,8 @@ components: provider: type: string description: Returned lowercased + version: + $ref: '#/components/schemas/ResourceVersion' chain: type: - string @@ -6508,8 +9939,7 @@ components: description: Transaction object (field casing in response is lowercased for status, type, provider, currency) properties: id: - type: string - format: uuid + $ref: '#/components/schemas/TransactionId' status: type: string description: Transaction status (lowercased) @@ -6533,19 +9963,22 @@ components: type: object properties: id: - type: string - format: uuid + $ref: '#/components/schemas/CustomerId' description: Customer (subject) unique ID. payment_method: type: object description: Payment method used on the source leg. properties: id: - type: string - format: uuid + $ref: '#/components/schemas/PaymentMethodId' type: type: string description: Payment method type (lowercased). + kind: + type: string + description: | + Payment method sub-type discriminator (lowercased). Canonical + alias of `type`; emitted alongside `type` with the same value. chain: type: string description: Blockchain network (lowercased). @@ -6565,19 +9998,22 @@ components: type: object properties: id: - type: string - format: uuid + $ref: '#/components/schemas/CustomerId' description: Destination customer (subject) unique ID. payment_method: type: object description: Payment method used on the destination leg. properties: id: - type: string - format: uuid + $ref: '#/components/schemas/PaymentMethodId' type: type: string description: Payment method type (lowercased). + kind: + type: string + description: | + Payment method sub-type discriminator (lowercased). Canonical + alias of `type`; emitted alongside `type` with the same value. chain: type: string description: Blockchain network (lowercased). @@ -6589,6 +10025,8 @@ components: Merchant-supplied arbitrary metadata, echoed back unchanged. Shape is defined by the merchant at request time. additionalProperties: true + version: + $ref: '#/components/schemas/ResourceVersion' created_at: type: string format: date-time @@ -6602,137 +10040,532 @@ components: format: date-time DisputeResponse: type: object + description: | + Dispute record returned by GET /disputes, GET /disputes/:id. + Shape reflects the runtime emission in + `services/disputeService.getDisputeResponse` (:460); the + associated `payment` is only present when the dispute has a + linked transaction, and `merchant_uid` / `merchant_name` are only + present on cross-merchant (CS console) reads. + required: + - id + - status + - evidences + - created_at properties: id: - type: string - format: uuid + $ref: '#/components/schemas/DisputeId' + description: Public dispute identifier. status: type: string - transaction_id: - type: string - format: uuid - amount: - type: integer - currency: - type: string - reason: - type: string + description: Dispute lifecycle status. + evidence_due_by: + type: + - string + - 'null' + description: | + Deadline for submitting evidence, sourced from provider metadata + (e.g. Stripe's `evidence_due_by`). Null when the provider has + not communicated one. + evidences: + type: array + description: Submitted evidence records for this dispute. + items: + type: object + additionalProperties: true + version: + $ref: '#/components/schemas/ResourceVersion' created_at: type: string format: date-time - updated_at: + payment: + type: object + description: | + Snapshot of the payment this dispute is against, built via + `transactionService.getTransactionInfoForResponse`. Same shape + as a transaction detail read — includes `id`, `status`, `type`, + `source`, `destination`, timestamps, and any provider response. + Absent when the dispute has no linked transaction id. + additionalProperties: true + properties: + id: + $ref: '#/components/schemas/PaymentId' + reference_transaction: + type: object + description: | + Point-in-time snapshot of the disputed payment under the canonical + nested reference key. Same value/shape as `payment`; emitted + alongside it during migration. For live state call `GET /payments/{id}`. + additionalProperties: true + properties: + id: + $ref: '#/components/schemas/PaymentId' + merchant_uid: type: string - format: date-time + format: uuid + description: Present only on cross-merchant reads (CS console). + merchant_name: + type: string + description: Present only on cross-merchant reads (CS console). + FileId: + type: string + description: | + File identifier (`file_` + UUID). Emitted prefixed (bare UUID + possible while prefixed IDs roll out); both forms are accepted on + input, and a prefix of a different resource type is rejected + with 422. + pattern: ^(file_)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ + example: file_4e8b2d91-6a35-4c7f-8e10-9b3d5f2a6c48 PlanResponse: type: object + required: + - id + - name + - description + - billing_cycle + - billing_interval + - price + - currency + - is_active + - is_auto_renewable + - allow_amount_override + - start_time + - end_time + - created_by + - created_at + - updated_at properties: - hash_id: + id: type: string + format: uuid + description: Plan unique ID name: type: string + description: Plan display name description: type: string - frequency: + description: Plan description + billing_cycle: + type: string + enum: + - day + - month + - year + description: Billing period unit + billing_interval: type: integer + description: Number of billing_cycle units per period price: type: integer - overridden_price: - type: integer + description: Price in smallest currency unit (e.g. cents) + currency: + type: string + enum: + - USD + - AUD + description: ISO 4217 currency code is_active: type: boolean - start_time: - type: string - format: date-time - end_time: - type: string - format: date-time + description: Whether the plan is published and accepting subscriptions is_auto_renewable: type: boolean - currency: - type: string + description: Whether subscriptions on this plan auto-renew allow_amount_override: type: boolean - campaign_id: + description: Whether subscribers can override the plan price + start_time: type: string - campaign_name: + format: date-time + description: Plan availability start + end_time: type: string + format: date-time + description: Plan availability end created_by: type: string + description: Identifier of the user who created the plan + updated_by: + type: + - string + - 'null' + description: Identifier of the user who last updated the plan created_at: type: string format: date-time + description: Record creation timestamp updated_at: type: string format: date-time + description: Record last-update timestamp CreatePlanRequest: type: object required: - name - description - - frequency + - billing_cycle + - billing_interval - price - currency + - start_date - created_by properties: name: type: string + minLength: 1 + maxLength: 255 + description: Plan display name description: type: string - frequency: + minLength: 1 + description: Plan description + billing_cycle: + type: string + enum: + - day + - month + - year + description: Billing period unit + billing_interval: type: integer - description: Billing frequency in days + minimum: 1 + description: Number of billing_cycle units per period (e.g. 1 month, 3 months) price: type: integer - description: Price amount + minimum: 1 + description: Price in smallest currency unit (e.g. cents) currency: type: string + enum: + - USD + - AUD + description: ISO 4217 currency code + is_auto_renewable: + type: boolean + default: false + description: Whether subscriptions on this plan auto-renew + allow_amount_override: + type: boolean + default: false + description: Whether subscribers can override the plan price + start_date: + type: string + format: date-time + description: Plan availability start (ISO 8601) + end_date: + type: string + format: date-time + description: Plan availability end (defaults to start_date + 10 years) created_by: type: string - overridden_price: - type: integer - start_time: + minLength: 1 + maxLength: 255 + description: Identifier of the user creating the plan + UpdatePlanRequest: + type: object + description: | + All fields are optional. When the plan is active, `price`, + `start_date`, `currency`, and `is_auto_renewable` cannot be modified. + properties: + name: type: string - format: date - end_time: + minLength: 1 + maxLength: 255 + description: type: string - format: date + minLength: 1 + billing_cycle: + type: string + enum: + - day + - month + - year + billing_interval: + type: integer + minimum: 1 + price: + type: integer + minimum: 1 + currency: + type: string + enum: + - USD + - AUD is_auto_renewable: type: boolean allow_amount_override: type: boolean - campaign_id: + start_date: + type: string + format: date-time + end_date: + type: string + format: date-time + updated_by: + type: string + minLength: 1 + maxLength: 255 + CreateSubscriptionRequest: + type: object + required: + - plan_id + - provider + - source + - destination + properties: + plan_id: + type: string + format: uuid + description: UUID of the published plan + provider: + type: string + minLength: 1 + maxLength: 50 + description: Payment provider (e.g. stripe) + flow: type: string - campaign_name: + enum: + - platform + - destination + description: | + Payment flow type. `platform` (default) routes the charge + through the platform account; `destination` charges the + connected account directly. + source: + type: object + required: + - customer + - payment_method + properties: + customer: + type: object + required: + - id + properties: + id: + $ref: '#/components/schemas/CustomerId' + description: Payer customer ID (`cus_` or bare UUID — both accepted) + payment_method: + type: object + required: + - type + - id + properties: + type: + type: string + minLength: 1 + maxLength: 50 + description: Payment method type (e.g. card) + id: + $ref: '#/components/schemas/PaymentMethodId' + description: Stored payment method ID (`pm_` or bare UUID — both accepted) + amount: + type: integer + minimum: 1 + description: Override price (only when plan allows amount override) + destination: + type: object + required: + - customer + properties: + customer: + type: object + required: + - id + properties: + id: + $ref: '#/components/schemas/CustomerId' + description: Recipient customer ID (`cus_` or bare UUID — both accepted) + fee: + type: object + description: Fee configuration + properties: + bearer: + type: string + enum: + - platform + - connected_account + description: Who bears the transaction fee + start_date: type: string + format: date-time + description: Subscription start (defaults to now; future dates queue the subscription) SubscriptionResponse: type: object + required: + - id + - plan_id + - status + - auto_renew + - provider + - source + - destination + - start_time + - end_time + - created_at + - updated_at properties: - hash_id: + id: + type: string + format: uuid + description: Subscription unique ID + plan_id: + type: string + format: uuid + description: Associated plan ID + status: + type: string + enum: + - active + - pending_activation + - canceled + - expired + - queued + - failed + description: Current subscription status + auto_renew: + type: boolean + description: Whether the subscription auto-renews at period end + provider: type: string + description: Payment provider (e.g. stripe) + flow: + type: + - string + - 'null' + enum: + - platform + - destination + - null + description: Payment flow type + source: + type: object + description: Payer details + properties: + customer: + type: object + properties: + id: + type: string + description: Customer ID of the payer + payment_method: + type: object + properties: + type: + type: string + description: Payment method type (e.g. card) + id: + type: string + description: Stored payment method ID + destination: + type: object + description: Recipient details + properties: + customer: + type: object + properties: + id: + type: string + description: Customer ID of the payment recipient + fee: + type: + - object + - 'null' + description: Fee configuration (null when no fee bearer is set) + properties: + bearer: + type: string + enum: + - platform + - connected_account + description: Who bears the transaction fee + original_billing_day: + type: + - integer + - 'null' + description: Original day-of-month for billing anchor start_time: type: string format: date-time + description: Subscription period start end_time: type: string format: date-time - plan_hash_id: - type: string - auto_renew: - type: boolean - status: - type: string - enum: - - active - - pending_activation - - canceled - - expired - - queued + description: Subscription period end created_at: type: string format: date-time + description: Record creation timestamp updated_at: type: string format: date-time + description: Record last-update timestamp + RetryPaymentRequest: + type: object + required: + - provider + - source + properties: + provider: + type: string + minLength: 1 + maxLength: 50 + description: Payment provider (e.g. stripe) + source: + type: object + required: + - payment_method + properties: + payment_method: + type: object + required: + - type + - id + properties: + type: + type: string + minLength: 1 + maxLength: 50 + description: Payment method type (e.g. card) + id: + $ref: '#/components/schemas/PaymentMethodId' + description: Payment method ID to use for the retry (`pm_` or bare UUID — both accepted) + ProviderProxyResponse: + type: object + properties: + msg: + type: string + description: Human-readable message describing the result. + data: + type: object + properties: + provider_status: + type: integer + description: Upstream provider HTTP status. 200 (or other 2xx) on success; a 4xx when the provider semantically rejected the request. SDK callers must branch on this before treating HTTP 200 as success. + provider_response: + description: Raw upstream provider response body, passed through verbatim (Bridge payloads have `developer_reason` fields stripped). Any JSON value — usually an object, but a provider or intermediary gateway may answer (notably on 4xx/5xx) with a string (plain-text or HTML error page), an array, or an empty body (`null`). Shape varies by provider and endpoint — consult the provider's own API docs and always branch on `provider_status` first. + required: + - provider_status + - provider_response + required: + - msg + - data + MaintenanceResponse: + type: object + properties: + id: + type: string + example: abc123xyz + name: + type: string + example: Scheduled DB migration + scheduledFor: + type: string + format: date-time + example: '2026-07-20T02:00:00Z' + scheduledUntil: + type: string + format: date-time + example: '2026-07-20T03:00:00Z' CountryResponse: type: object description: | @@ -6757,6 +10590,24 @@ components: description: | True when CrowdSplit currently accepts this country for KYC (mutual Stripe ∩ Bridge support). + ProviderSnapshotData: + type: object + required: + - provider_response + - version + - retrieved_at + properties: + provider_response: + type: object + additionalProperties: true + description: The full untruncated provider snapshot for this (resource, id, version), curated to the resource's §2.4 allowlist with the same redaction policy as webhook payloads. + version: + $ref: '#/components/schemas/ResourceVersion' + description: Echoes the `version` path parameter. + retrieved_at: + type: string + format: date-time + description: Server-side time the snapshot was fetched. WebhookEnvelope: type: object description: | @@ -6765,16 +10616,17 @@ components: concrete event schemas compose this base via `allOf` and refine `event` / `category` to their `const` values and `data` to a specific schema. - Each delivery carries three headers: + Each delivery carries two headers: CrowdSplit-Signature `t={unix-ts},v1={hex-hmac-sha256}` CrowdSplit-Timestamp seconds since epoch - CrowdSplit-Notification-Id unique per delivery (idempotency) - Merchants MUST verify the HMAC of the raw body using the webhook - secret returned at registration, and SHOULD reject the delivery if - `CrowdSplit-Timestamp` differs from the current time by more than - 5 minutes. Duplicate `CrowdSplit-Notification-Id` values indicate a - retry of a previously dispatched delivery and should be deduplicated. + There is no notification-ID header — the notification ID is the + envelope's top-level `id` field, stable across retries of the same + logical event. Merchants MUST verify the HMAC of the raw body using + the webhook secret returned at registration, and SHOULD reject the + delivery if `CrowdSplit-Timestamp` differs from the current time by + more than 5 minutes. Duplicate envelope `id` values indicate a retry + of a previously dispatched delivery and should be deduplicated. required: - id - api_version @@ -6784,8 +10636,12 @@ components: properties: id: type: string - format: uuid - description: Unique notification ID (stable across retry attempts). + pattern: ^(evt_)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ + description: | + Notification ID per §1.4 — `evt_` prefix + UUID v4, stable across + retry attempts; the merchant dedup key. Transition: events emitted + before the prefix cutover carry the bare UUID form; both forms are + accepted wherever an id is supplied back. api_version: type: string pattern: ^v[0-9]+$ @@ -6832,6 +10688,30 @@ components: - failed - canceled_after_completion - canceled + TruncatedProviderResponse: + type: object + required: + - truncated + - version + - size_bytes + - full_resource_url + properties: + truncated: + type: boolean + enum: + - true + description: Discriminator — present (and true) only on the summary form. + version: + $ref: '#/components/schemas/ResourceVersion' + description: The resource version whose snapshot the URL serves — the version at which provider_response last changed. May be LOWER than the payload's own `version` (only other fields changed since); never higher. + size_bytes: + type: integer + minimum: 0 + description: Serialized size of the removed provider_response value. + full_resource_url: + type: string + format: uri + description: '`GET /api/v1//{id}/provider-snapshot/{version}` (§10h) — bearer-authenticated with your normal API token (not a presigned URL), multi-use within audit retention, 410 Gone after purge.' WebhookTransactionData: type: object description: Data payload for payment, refund, transfer, buy, sell, payout, and external events @@ -6844,13 +10724,27 @@ components: - updated_at properties: id: - type: string - format: uuid - description: Transaction unique ID + $ref: '#/components/schemas/TransactionId' + description: Transaction unique ID (prefixed by transaction type — `pay_`/`ref_`/`tr_`/`po_`/`buy_`). provider: $ref: '#/components/schemas/ProviderName' status: $ref: '#/components/schemas/TransactionStatus' + version: + $ref: '#/components/schemas/ResourceVersion' + changes: + type: array + description: | + Top-level fields whose value changed in the resource's most recent + state transition, computed server-side in the database and persisted + with the resource. Empty on the resource's first event; an + immaterial re-save or redelivery carries the prior diff forward + rather than resetting it. Absent when the server could not compute + the diff — treat an absent value as "anything may have changed", + never as "nothing changed". Use it to route handling (e.g. react + only to `status` transitions) without diffing full payloads. + items: + type: string source: type: object description: Source details (amount, currency, customer, payment_method). @@ -6865,20 +10759,23 @@ components: type: object properties: id: - type: string - format: uuid + $ref: '#/components/schemas/CustomerId' description: Customer (subject) unique ID. payment_method: type: object description: Payment method used on the source leg. Extra keys vary by provider. properties: id: - type: string - format: uuid + $ref: '#/components/schemas/PaymentMethodId' description: Payment method unique ID. type: type: string description: Payment method type (lowercased — card, pix, bank, …). + kind: + type: string + description: | + Payment method sub-type discriminator (lowercased). Canonical + alias of `type`; emitted alongside `type` with the same value. chain: type: string description: Blockchain network when the method is on-chain (lowercased). @@ -6905,9 +10802,15 @@ components: type: string description: Sequence strategy identifier (lowercased). additionalProperties: true + additionalProperties: true destination: type: object - description: Destination details (if applicable). + description: | + Destination details (if applicable). Shares the same shape as + `source` because the wire serialization reuses one TS type + (`WebhookTransactionLeg`) for both legs — `capture_method` / + `fraud_check` are uncommon on a destination but can appear when + the merchant supplies them in the request body. nullable: true properties: amount: @@ -6920,23 +10823,34 @@ components: type: object properties: id: - type: string - format: uuid + $ref: '#/components/schemas/CustomerId' description: Destination customer (subject) unique ID. payment_method: type: object description: Payment method used on the destination leg. properties: id: - type: string - format: uuid + $ref: '#/components/schemas/PaymentMethodId' type: type: string description: Payment method type (lowercased). + kind: + type: string + description: | + Payment method sub-type discriminator (lowercased). Canonical + alias of `type`; emitted alongside `type` with the same value. chain: type: string description: Blockchain network (lowercased). additionalProperties: true + capture_method: + type: string + description: Capture strategy (lowercased). Rarely set on destination. + fraud_check: + type: object + description: Fraud-check details. Rarely set on destination. + additionalProperties: true + additionalProperties: true fee: type: object nullable: true @@ -6957,12 +10871,22 @@ components: type: string description: Fee currency (lowercased). additionalProperties: true + application_fee_amount: + type: integer + description: | + Platform fee taken from the charge, in the charge currency's + minor units. Echoed back from the request body; present only + when the merchant supplied one. reference_transaction_id: type: - string - 'null' - format: uuid - description: Parent transaction ID (for refunds) + pattern: ^(pay_)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ + description: | + Parent payment ID (for refunds). Emitted as `pay_` (bare + UUID possible while prefixed IDs roll out); both forms are + accepted wherever this value is sent back as input. + example: pay_a4c7e1f2-9b53-4d62-b0d4-3f8e2c5a1b9d total_installments: type: - integer @@ -6996,13 +10920,21 @@ components: Shape is defined by the merchant at request time. additionalProperties: true provider_response: - type: object - nullable: true description: | Raw excerpt of the upstream provider's response. Key set varies by provider and by call — do not rely on specific keys. Useful for debugging and for forwarding provider-specific error codes. - additionalProperties: true + + When the serialized payload would exceed the §2.9.3 256KB cap, + this field alone is replaced by the truncated summary form + (`truncated: true`) — follow its `full_resource_url` to retrieve + the full snapshot. Documented top-level fields are never affected. + oneOf: + - type: object + nullable: true + additionalProperties: true + description: Full provider excerpt (normal case). + - $ref: '#/components/schemas/TruncatedProviderResponse' created_at: type: string format: date-time @@ -7017,11 +10949,25 @@ components: - status properties: id: - type: string - format: uuid + $ref: '#/components/schemas/DisputeId' description: Dispute unique ID status: type: string + version: + $ref: '#/components/schemas/ResourceVersion' + changes: + type: array + description: | + Top-level fields whose value changed in the resource's most recent + state transition, computed server-side in the database and persisted + with the resource. Empty on the resource's first event; an + immaterial re-save or redelivery carries the prior diff forward + rather than resetting it. Absent when the server could not compute + the diff — treat an absent value as "anything may have changed", + never as "nothing changed". Use it to route handling (e.g. react + only to `status` transitions) without diffing full payloads. + items: + type: string evidence_due_by: type: - string @@ -7039,10 +10985,13 @@ components: payment: type: object description: Associated payment transaction (summary fields only). + required: + - id + - type + - status properties: id: - type: string - format: uuid + $ref: '#/components/schemas/PaymentId' description: Payment transaction UID. type: type: string @@ -7050,6 +10999,31 @@ components: status: type: string description: Payment's current status (lowercased). + reference_transaction: + type: object + description: | + Point-in-time snapshot of the disputed payment (id, status, source, + destination, …). Canonical nested reference; emitted alongside + `payment` with the same value. For live state call `GET /payments/{id}`. + + The nested `provider_response` is subject to §2.9.3 truncation like + any other: on oversized dispute payloads both nested snapshots carry + the truncated summary (`truncated: true`) pointing at the payment's + own provider snapshot. + additionalProperties: true + properties: + id: + $ref: '#/components/schemas/PaymentId' + status: + type: string + type: + type: string + provider_response: + oneOf: + - type: object + nullable: true + additionalProperties: true + - $ref: '#/components/schemas/TruncatedProviderResponse' NormalizedFailureReason: type: object description: | @@ -7094,9 +11068,8 @@ components: - updated_at properties: id: - type: string - format: uuid - description: Customer unique ID + $ref: '#/components/schemas/CustomerId' + description: Customer unique ID (carries the customer `cus_` prefix) status: type: string description: Registration status (lowercased) @@ -7106,12 +11079,20 @@ components: type: string description: Target role (lowercased) provider_response: - type: object - nullable: true description: | Raw excerpt of the provider's registration response. Shape varies by provider — do not rely on specific keys. - additionalProperties: true + + When the serialized payload would exceed the §2.9.3 256KB cap, + this field alone is replaced by the truncated summary form + (`truncated: true`) — follow its `full_resource_url` to retrieve + the full snapshot. + oneOf: + - type: object + nullable: true + additionalProperties: true + description: Full provider excerpt (normal case). + - $ref: '#/components/schemas/TruncatedProviderResponse' failure_reasons: type: - array @@ -7127,6 +11108,8 @@ components: - string - 'null' description: Provider readiness status + version: + $ref: '#/components/schemas/ResourceVersion' changes: type: - array @@ -7152,6 +11135,7 @@ components: updated_at: type: string format: date-time + additionalProperties: true WebhookCustomerSyncData: type: object description: Data payload for customer sync events @@ -7162,8 +11146,7 @@ components: - attempt_id properties: id: - type: string - format: uuid + $ref: '#/components/schemas/CustomerId' description: Customer unique ID provider: $ref: '#/components/schemas/ProviderName' @@ -7175,6 +11158,21 @@ components: attempt_id: type: string description: Sync attempt identifier + version: + $ref: '#/components/schemas/ResourceVersion' + changes: + type: array + description: | + Top-level fields whose value changed in the resource's most recent + state transition, computed server-side in the database and persisted + with the resource. Empty on the resource's first event; an + immaterial re-save or redelivery carries the prior diff forward + rather than resetting it. Absent when the server could not compute + the diff — treat an absent value as "anything may have changed", + never as "nothing changed". Use it to route handling (e.g. react + only to `status` transitions) without diffing full payloads. + items: + type: string synced_at: type: - string @@ -7220,17 +11218,30 @@ components: - updated_at properties: id: - type: string - format: uuid + $ref: '#/components/schemas/PaymentMethodId' description: Payment method unique ID customer_id: - type: string - format: uuid + $ref: '#/components/schemas/CustomerId' provider: $ref: '#/components/schemas/ProviderName' status: type: string description: Payment method status (lowercased) + version: + $ref: '#/components/schemas/ResourceVersion' + changes: + type: array + description: | + Top-level fields whose value changed in the resource's most recent + state transition, computed server-side in the database and persisted + with the resource. Empty on the resource's first event; an + immaterial re-save or redelivery carries the prior diff forward + rather than resetting it. Absent when the server could not compute + the diff — treat an absent value as "anything may have changed", + never as "nothing changed". Use it to route handling (e.g. react + only to `status` transitions) without diffing full payloads. + items: + type: string type: $ref: '#/components/schemas/PaymentMethodType' country: @@ -7277,6 +11288,51 @@ components: type: string format: date-time additionalProperties: true + WebhookSubscriptionData: + type: object + description: Data payload for subscription lifecycle events + required: + - id + - plan_id + - status + - source_customer_id + properties: + id: + type: string + format: uuid + description: Subscription unique ID + plan_id: + type: string + format: uuid + description: Associated plan ID + status: + type: string + enum: + - queued + - pending_activation + - active + - canceled + - expired + - failed + description: Current subscription status + start_time: + type: + - string + - 'null' + format: date-time + description: Subscription start time + end_time: + type: + - string + - 'null' + format: date-time + description: Subscription end time + auto_renew: + type: boolean + description: Whether the subscription auto-renews + source_customer_id: + type: string + description: Customer being charged responses: ValidationError: description: Request validation failed @@ -7285,7 +11341,17 @@ components: schema: $ref: '#/components/schemas/ApiErrorResponse' example: - msg: Validation error message + msg: Required + error_code: validation_error + failure_reasons: + - code: missing_required_information + message: Required + fields: + - source.amount + deadline: null + provider_code: null + requires_manual_review: false + support_reference: null data: null UnauthorizedError: description: Authentication credentials are missing or invalid @@ -7294,7 +11360,16 @@ components: schema: $ref: '#/components/schemas/ApiErrorResponse' example: - msg: Unauthorized + msg: Token is Missing! + error_code: authentication_error + failure_reasons: + - code: missing_token + message: null + fields: [] + deadline: null + provider_code: null + requires_manual_review: false + support_reference: null data: null ForbiddenError: description: Authenticated but not authorized for this resource @@ -7303,7 +11378,16 @@ components: schema: $ref: '#/components/schemas/ApiErrorResponse' example: - msg: Forbidden + msg: You don't have access + error_code: authorization_error + failure_reasons: + - code: insufficient_permissions + message: Insufficient permissions + fields: [] + deadline: null + provider_code: null + requires_manual_review: false + support_reference: null data: null NotFoundError: description: Requested resource not found @@ -7313,6 +11397,138 @@ components: $ref: '#/components/schemas/ApiErrorResponse' example: msg: Resource not found + error_code: not_found + failure_reasons: [] + data: null + ConflictError: + description: State conflict (e.g. Idempotency-Key reuse with a different body) + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + example: + msg: This Idempotency-Key was already used with a different request body. + error_code: conflict + failure_reasons: + - code: idempotency_mismatch + message: This Idempotency-Key was already used with a different request body. + fields: [] + deadline: null + provider_code: null + requires_manual_review: false + support_reference: null + data: null + PayloadTooLargeError: + description: Body-size violation (file upload >10 MB, proxy body >256 KB) + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + example: + msg: File too large + error_code: validation_error + failure_reasons: + - code: payload_too_large + message: null + fields: + - file + deadline: null + provider_code: null + requires_manual_review: false + support_reference: null + data: null + RateLimitedError: + description: Per-merchant rate limit exceeded. Carries Retry-After and X-RateLimit-* headers (§1.10). + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + example: + msg: Rate limit exceeded. Retry after the interval indicated by the Retry-After header. + error_code: rate_limited + failure_reasons: + - code: rate_limit_exceeded + message: null + fields: [] + deadline: null + provider_code: null + requires_manual_review: false + support_reference: null + data: null + InternalServerError: + description: Platform-side error. Retryable with the same Idempotency-Key (§1.9); support_reference correlates with server logs. + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + example: + msg: Internal server error. + error_code: internal_error + failure_reasons: + - code: internal_error + message: null + fields: [] + deadline: null + provider_code: null + requires_manual_review: true + support_reference: + data: null + ProviderError: + description: Upstream provider returned an error (§1.8). data.provider_response carries a curated upstream snapshot. + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + example: + msg: Stripe rejected the payment intent + error_code: provider_error + failure_reasons: + - code: card_declined + message: Your card was declined. + fields: + - source.payment_method + deadline: null + provider_code: card_declined + requires_manual_review: false + support_reference: null + data: + provider_response: + decline_code: generic_decline + ProviderUnavailableError: + description: Upstream provider unreachable (503) or timed out (504). Transport failure — retry with the same Idempotency-Key (§1.9). + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + example: + msg: Bridge did not respond within the configured timeout. + error_code: provider_unavailable + failure_reasons: + - code: provider_unavailable + message: null + fields: [] + deadline: null + provider_code: null + requires_manual_review: false + support_reference: null + data: null + GoneError: + description: 'Resource existed but has been purged — §10h provider snapshots outside the §1.12 audit-retention window (non-financial: 1 year). The resource itself may still exist via its per-resource GET; only the snapshot at that specific version is gone.' + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorResponse' + example: + msg: Provider snapshot purged. + error_code: gone + failure_reasons: + - code: snapshot_purged + message: Snapshot for this version has been purged per audit retention policy + fields: [] + deadline: null + provider_code: null + requires_manual_review: false + support_reference: null data: null parameters: RefAppKeyHeader: @@ -7335,11 +11551,75 @@ components: name: limit in: query required: false - description: Number of records to return per page + description: Page size. Default 10, maximum 100 (larger values are clamped). schema: type: integer minimum: 1 + maximum: 100 default: 10 + StartingAfterQuery: + name: starting_after + in: query + required: false + description: | + Cursor — the `id` of a record from a previous page (typically the last + item's id). Returns records after it in the list order + (`created_at DESC, id DESC`; `updated_at DESC, id DESC` when an + `updated_at[*]` filter is present). The cursor record itself is excluded. + Mutually exclusive with `ending_before`. Treated as an opaque token — + an id that doesn't resolve to a record fails with `invalid_cursor`. + schema: + type: string + EndingBeforeQuery: + name: ending_before + in: query + required: false + description: | + Cursor — the `id` of a record from a previous page (typically the first + item's id). Returns the records immediately preceding it in the list + order. The cursor record itself is excluded. Mutually exclusive with + `starting_after`. Treated as an opaque token — an id that doesn't + resolve to a record fails with `invalid_cursor`. + schema: + type: string + CreatedAtGteQuery: + name: created_at[gte] + in: query + required: false + description: Only records created at or after this RFC 3339 timestamp. + schema: + type: string + format: date-time + CreatedAtLteQuery: + name: created_at[lte] + in: query + required: false + description: Only records created at or before this RFC 3339 timestamp. + schema: + type: string + format: date-time + UpdatedAtGteQuery: + name: updated_at[gte] + in: query + required: false + description: | + Only records updated at or after this RFC 3339 timestamp. Any + `updated_at[*]` filter switches the list to reconciliation mode — + ordering and cursors anchor on `updated_at` instead of `created_at`. + Use for post-outage "everything changed since" walks. + schema: + type: string + format: date-time + UpdatedAtLteQuery: + name: updated_at[lte] + in: query + required: false + description: | + Only records updated at or before this RFC 3339 timestamp. Triggers + reconciliation mode like `updated_at[gte]`. + schema: + type: string + format: date-time OffsetQuery: name: offset in: query @@ -7370,17 +11650,6 @@ components: schema: type: integer example: 1713456789 - CrowdSplitNotificationIdHeader: - name: CrowdSplit-Notification-Id - in: header - required: true - description: | - Unique delivery ID. Stable across retries of the same logical event - — use it to deduplicate idempotently on receipt. - schema: - type: string - format: uuid - example: a1b2c3d4-e5f6-7890-abcd-ef1234567890 headers: {} x-tagGroups: - name: Authentication @@ -7407,6 +11676,7 @@ x-tagGroups: tags: - Transactions - Disputes + - Provider Snapshots - Tax - Files - Generic