openapi: 3.1.0

info:
  title: Silora Partner API
  version: "1.0.0"
  summary: Connect once. Access every global payment network.
  description: |
    The public Silora Partner API. JSON only, modelled on ISO 20022.

    Silora is an **enterprise payment connectivity platform**. It does not move
    money and does not act as a PSP. Customers hold their own commercial,
    compliance and prefunding relationships with each payment provider; Silora
    instructs, tracks and records.

    ## Conventions

    * **Amounts are strings, never JSON numbers.** `"value": "24500.00"`.
      IEEE-754 doubles silently corrupt money.
    * **Dates** are `YYYY-MM-DD`; **timestamps** are RFC 3339 in UTC.
    * **Repeating elements are always arrays**, even at length 1.
    * **Codes are ISO 20022 external code values**, validated against the
      quarterly code set rather than a hardcoded enum.
    * `uetr` is the identity of a payment and the key on every path, event,
      webhook and trace. `endToEndId` is *your* reference and is never a path key.

    ## Nothing provider-specific inbound

    A customer may **name** a rail via `provider`; it never **describes** one.
    Funding accounts, wallet addresses, chains, token contracts and FX rates are
    execution facts resolved by Silora, never request fields.

    ## The shape of an integration

    1. `GET /corridors/{pair}/requirements` — what does this corridor demand?
    2. `POST /beneficiaries` — store the payee once
    3. `POST /beneficiaries/{id}/validate` — confirm the account before paying it
    4. `POST /payments/quote` — which provider, what rate, what fee
    5. `POST /payments` — returns `202` with an initial status
    6. Webhook `payment.settled` or `payment.failed` delivers the outcome
  contact:
    name: Silora Developer Support
    url: https://docs.silorapay.ai
    email: developers@silorapay.ai
  license:
    name: Proprietary
    url: https://silorapay.ai/terms

servers:
  - url: https://api.silorapay.ai/v1
    description: Production
  - url: https://api.sandbox.silorapay.ai/v1
    description: Sandbox

security:
  - ApiKey: []
    Signature: []
    Timestamp: []

tags:
  - name: Corridors
    description: What a corridor requires before you build a payload.
  - name: Beneficiaries
    description: Store payees once; validate them before money moves.
  - name: Payments
    description: Create, track and act on payments.
  - name: Quotes
    description: Resolve routing, FX and fees without creating anything.
  - name: Providers
    description: Provider catalogue, capability and funding balances.
  - name: Documents
    description: Supporting documents for corridors that require them.
  - name: Webhooks
    description: Endpoint management and delivery replay.

x-webhooks-note: |
  Webhook payload schemas are defined under the top-level `webhooks` object.

# ─────────────────────────────────────────────────────────────── paths
paths:

  /corridors:
    get:
      tags: [Corridors]
      operationId: listCorridors
      summary: List corridors you can serve
      parameters:
        - $ref: '#/components/parameters/CorrelationId'
      responses:
        '200':
          description: Corridors available to this client.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Corridor' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /corridors/{pair}/requirements:
    get:
      tags: [Corridors]
      operationId: getCorridorRequirements
      summary: Which fields this corridor demands
      description: |
        Cross-border corridors differ enormously in what they require. Rather
        than documenting that in prose and letting you discover it through
        rejections, the API tells you. Cache the response; `ETag` is supported.
      parameters:
        - name: pair
          in: path
          required: true
          description: Corridor as `{fromCountry}-{toCountry}`, ISO 3166-1 alpha-2.
          schema: { type: string, pattern: '^[A-Z]{2}-[A-Z]{2}$' }
          example: US-IN
        - name: currency
          in: query
          schema: { $ref: '#/components/schemas/CurrencyCode' }
          example: USD
        - name: amount
          in: query
          description: Some requirements are threshold-dependent.
          schema: { $ref: '#/components/schemas/DecimalString' }
          example: "24500.00"
        - name: creditorType
          in: query
          schema: { $ref: '#/components/schemas/PartyType' }
        - $ref: '#/components/parameters/CorrelationId'
      responses:
        '200':
          description: The conditional field schema for this corridor.
          headers:
            ETag: { schema: { type: string } }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CorridorRequirements' }
              example:
                corridor: US-IN
                currency: USD
                supported: true
                fields:
                  - path: debtor.identification
                    requirement: REQUIRED
                    reason: FATF R.16 — one of nationalId, taxId, customerId, or dateAndPlaceOfBirth
                  - path: creditorAgent.clearingSystemMemberId
                    requirement: REQUIRED
                    label: IFSC code
                    pattern: '^[A-Z]{4}0[A-Z0-9]{6}$'
                  - path: purpose
                    requirement: REQUIRED
                    codeList: IN_RBI_PURPOSE
                    codeListUrl: /v1/code-lists/IN_RBI_PURPOSE
                  - path: documents
                    requirement: CONDITIONAL
                    condition: purpose in [P0103, P0104] and amount > 25000
                    reason: Import documentation
                limits:
                  minAmount: { currency: USD, value: "1.00" }
                  maxAmount: { currency: USD, value: "1000000.00" }
                  cutOff: "14:30"
                  cutOffTimezone: America/New_York
                settlement: { typical: same-day, window: T+0 to T+1 }
                providers: [BRIDGE, XBS, NIUM]
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /code-lists/{listId}:
    get:
      tags: [Corridors]
      operationId: getCodeList
      summary: A local purpose or reporting code list
      parameters:
        - name: listId
          in: path
          required: true
          schema: { type: string }
          example: IN_RBI_PURPOSE
        - $ref: '#/components/parameters/CorrelationId'
      responses:
        '200':
          description: The code list.
          content:
            application/json:
              schema:
                type: object
                required: [listId, codes]
                properties:
                  listId: { type: string }
                  authority: { type: string }
                  updatedAt: { type: string, format: date-time }
                  codes:
                    type: array
                    items:
                      type: object
                      required: [code, name]
                      properties:
                        code: { type: string, example: P0103 }
                        name: { type: string, example: Import of goods }
                        description: { type: string }
        '404': { $ref: '#/components/responses/NotFound' }

  /beneficiaries:
    get:
      tags: [Beneficiaries]
      operationId: listBeneficiaries
      summary: List and search beneficiaries
      parameters:
        - name: q
          in: query
          description: Free-text search across name, reference and account.
          schema: { type: string }
        - name: corridor
          in: query
          schema: { type: string, pattern: '^[A-Z]{2}-[A-Z]{2}$' }
        - name: status
          in: query
          schema: { $ref: '#/components/schemas/BeneficiaryStatus' }
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/CorrelationId'
      responses:
        '200':
          description: A page of beneficiaries.
          content:
            application/json:
              schema:
                type: object
                required: [data, page]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Beneficiary' }
                  page: { $ref: '#/components/schemas/Page' }
        '401': { $ref: '#/components/responses/Unauthorized' }

    post:
      tags: [Beneficiaries]
      operationId: createBeneficiary
      summary: Store a payee
      description: |
        Storing a payee removes the largest, most error-prone part of the payment
        payload and lets you validate the account before you ever move money.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/CorrelationId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BeneficiaryRequest' }
            example:
              reference: MERIDIAN-TEXTILES
              type: ORGA
              name: Meridian Textiles Pvt Ltd
              address:
                streetName: Trade Centre
                buildingNumber: "402"
                postCode: "400051"
                townName: Mumbai
                countrySubDivision: Maharashtra
                country: IN
              identification: { taxId: AAACM1234F }
              contact: { email: ap@meridiantextiles.in, phone: "+912266778899" }
              account: { scheme: IN_ACCOUNT, id: "50100234567890", currency: INR }
              agent:
                clearingSystem: INIFSC
                clearingSystemMemberId: HDFC0000123
                bic: HDFCINBBXXX
                country: IN
              defaultPurpose: GDDS
      responses:
        '201':
          description: Beneficiary stored.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Beneficiary' }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '409': { $ref: '#/components/responses/Conflict' }

  /beneficiaries/{beneficiaryId}:
    parameters:
      - $ref: '#/components/parameters/BeneficiaryId'
    get:
      tags: [Beneficiaries]
      operationId: getBeneficiary
      summary: Read a beneficiary
      responses:
        '200':
          description: The beneficiary. Account numbers are masked.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Beneficiary' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Beneficiaries]
      operationId: updateBeneficiary
      summary: Update a beneficiary
      description: Changing the account or agent resets `status` to `UNVALIDATED`.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BeneficiaryRequest' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Beneficiary' }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Beneficiaries]
      operationId: deactivateBeneficiary
      summary: Deactivate a beneficiary
      description: Soft delete. Historic payments keep their reference.
      responses:
        '204': { description: Deactivated. }
        '404': { $ref: '#/components/responses/NotFound' }

  /beneficiaries/{beneficiaryId}/validate:
    post:
      tags: [Beneficiaries]
      operationId: validateBeneficiary
      summary: Confirmation of Payee
      description: |
        Checks the account exists and the name matches **before** money moves.
        This is the single largest lever on your `AC03` rejection rate.

        Validation does not block payment — you may pay an unvalidated
        beneficiary — but the result is recorded on every payment made to it.
      parameters:
        - $ref: '#/components/parameters/BeneficiaryId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                amount: { $ref: '#/components/schemas/Amount' }
      responses:
        '200':
          description: Validation result.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BeneficiaryValidation' }
              example:
                beneficiaryId: ben_01J8M4TQ7X2K9
                result: MATCH
                nameMatch: FULL
                accountStatus: ACTIVE
                accountHolderName: MERIDIAN TEXTILES PRIVATE LIMITED
                validatedBy: nium
                validatedAt: "2026-08-22T09:02:40Z"
                expiresAt: "2026-09-21T09:02:40Z"
        '404': { $ref: '#/components/responses/NotFound' }

  /payments/quote:
    post:
      tags: [Quotes]
      operationId: quotePayment
      summary: Resolve provider, FX and fees without creating anything
      description: |
        Takes the same body as `POST /payments`. Synchronous. Send `hold: true`
        to hold the rate where the resolved provider supports it, then pass the
        returned `quoteId` in `fx.quoteId` on the payment.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyOptional'
        - $ref: '#/components/parameters/CorrelationId'
      requestBody:
        required: true
        content:
          application/vnd.silora.simple+json:
            schema:
              allOf:
                - $ref: '#/components/schemas/PaymentRequest'
                - type: object
                  properties:
                    hold: { type: boolean, default: false }
      responses:
        '200':
          description: The resolution. Nothing was created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Quote' }
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '422': { $ref: '#/components/responses/Rejected' }

  /payments:
    post:
      tags: [Payments]
      operationId: createPayment
      summary: Create a payment
      description: |
        **Asynchronous.** Acceptance is not execution. A `202` means *accepted
        for processing*, not *paid* — screening, routing and submission happen
        after the response, and settlement never resolves on the same call. The
        outcome arrives by webhook.

        Two content types carry the same model: the Simple usage profile, and the
        full ISO 20022 `pain.001` body. There is no XML endpoint.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/CorrelationId'
      requestBody:
        required: true
        content:
          application/vnd.silora.simple+json:
            schema: { $ref: '#/components/schemas/PaymentRequest' }
            examples:
              storedBeneficiary:
                summary: With a stored beneficiary — the recommended form
                value:
                  endToEndId: INV-2026-4471
                  provider: AUTO
                  instructedAmount: { currency: USD, value: "24500.00" }
                  debtorAccount: { scheme: US_ACCOUNT, id: "8901234567" }
                  beneficiaryId: ben_01J8M4TQ7X2K9
                  purpose: P0103
                  remittanceInformation: { unstructured: [Invoice INV-2026-4471] }
                  requestedExecutionDate: "2026-08-22"
                  metadata: { costCentre: CC-EMEA-07, poNumber: PO-88213 }
              inline:
                summary: Full inline form
                value:
                  endToEndId: INV-2026-4471
                  provider: AUTO
                  instructedAmount: { currency: USD, value: "24500.00" }
                  execution: { requestedExecutionDate: "2026-08-22", priority: NORM, serviceLevel: G001 }
                  charges: { bearer: SHAR, fxSpreadBearer: DEBT }
                  debtor:
                    type: ORGA
                    name: Bank of NewYork
                    address: { streetName: Wall Street, buildingNumber: "240", postCode: "10286", townName: New York, countrySubDivision: NY, country: US }
                    identification: { lei: HPFHU0OQ28E4N0NFVK49 }
                  debtorAccount: { scheme: US_ACCOUNT, id: "8901234567", currency: USD }
                  creditor:
                    type: ORGA
                    name: Meridian Textiles Pvt Ltd
                    address: { streetName: Trade Centre, buildingNumber: "402", postCode: "400051", townName: Mumbai, country: IN }
                    identification: { taxId: AAACM1234F }
                  creditorAccount: { scheme: IN_ACCOUNT, id: "50100234567890", currency: INR }
                  creditorAgent: { clearingSystem: INIFSC, clearingSystemMemberId: HDFC0000123, bic: HDFCINBBXXX }
                  purpose: P0103
                  regulatoryReporting:
                    - authority: { name: Reserve Bank of India, country: IN }
                      details:
                        - type: PURPOSE
                          code: P0103
                          amount: { currency: USD, value: "24500.00" }
                          information: [Import of textiles]
                  remittanceInformation:
                    unstructured: [Invoice INV-2026-4471]
                    structured:
                      - referredDocument:
                          - type: CINV
                            number: INV-2026-4471
                            relatedDate: "2026-08-01"
                            amount: { currency: USD, value: "24500.00" }
                        creditorReference: { type: SCOR, reference: RF18539007547034 }
          application/json:
            schema:
              type: object
              description: Full ISO 20022 `pain.001` CustomerCreditTransferInitiation, JSON binding.
              required: [CstmrCdtTrfInitn]
              properties:
                CstmrCdtTrfInitn: { type: object }
      responses:
        '202':
          description: Accepted for processing.
          headers:
            X-Silora-Uetr: { schema: { type: string, format: uuid } }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PaymentAccepted' }
              example:
                uetr: 7a9c1e02-4f3b-4c8e-9d21-6b0f5a8e33c1
                endToEndId: INV-2026-4471
                createdAt: "2026-08-22T09:14:03Z"
                status: INITIATED
                isoStatus: RCVD
                instructedAmount: { currency: USD, value: "24500.00" }
                corridor: { from: US, to: IN }
                beneficiaryId: ben_01J8M4TQ7X2K9
                accepted:
                  validation: PASSED
                  corridorRequirements: SATISFIED
                  beneficiaryValidation: { result: MATCH, validatedAt: "2026-08-22T09:02:40Z" }
                execution: null
        '400': { $ref: '#/components/responses/ValidationFailed' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/Rejected' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/ProviderUnavailable' }

  /payments/{uetr}:
    get:
      tags: [Payments]
      operationId: getPayment
      summary: Full current state of a payment
      parameters:
        - $ref: '#/components/parameters/Uetr'
        - $ref: '#/components/parameters/CorrelationId'
      responses:
        '200':
          description: The payment. Account numbers are masked.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Payment' }
        '404': { $ref: '#/components/responses/NotFound' }

  /payments/{uetr}/timeline:
    get:
      tags: [Payments]
      operationId: getPaymentTimeline
      summary: Lifecycle events for a payment
      parameters:
        - $ref: '#/components/parameters/Uetr'
      responses:
        '200':
          description: Ordered lifecycle events.
          content:
            application/json:
              schema:
                type: object
                required: [uetr, events]
                properties:
                  uetr: { type: string, format: uuid }
                  events:
                    type: array
                    items: { $ref: '#/components/schemas/TimelineEvent' }
        '404': { $ref: '#/components/responses/NotFound' }

  /payments/{uetr}/retry:
    post:
      tags: [Payments]
      operationId: retryPayment
      summary: Re-attempt a failed payment
      description: |
        Same `uetr`, new `transactionId`, new provider attempt key. Rejected
        `409` if the payment is not retryable, or if the failure was terminal
        (`AC03`, `AC04`, `RR04`, `AM02`).
      parameters:
        - $ref: '#/components/parameters/Uetr'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string, maxLength: 256 }
      responses:
        '202':
          description: Retry accepted.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PaymentAccepted' }
        '409': { $ref: '#/components/responses/Conflict' }

  /payments/{uetr}/reroute:
    post:
      tags: [Payments]
      operationId: reroutePayment
      summary: Re-attempt on a different provider
      description: |
        Same `uetr`, different `providerId`. If the payment carried an explicit
        `provider`, every subsequent read shows `providerPreference` diverging
        from `execution.providerId`, and the override is written to the audit
        trail.
      parameters:
        - $ref: '#/components/parameters/Uetr'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [provider]
              properties:
                provider: { $ref: '#/components/schemas/ProviderCode' }
                reason: { type: string, maxLength: 256 }
      responses:
        '202':
          description: Re-route accepted.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PaymentAccepted' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/Rejected' }

  /payments/{uetr}/cancel:
    post:
      tags: [Payments]
      operationId: cancelPayment
      summary: Cancel, or request cancellation
      description: |
        Before `SUBMITTED` this cancels outright. After submission it becomes a
        cancellation *request* to the provider (`camt.056`) whose outcome is not
        guaranteed; `cancellationType` in the response says which happened and
        the resolution arrives by webhook.
      parameters:
        - $ref: '#/components/parameters/Uetr'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reason]
              properties:
                reason:
                  type: string
                  description: ISO cancellation reason code.
                  enum: [CUST, DUPL, TECH, FRAD, NARR]
                detail: { type: string, maxLength: 256 }
      responses:
        '202':
          description: Cancelled, or cancellation requested.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaymentAccepted'
                  - type: object
                    properties:
                      cancellationType:
                        type: string
                        enum: [CANCELLED, REQUESTED]
        '409': { $ref: '#/components/responses/Conflict' }

  /query/payments:
    get:
      tags: [Payments]
      operationId: queryPayments
      summary: Paged, filtered list of payments
      description: |
        Cursor pagination, not offset — the ledger is append-heavy and offsets
        drift. `totals` covers the whole filtered set, not the page.
        `Accept: text/csv` streams an export.
      parameters:
        - name: status
          in: query
          schema: { $ref: '#/components/schemas/PaymentStatus' }
        - name: from
          in: query
          schema: { type: string, format: date }
        - name: to
          in: query
          schema: { type: string, format: date }
        - name: updatedSince
          in: query
          description: Reconciliation fallback when webhook delivery has failed.
          schema: { type: string, format: date-time }
        - name: corridor
          in: query
          schema: { type: string, pattern: '^[A-Z]{2}-[A-Z]{2}$' }
        - name: provider
          in: query
          schema: { type: string }
        - name: currency
          in: query
          schema: { $ref: '#/components/schemas/CurrencyCode' }
        - name: minAmount
          in: query
          schema: { $ref: '#/components/schemas/DecimalString' }
        - name: beneficiaryId
          in: query
          schema: { type: string }
        - name: q
          in: query
          schema: { type: string }
        - name: sort
          in: query
          schema: { type: string, default: '-createdAt' }
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: A page of payments.
          content:
            application/json:
              schema:
                type: object
                required: [data, page]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/PaymentSummary' }
                  page: { $ref: '#/components/schemas/Page' }
                  totals:
                    type: object
                    properties:
                      count: { type: integer }
                      volume: { $ref: '#/components/schemas/Amount' }
            text/csv:
              schema: { type: string }

  /providers:
    get:
      tags: [Providers]
      operationId: listProviders
      summary: Provider catalogue and your subscriptions
      responses:
        '200':
          description: Providers.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Provider' }

  /providers/capability-matrix:
    get:
      tags: [Providers]
      operationId: getCapabilityMatrix
      summary: Which providers can serve a corridor
      parameters:
        - name: corridor
          in: query
          required: true
          schema: { type: string, pattern: '^[A-Z]{2}-[A-Z]{2}$' }
        - name: currency
          in: query
          schema: { $ref: '#/components/schemas/CurrencyCode' }
        - name: amount
          in: query
          schema: { $ref: '#/components/schemas/DecimalString' }
      responses:
        '200':
          description: Capability per provider.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CapabilityMatrix' }

  /ledger/accounts:
    get:
      tags: [Providers]
      operationId: listFundingAccounts
      summary: Funding balances you hold at each provider
      description: |
        These are balances **you** hold at each provider, read by Silora.
        Silora takes no custody.
      responses:
        '200':
          description: Funding accounts.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/FundingAccount' }

  /documents:
    post:
      tags: [Documents]
      operationId: uploadDocument
      summary: Upload a supporting document
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file, type]
              properties:
                file: { type: string, format: binary }
                type:
                  type: string
                  enum: [COMMERCIAL_INVOICE, CONTRACT, CUSTOMS_DECLARATION, TAX_CERTIFICATE, OTHER]
                description: { type: string, maxLength: 256 }
      responses:
        '201':
          description: Stored.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Document' }
        '413': { $ref: '#/components/responses/ValidationFailed' }

  /webhooks/endpoints:
    get:
      tags: [Webhooks]
      operationId: listWebhookEndpoints
      summary: List webhook endpoints
      responses:
        '200':
          description: Endpoints.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/WebhookEndpoint' }
    post:
      tags: [Webhooks]
      operationId: createWebhookEndpoint
      summary: Register a webhook endpoint
      description: The response returns `secret` **once**. Store it; it is the HMAC key.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url: { type: string, format: uri }
                events:
                  type: array
                  minItems: 1
                  items: { $ref: '#/components/schemas/WebhookEvent' }
                description: { type: string, maxLength: 256 }
      responses:
        '201':
          description: Registered.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/WebhookEndpoint'
                  - type: object
                    required: [secret]
                    properties:
                      secret:
                        type: string
                        description: Returned once, never again.

  /webhooks/endpoints/{endpointId}:
    delete:
      tags: [Webhooks]
      operationId: deleteWebhookEndpoint
      summary: Remove a webhook endpoint
      parameters:
        - name: endpointId
          in: path
          required: true
          schema: { type: string }
      responses:
        '204': { description: Removed. }
        '404': { $ref: '#/components/responses/NotFound' }

  /webhooks/endpoints/{endpointId}/deliveries:
    get:
      tags: [Webhooks]
      operationId: listWebhookDeliveries
      summary: Delivery log for an endpoint
      parameters:
        - name: endpointId
          in: path
          required: true
          schema: { type: string }
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Deliveries.
          content:
            application/json:
              schema:
                type: object
                required: [data, page]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/WebhookDelivery' }
                  page: { $ref: '#/components/schemas/Page' }

  /webhooks/endpoints/{endpointId}/deliveries/{deliveryId}/replay:
    post:
      tags: [Webhooks]
      operationId: replayWebhookDelivery
      summary: Replay a delivery
      parameters:
        - name: endpointId
          in: path
          required: true
          schema: { type: string }
        - name: deliveryId
          in: path
          required: true
          schema: { type: string }
        - $ref: '#/components/parameters/IdempotencyKeyOptional'
      responses:
        '202': { description: Replay queued. }
        '404': { $ref: '#/components/responses/NotFound' }

# ─────────────────────────────────────────────────────────────── webhooks
webhooks:

  payment.settled:
    post:
      operationId: onPaymentSettled
      summary: A payment settled — terminal
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PaymentWebhook' }
            example:
              event: payment.settled
              eventId: evt_01J8KXB4N2M7Q
              occurredAt: "2026-08-22T15:52:08Z"
              uetr: 7a9c1e02-4f3b-4c8e-9d21-6b0f5a8e33c1
              endToEndId: INV-2026-4471
              status: SETTLED
              isoStatus: ACSC
              terminal: true
              execution:
                providerId: bridge
                providerReference: brg_tr_01J8KX2M4Q
                creditAmount: { currency: INR, value: "2043725.00" }
                exchangeRate: { pair: USD/INR, rate: "83.4173", spreadBps: "18" }
                settlementAsset: { token: USDC, chain: base }
                settledAt: "2026-08-22T15:52:08Z"
              metadata: { costCentre: CC-EMEA-07 }
      responses:
        '2XX':
          description: Acknowledged. Respond within 5 seconds; do your work asynchronously.

  payment.failed:
    post:
      operationId: onPaymentFailed
      summary: A payment was rejected — terminal
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PaymentWebhook' }
            example:
              event: payment.failed
              eventId: evt_01J8KXC91P3R
              occurredAt: "2026-08-22T09:41:22Z"
              uetr: b41d77aa-9c02-4e15-8f6b-2a7de1440c93
              endToEndId: INV-2026-4472
              status: FAILED
              isoStatus: RJCT
              isoReasonCode: AC03
              reasonText: Creditor account number failed provider validation
              terminal: true
              retryable: false
              failedStage: PROVIDER_SUBMISSION
              providerId: bridge
      responses:
        '2XX': { description: Acknowledged. }

  payment.returned:
    post:
      operationId: onPaymentReturned
      summary: Funds returned after settlement — terminal
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PaymentWebhook' }
      responses:
        '2XX': { description: Acknowledged. }

  payment.cancelled:
    post:
      operationId: onPaymentCancelled
      summary: Cancelled before submission — terminal
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PaymentWebhook' }
      responses:
        '2XX': { description: Acknowledged. }

  payment.accepted:
    post:
      operationId: onPaymentAccepted
      summary: Instruction validated and accepted
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PaymentWebhook' }
      responses:
        '2XX': { description: Acknowledged. }

  payment.screened:
    post:
      operationId: onPaymentScreened
      summary: Sanctions and AML cleared
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PaymentWebhook' }
      responses:
        '2XX': { description: Acknowledged. }

  payment.routed:
    post:
      operationId: onPaymentRouted
      summary: Provider resolved
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PaymentWebhook' }
      responses:
        '2XX': { description: Acknowledged. }

  payment.submitted:
    post:
      operationId: onPaymentSubmitted
      summary: Handed to the provider
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PaymentWebhook' }
      responses:
        '2XX': { description: Acknowledged. }

  beneficiary.validated:
    post:
      operationId: onBeneficiaryValidated
      summary: Confirmation of Payee completed
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BeneficiaryValidation' }
      responses:
        '2XX': { description: Acknowledged. }

# ─────────────────────────────────────────────────────────────── components
components:

  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: X-Silora-Key
      description: Your API key **id**, not the secret.
    Signature:
      type: apiKey
      in: header
      name: X-Silora-Signature
      description: |
        `HMAC-SHA256(secret, timestamp + "." + method + "." + path + "." + sha256(body))`,
        hex-encoded.
    Timestamp:
      type: apiKey
      in: header
      name: X-Silora-Timestamp
      description: Unix seconds. Skew over 300 seconds is rejected.

  parameters:
    Uetr:
      name: uetr
      in: path
      required: true
      description: The payment's UETR — its identity everywhere.
      schema: { type: string, format: uuid }
      example: 7a9c1e02-4f3b-4c8e-9d21-6b0f5a8e33c1
    BeneficiaryId:
      name: beneficiaryId
      in: path
      required: true
      schema: { type: string }
      example: ben_01J8M4TQ7X2K9
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: |
        UUID. Replaying with an identical body returns the original response and
        `Idempotency-Replayed: true`; with a different body it returns `409`.
        Keys are retained 24 hours.
      schema: { type: string, format: uuid }
    IdempotencyKeyOptional:
      name: Idempotency-Key
      in: header
      required: false
      description: |
        Optional here. Supply one when the call has a side effect you do not want
        duplicated — holding an FX rate, or re-queuing a delivery.
      schema: { type: string, format: uuid }
    CorrelationId:
      name: X-Correlation-Id
      in: header
      description: Echoed on the response and threaded through every internal hop.
      schema: { type: string }
    Cursor:
      name: cursor
      in: query
      schema: { type: string }
    Limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 500, default: 100 }

  responses:
    Unauthorized:
      description: Bad key, signature or timestamp skew.
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    NotFound:
      description: Unknown resource.
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    Conflict:
      description: Idempotency-key reuse with a different body, or invalid state transition.
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    RateLimited:
      description: Rate limited.
      headers:
        RateLimit-Limit: { schema: { type: integer } }
        RateLimit-Remaining: { schema: { type: integer } }
        RateLimit-Reset: { schema: { type: integer } }
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    ProviderUnavailable:
      description: Provider unreachable. Retry with the same `Idempotency-Key`.
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    ValidationFailed:
      description: Malformed, or failed schema / corridor / CBPR+ validation.
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
          example:
            type: https://docs.silorapay.ai/errors/validation-failed
            title: Validation failed
            status: 400
            detail: 3 fields failed validation for corridor US-IN
            correlationId: 01J8KX9P2R7M4Q
            isoReasonCode: FF01
            errors:
              - pointer: /purpose
                code: FF05
                detail: Required for US-IN. Use a code from IN_RBI_PURPOSE.
                requirementsUrl: /v1/corridors/US-IN/requirements
              - pointer: /creditor/address/postCode
                code: RR03
                detail: Structured creditor address is mandatory for this corridor under CBPR+
              - pointer: /instructedAmount/value
                code: FF01
                detail: Amount must be a decimal string, not a JSON number
    Rejected:
      description: Well-formed but rejected. Always carries `isoReasonCode`.
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
          example:
            type: https://docs.silorapay.ai/errors/payment-rejected
            title: Payment rejected
            status: 422
            detail: No capable provider for corridor US-IN in USD at this amount
            correlationId: 01J8KX9P2R7M4Q
            isoReasonCode: AG01
            routingDecision:
              preference: AUTO
              tier: 3
              providerId: null
              evaluated:
                - providerId: bridge
                  matched: false
                  reason: Insufficient prefunding
                - providerId: xbs
                  matched: false
                  reason: Corridor not enabled

  schemas:

    # ── primitives ────────────────────────────────────────────
    DecimalString:
      type: string
      pattern: '^-?[0-9]+(\.[0-9]+)?$'
      description: |
        A decimal encoded as a string. **Never a JSON number** — IEEE-754 doubles
        silently corrupt money.
      example: "24500.00"

    CurrencyCode:
      type: string
      pattern: '^[A-Z]{3}$'
      description: ISO 4217 currency. Stablecoin tickers are never valid here.
      example: USD

    CountryCode:
      type: string
      pattern: '^[A-Z]{2}$'
      description: ISO 3166-1 alpha-2.

    Amount:
      type: object
      required: [currency, value]
      properties:
        currency: { $ref: '#/components/schemas/CurrencyCode' }
        value: { $ref: '#/components/schemas/DecimalString' }

    Page:
      type: object
      required: [hasMore, limit]
      properties:
        cursor: { type: [string, 'null'] }
        hasMore: { type: boolean }
        limit: { type: integer }

    # ── enumerations ──────────────────────────────────────────
    PartyType:
      type: string
      enum: [INDV, ORGA]
      description: Individual or organisation. Determines which identification fields apply.

    ChargeBearer:
      type: string
      enum: [DEBT, CRED, SHAR, SLEV]
      description: ISO `ChargeBearerType1Code`.

    ProviderCode:
      type: string
      description: |
        `AUTO` is a reserved value, not a provider. Explicit codes come from
        `GET /providers`. An explicit provider is a **hard constraint** — if it
        cannot serve the payment, the payment is rejected, never re-resolved.
      enum: [AUTO, BRIDGE, XBS, NIUM, THUNES, VISA_B2B, MC_MOVE, RIPPLE]
      default: AUTO

    PaymentStatus:
      type: string
      enum: [INITIATED, SCREENED, ROUTED, SUBMITTED, SETTLED, FAILED, RETURNED]

    IsoStatus:
      type: string
      description: ISO `ExternalPaymentTransactionStatus1Code`.
      enum: [RCVD, ACTC, ACSP, ACSC, ACCC, PDNG, RJCT, CANC]

    IsoReasonCode:
      type: string
      description: |
        ISO `ExternalStatusReason1Code`, validated against the quarterly code set
        rather than a fixed enum. Common values: `AC03` invalid creditor account,
        `AC04` closed, `AC06` blocked, `AM02` limit exceeded, `AM04` insufficient
        funds, `AG01` forbidden or no capable provider, `RR03` missing creditor
        address, `RR04` regulatory, `CURR` unsupported currency, `TM01` past
        cut-off, `DT01` invalid date or expired quote, `DUPL` duplicate, `ED05`
        settlement failed, `FF01` invalid format, `FF05` invalid purpose code,
        `AB06`/`AB07` provider timeout or offline.
      example: AC03

    BeneficiaryStatus:
      type: string
      enum: [UNVALIDATED, VALIDATED, VALIDATION_FAILED, INACTIVE]

    WebhookEvent:
      type: string
      enum:
        - payment.accepted
        - payment.screened
        - payment.routed
        - payment.submitted
        - payment.settled
        - payment.failed
        - payment.cancelled
        - payment.returned
        - beneficiary.validated
        - quote.expired

    # ── party and account ─────────────────────────────────────
    PostalAddress:
      type: object
      description: |
        Structured address. CBPR+ is moving to structured-mandatory — send these
        fields rather than free-text `addressLine`.
      properties:
        streetName: { type: string, maxLength: 70 }
        buildingNumber: { type: string, maxLength: 16 }
        postCode: { type: string, maxLength: 16 }
        townName: { type: string, maxLength: 35 }
        countrySubDivision: { type: string, maxLength: 35 }
        country: { $ref: '#/components/schemas/CountryCode' }
        addressLine:
          type: array
          maxItems: 3
          items: { type: string, maxLength: 70 }
          description: Unstructured fallback. Discouraged.

    DateAndPlaceOfBirth:
      type: object
      required: [birthDate, countryOfBirth]
      properties:
        birthDate: { type: string, format: date }
        cityOfBirth: { type: string, maxLength: 35 }
        countryOfBirth: { $ref: '#/components/schemas/CountryCode' }

    PartyIdentification:
      type: object
      description: |
        FATF Recommendation 16 requires an identifier for the originator on
        cross-border wires. For `ORGA` use `lei` or `taxId`; for `INDV` use
        `nationalId`, `passportNumber`, `customerId` or `dateAndPlaceOfBirth`.
      properties:
        lei: { type: string, pattern: '^[A-Z0-9]{18}[0-9]{2}$' }
        taxId: { type: string, maxLength: 35 }
        bic: { type: string, pattern: '^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$' }
        nationalId: { type: string, maxLength: 35 }
        passportNumber: { type: string, maxLength: 35 }
        driversLicence: { type: string, maxLength: 35 }
        customerId: { type: string, maxLength: 35 }
        dateAndPlaceOfBirth: { $ref: '#/components/schemas/DateAndPlaceOfBirth' }

    Contact:
      type: object
      properties:
        email: { type: string, format: email }
        phone:
          type: string
          pattern: '^\+[0-9]{6,18}$'
          description: E.164. Required by mobile-money and several payout rails.

    Party:
      type: object
      required: [name]
      properties:
        type: { $ref: '#/components/schemas/PartyType' }
        name: { type: string, maxLength: 140 }
        address: { $ref: '#/components/schemas/PostalAddress' }
        identification: { $ref: '#/components/schemas/PartyIdentification' }
        contact: { $ref: '#/components/schemas/Contact' }
        countryOfResidence:
          allOf: [{ $ref: '#/components/schemas/CountryCode' }]
          description: Distinct from address country; matters for sanctions screening.

    Account:
      type: object
      description: Either an IBAN or a scheme-qualified local account identifier — never both.
      oneOf:
        - type: object
          required: [iban]
          properties:
            iban: { type: string, pattern: '^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$' }
            currency: { $ref: '#/components/schemas/CurrencyCode' }
        - type: object
          required: [scheme, id]
          properties:
            scheme:
              type: string
              description: Local scheme name, from the corridor requirements.
              example: IN_ACCOUNT
            id: { type: string, maxLength: 34 }
            issuer: { type: string, maxLength: 35 }
            currency: { $ref: '#/components/schemas/CurrencyCode' }

    Agent:
      type: object
      description: At least one identifier is required.
      anyOf:
        - required: [bic]
        - required: [clearingSystemMemberId]
      properties:
        bic: { type: string, pattern: '^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$' }
        clearingSystem:
          type: string
          description: e.g. `INIFSC`, `GBDSC`, `USABA`, `AUBSB`.
          example: INIFSC
        clearingSystemMemberId: { type: string, maxLength: 35, example: HDFC0000123 }
        lei: { type: string }
        name: { type: string, maxLength: 140 }
        country: { $ref: '#/components/schemas/CountryCode' }
        address: { $ref: '#/components/schemas/PostalAddress' }

    # ── remittance and regulatory ─────────────────────────────
    ReferredDocument:
      type: object
      required: [type, number]
      properties:
        type:
          type: string
          description: ISO `ExternalDocumentType1Code` — `CINV` commercial invoice, `CREN` credit note.
          example: CINV
        number: { type: string, maxLength: 35 }
        relatedDate: { type: string, format: date }
        amount: { $ref: '#/components/schemas/Amount' }

    StructuredRemittance:
      type: object
      description: What lets the *beneficiary* reconcile automatically.
      properties:
        referredDocument:
          type: array
          items: { $ref: '#/components/schemas/ReferredDocument' }
        creditorReference:
          type: object
          properties:
            type: { type: string, example: SCOR }
            reference:
              type: string
              description: ISO 11649 RF creditor reference.
              example: RF18539007547034
        additionalInformation:
          type: array
          items: { type: string, maxLength: 140 }

    RemittanceInformation:
      type: object
      properties:
        unstructured:
          type: array
          items: { type: string, maxLength: 140 }
        structured:
          type: array
          items: { $ref: '#/components/schemas/StructuredRemittance' }

    RegulatoryReporting:
      type: object
      properties:
        debitCreditReportingIndicator: { type: string, enum: [CRED, DEBT, BOTH] }
        authority:
          type: object
          properties:
            name: { type: string, maxLength: 140 }
            country: { $ref: '#/components/schemas/CountryCode' }
        details:
          type: array
          items:
            type: object
            properties:
              type: { type: string, example: PURPOSE }
              code: { type: string, example: P0103 }
              amount: { $ref: '#/components/schemas/Amount' }
              information:
                type: array
                items: { type: string, maxLength: 35 }

    # ── payment request ───────────────────────────────────────
    PaymentRequest:
      type: object
      required: [endToEndId]
      description: |
        The instruction block. Exactly one of `instructedAmount` (fixed-send) or
        `equivalentAmount` (fixed-receive) is required. Provide either
        `beneficiaryId` or the inline `creditor` / `creditorAccount` /
        `creditorAgent` trio.

        Provider mechanics — funding accounts, wallet addresses, chains, tokens,
        FX rates — are **never** accepted here. A payload containing one is
        rejected `FF01`.
      oneOf:
        - required: [instructedAmount]
        - required: [equivalentAmount]
      properties:
        endToEndId:
          type: string
          maxLength: 35
          description: Your reference. Echoed everywhere. **Not** an idempotency key.
        instructionId: { type: string, maxLength: 35 }
        provider: { $ref: '#/components/schemas/ProviderCode' }

        instructedAmount:
          allOf: [{ $ref: '#/components/schemas/Amount' }]
          description: Fixed-send — debit exactly this. Maps to `Amt/InstdAmt`.
        equivalentAmount:
          type: object
          description: Fixed-receive — deliver exactly this. Maps to `Amt/EqvtAmt` + `CcyOfTrf`.
          required: [currency, value, debitCurrency]
          properties:
            currency: { $ref: '#/components/schemas/CurrencyCode' }
            value: { $ref: '#/components/schemas/DecimalString' }
            debitCurrency: { $ref: '#/components/schemas/CurrencyCode' }

        execution:
          type: object
          properties:
            requestedExecutionDate: { type: string, format: date }
            priority: { type: string, enum: [NORM, HIGH], default: NORM }
            serviceLevel:
              type: string
              description: ISO `ExternalServiceLevel1Code` — `G001`, `URGP`, `SDVA`, `SEPA`.
              example: G001
            expiresAt:
              type: string
              format: date-time
              description: Do not submit after this instant; reject `TM01`.

        charges:
          type: object
          properties:
            bearer:
              allOf: [{ $ref: '#/components/schemas/ChargeBearer' }]
              default: SHAR
            fxSpreadBearer:
              type: string
              enum: [DEBT, CRED]
              description: Who absorbs the FX spread — a question `bearer` alone does not answer.

        fx:
          type: object
          properties:
            quoteId:
              type: string
              description: Bind to a held quote. Rejected `DT01` if expired.
            maxSlippageBps: { $ref: '#/components/schemas/DecimalString' }
            onSlippageExceeded:
              type: string
              enum: [REJECT, PROCEED]
              default: REJECT

        debtor: { $ref: '#/components/schemas/Party' }
        debtorAccount: { $ref: '#/components/schemas/Account' }
        ultimateDebtor:
          allOf: [{ $ref: '#/components/schemas/Party' }]
          description: On-behalf-of. Needed by any bank running a payment factory.

        beneficiaryId:
          type: string
          description: A stored beneficiary. Resolves creditor, account and agent server-side.
        creditor: { $ref: '#/components/schemas/Party' }
        creditorAccount: { $ref: '#/components/schemas/Account' }
        creditorAgent: { $ref: '#/components/schemas/Agent' }
        ultimateCreditor: { $ref: '#/components/schemas/Party' }

        purpose:
          type: string
          description: |
            ISO `ExternalPurpose1Code`, or a **local** code where the corridor
            mandates one. `GET /corridors/{pair}/requirements` names which list applies.
          example: P0103
        categoryPurpose:
          type: string
          description: ISO `ExternalCategoryPurpose1Code` — `SUPP`, `SALA`, `TRAD`, `INTC`.
          example: SUPP
        regulatoryReporting:
          type: array
          items: { $ref: '#/components/schemas/RegulatoryReporting' }
        documents:
          type: array
          description: Document ids from `POST /documents`.
          items: { type: string }

        remittanceInformation: { $ref: '#/components/schemas/RemittanceInformation' }

        instructionsForCreditorAgent:
          type: array
          items:
            type: object
            properties:
              code: { type: string, enum: [PHOB, TELB, HOLD, CHQB] }
              information: { type: string, maxLength: 140 }

        metadata:
          type: object
          description: |
            Free-form key/value, at most 20 keys. Silora stores, echoes and
            indexes it for your reporting, and never interprets it. Echoed on
            every webhook so reconciliation needs no second lookup.
          additionalProperties: { type: string, maxLength: 256 }

    # ── payment responses ─────────────────────────────────────
    RouteEvaluation:
      type: object
      required: [providerId, matched, reason]
      properties:
        providerId: { type: string }
        matched: { type: boolean }
        winner: { type: boolean }
        reason: { type: string }

    RoutingDecision:
      type: object
      description: |
        Why this provider. "The system chose NIUM" is not an answer an operations
        team will accept; this is.
      required: [preference, tier]
      properties:
        preference: { type: string, example: AUTO }
        tier:
          type: integer
          minimum: 0
          maximum: 3
          description: |
            0 explicit code · 1 sole capable subscription · 2 routing rules ·
            3 best available.
        objective:
          type: string
          enum: [COST, SPEED, RELIABILITY]
          description: Tier 3 only.
        providerId: { type: [string, 'null'] }
        evaluated:
          type: array
          items: { $ref: '#/components/schemas/RouteEvaluation' }

    ExchangeRate:
      type: object
      properties:
        pair: { type: string, example: USD/INR }
        rate: { $ref: '#/components/schemas/DecimalString' }
        inverseRate: { $ref: '#/components/schemas/DecimalString' }
        spreadBps: { $ref: '#/components/schemas/DecimalString' }
        source: { type: string, example: provider }
        asOf: { type: string, format: date-time }

    Charge:
      type: object
      properties:
        type: { type: string, enum: [PLATFORM, PROVIDER, INTERMEDIARY, BENEFICIARY_BANK] }
        bearer: { $ref: '#/components/schemas/ChargeBearer' }
        amount: { $ref: '#/components/schemas/Amount' }

    SettlementAsset:
      type: object
      description: |
        Present only when the resolved rail used one. An **execution outcome** —
        never an input. The instructed amount stays in its ISO 4217 currency.
      properties:
        token: { type: string, example: USDC }
        chain: { type: string, example: base }
        contractAddress: { type: string }
        decimals: { type: integer }

    PaymentExecution:
      type: object
      description: Resolved by the platform. Never accepted from a caller.
      properties:
        providerId: { type: string }
        providerName: { type: string }
        routingDecision: { $ref: '#/components/schemas/RoutingDecision' }
        providerReference: { type: string }
        settlementAmount: { $ref: '#/components/schemas/Amount' }
        creditAmount: { $ref: '#/components/schemas/Amount' }
        exchangeRate: { $ref: '#/components/schemas/ExchangeRate' }
        charges:
          type: array
          items: { $ref: '#/components/schemas/Charge' }
        settlementAsset: { $ref: '#/components/schemas/SettlementAsset' }
        settlementDate: { type: string, format: date }
        settledAt: { type: string, format: date-time }
        attempts: { type: integer }

    Corridor:
      type: object
      properties:
        pair: { type: string, example: US-IN }
        from: { $ref: '#/components/schemas/CountryCode' }
        to: { $ref: '#/components/schemas/CountryCode' }
        currencies:
          type: array
          items: { $ref: '#/components/schemas/CurrencyCode' }
        providers:
          type: array
          items: { type: string }

    PaymentAccepted:
      type: object
      required: [uetr, endToEndId, status, isoStatus]
      properties:
        uetr: { type: string, format: uuid }
        endToEndId: { type: string }
        instructionId: { type: string }
        createdAt: { type: string, format: date-time }
        status: { $ref: '#/components/schemas/PaymentStatus' }
        isoStatus: { $ref: '#/components/schemas/IsoStatus' }
        instructedAmount: { $ref: '#/components/schemas/Amount' }
        corridor: { $ref: '#/components/schemas/Corridor' }
        beneficiaryId: { type: string }
        accepted:
          type: object
          description: What was checked at acceptance, so you know the rejection risk that remains.
          properties:
            validation: { type: string, enum: [PASSED] }
            corridorRequirements: { type: string, enum: [SATISFIED] }
            beneficiaryValidation: { $ref: '#/components/schemas/BeneficiaryValidation' }
        execution:
          oneOf:
            - $ref: '#/components/schemas/PaymentExecution'
            - type: 'null'
          description: Null until the payment is routed.
        links:
          type: object
          properties:
            self: { type: string, format: uri }
            timeline: { type: string, format: uri }

    Payment:
      type: object
      required: [uetr, status, isoStatus]
      properties:
        uetr: { type: string, format: uuid }
        endToEndId: { type: string }
        instructionId: { type: string }
        transactionId: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        status: { $ref: '#/components/schemas/PaymentStatus' }
        isoStatus: { $ref: '#/components/schemas/IsoStatus' }
        isoReasonCode:
          oneOf:
            - $ref: '#/components/schemas/IsoReasonCode'
            - type: 'null'
        reasonText: { type: [string, 'null'] }
        instruction: { $ref: '#/components/schemas/PaymentRequest' }
        execution:
          oneOf:
            - $ref: '#/components/schemas/PaymentExecution'
            - type: 'null'
        compliance:
          type: object
          properties:
            screening:
              type: object
              properties:
                result: { type: string, enum: [CLEAR, HIT, PENDING] }
                screenedAt: { type: string, format: date-time }
            beneficiaryValidation: { $ref: '#/components/schemas/BeneficiaryValidation' }
        corridor: { $ref: '#/components/schemas/Corridor' }

    PaymentSummary:
      type: object
      properties:
        uetr: { type: string, format: uuid }
        endToEndId: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        status: { $ref: '#/components/schemas/PaymentStatus' }
        isoStatus: { $ref: '#/components/schemas/IsoStatus' }
        instructedAmount: { $ref: '#/components/schemas/Amount' }
        creditAmount: { $ref: '#/components/schemas/Amount' }
        corridor: { $ref: '#/components/schemas/Corridor' }
        creditorName: { type: string }
        providerId: { type: string }
        metadata:
          type: object
          additionalProperties: { type: string }

    TimelineEvent:
      type: object
      required: [at, status, isoStatus, label]
      properties:
        at: { type: string, format: date-time }
        status: { $ref: '#/components/schemas/PaymentStatus' }
        isoStatus: { $ref: '#/components/schemas/IsoStatus' }
        label: { type: string }
        detail: { type: string }

    # ── beneficiaries ─────────────────────────────────────────
    BeneficiaryRequest:
      type: object
      required: [name, account]
      properties:
        reference: { type: string, maxLength: 35 }
        type: { $ref: '#/components/schemas/PartyType' }
        name: { type: string, maxLength: 140 }
        address: { $ref: '#/components/schemas/PostalAddress' }
        identification: { $ref: '#/components/schemas/PartyIdentification' }
        contact: { $ref: '#/components/schemas/Contact' }
        countryOfResidence: { $ref: '#/components/schemas/CountryCode' }
        account: { $ref: '#/components/schemas/Account' }
        agent: { $ref: '#/components/schemas/Agent' }
        defaultPurpose: { type: string }
        metadata:
          type: object
          additionalProperties: { type: string, maxLength: 256 }

    Beneficiary:
      type: object
      required: [beneficiaryId, name, status]
      properties:
        beneficiaryId: { type: string }
        reference: { type: string }
        status: { $ref: '#/components/schemas/BeneficiaryStatus' }
        type: { $ref: '#/components/schemas/PartyType' }
        name: { type: string }
        address: { $ref: '#/components/schemas/PostalAddress' }
        contact: { $ref: '#/components/schemas/Contact' }
        account:
          allOf: [{ $ref: '#/components/schemas/Account' }]
          description: Account identifiers are masked on read.
        agent: { $ref: '#/components/schemas/Agent' }
        corridors:
          type: array
          description: Derived from the account and agent.
          items: { type: string }
        lastValidation: { $ref: '#/components/schemas/BeneficiaryValidation' }
        createdAt: { type: string, format: date-time }

    BeneficiaryValidation:
      type: object
      properties:
        beneficiaryId: { type: string }
        result:
          type: string
          enum: [MATCH, CLOSE_MATCH, NO_MATCH, ACCOUNT_NOT_FOUND, ACCOUNT_CLOSED, UNAVAILABLE]
        nameMatch: { type: string, enum: [FULL, PARTIAL, NONE] }
        accountStatus: { type: string, enum: [ACTIVE, CLOSED, BLOCKED, UNKNOWN] }
        accountHolderName:
          type: string
          description: Returned on `CLOSE_MATCH` so a human can decide.
        validatedBy: { type: string }
        validatedAt: { type: string, format: date-time }
        expiresAt: { type: string, format: date-time }

    # ── corridor requirements ─────────────────────────────────
    FieldRequirement:
      type: object
      required: [path, requirement]
      properties:
        path:
          type: string
          description: Dotted path into the payment request.
          example: creditorAgent.clearingSystemMemberId
        requirement:
          type: string
          enum: [REQUIRED, CONDITIONAL, RECOMMENDED, OPTIONAL, NOT_SUPPORTED]
        condition:
          type: string
          description: Present when `requirement` is `CONDITIONAL`.
        reason: { type: string }
        label: { type: string }
        pattern: { type: string }
        maxLength: { type: integer }
        allowed:
          type: array
          items: { type: string }
        codeList: { type: string }
        codeListUrl: { type: string }

    CorridorRequirements:
      type: object
      required: [corridor, supported, fields]
      properties:
        corridor: { type: string, example: US-IN }
        currency: { $ref: '#/components/schemas/CurrencyCode' }
        supported: { type: boolean }
        fields:
          type: array
          items: { $ref: '#/components/schemas/FieldRequirement' }
        limits:
          type: object
          properties:
            minAmount: { $ref: '#/components/schemas/Amount' }
            maxAmount: { $ref: '#/components/schemas/Amount' }
            cutOff: { type: string, example: "14:30" }
            cutOffTimezone: { type: string, example: America/New_York }
        settlement:
          type: object
          properties:
            typical: { type: string, example: same-day }
            window: { type: string, example: T+0 to T+1 }
        providers:
          type: array
          items: { type: string }

    # ── quote ─────────────────────────────────────────────────
    Quote:
      type: object
      required: [quoteId, routingDecision]
      properties:
        quoteId: { type: string }
        expiresAt: { type: string, format: date-time }
        rateHeld: { type: boolean }
        routingDecision: { $ref: '#/components/schemas/RoutingDecision' }
        debitAmount: { $ref: '#/components/schemas/Amount' }
        creditAmount: { $ref: '#/components/schemas/Amount' }
        exchangeRate: { $ref: '#/components/schemas/ExchangeRate' }
        charges:
          type: array
          items: { $ref: '#/components/schemas/Charge' }
        totalCost:
          allOf: [{ $ref: '#/components/schemas/Amount' }]
        settlement:
          type: object
          properties:
            estimatedAt: { type: string, format: date-time }
            window: { type: string }
            cutOff: { type: string, format: date-time }
        requirements:
          type: object
          description: Quote first and you never discover a missing purpose code by rejection.
          properties:
            satisfied: { type: boolean }
            missing:
              type: array
              items: { $ref: '#/components/schemas/FieldRequirement' }

    # ── providers ─────────────────────────────────────────────
    Provider:
      type: object
      properties:
        code: { type: string, example: BRIDGE }
        providerId: { type: string, example: bridge }
        name: { type: string }
        kind: { type: string, enum: [STABLECOIN, PSP] }
        subscribed: { type: boolean }
        status: { type: string, enum: [HEALTHY, DEGRADED, DOWN] }
        currencies:
          type: array
          items: { $ref: '#/components/schemas/CurrencyCode' }
        corridors:
          type: array
          items: { type: string }

    CapabilityMatrix:
      type: object
      properties:
        corridor: { type: string }
        currency: { $ref: '#/components/schemas/CurrencyCode' }
        amount: { $ref: '#/components/schemas/DecimalString' }
        supported: { type: boolean }
        providers:
          type: array
          items:
            type: object
            properties:
              code: { type: string }
              capable: { type: boolean }
              funded: { type: [boolean, 'null'] }
              estimatedCostBps: { $ref: '#/components/schemas/DecimalString' }
              settlementWindow: { type: string }
              note: { type: string }

    FundingAccount:
      type: object
      properties:
        accountId: { type: string }
        providerId: { type: string }
        currency: { $ref: '#/components/schemas/CurrencyCode' }
        available: { $ref: '#/components/schemas/Amount' }
        pending: { $ref: '#/components/schemas/Amount' }
        asOf: { type: string, format: date-time }

    Document:
      type: object
      properties:
        documentId: { type: string }
        type: { type: string }
        filename: { type: string }
        sizeBytes: { type: integer }
        uploadedAt: { type: string, format: date-time }

    # ── webhooks ──────────────────────────────────────────────
    WebhookEndpoint:
      type: object
      properties:
        endpointId: { type: string }
        url: { type: string, format: uri }
        events:
          type: array
          items: { $ref: '#/components/schemas/WebhookEvent' }
        description: { type: string }
        status: { type: string, enum: [ACTIVE, SUSPENDED] }
        createdAt: { type: string, format: date-time }

    WebhookDelivery:
      type: object
      properties:
        deliveryId: { type: string }
        eventId: { type: string }
        event: { $ref: '#/components/schemas/WebhookEvent' }
        uetr: { type: string, format: uuid }
        attempts: { type: integer }
        lastStatusCode: { type: integer }
        deliveredAt: { type: string, format: date-time }
        status: { type: string, enum: [DELIVERED, FAILED, PENDING] }

    PaymentWebhook:
      type: object
      description: |
        Delivered with `X-Silora-Signature: sha256=HMAC(secret, timestamp + "." + rawBody)`.

        At-least-once — deduplicate on `eventId`. Order is **not** guaranteed;
        trust `occurredAt`, never arrival order. Retried with exponential backoff
        over 24 hours on any non-`2xx`.
      required: [event, eventId, occurredAt, uetr, status, isoStatus]
      properties:
        event: { $ref: '#/components/schemas/WebhookEvent' }
        eventId: { type: string }
        occurredAt: { type: string, format: date-time }
        uetr: { type: string, format: uuid }
        endToEndId: { type: string }
        instructionId: { type: string }
        status: { $ref: '#/components/schemas/PaymentStatus' }
        isoStatus: { $ref: '#/components/schemas/IsoStatus' }
        isoReasonCode:
          oneOf:
            - $ref: '#/components/schemas/IsoReasonCode'
            - type: 'null'
        reasonText: { type: [string, 'null'] }
        terminal: { type: boolean }
        retryable: { type: boolean }
        failedStage:
          type: string
          enum: [VALIDATION, SCREENING, ROUTING, PROVIDER_SUBMISSION, SETTLEMENT]
        providerId: { type: string }
        remediation: { type: string }
        execution: { $ref: '#/components/schemas/PaymentExecution' }
        returnedAmount: { $ref: '#/components/schemas/Amount' }
        chargesDeducted: { $ref: '#/components/schemas/Amount' }
        returnedAt: { type: string, format: date-time }
        metadata:
          type: object
          additionalProperties: { type: string }

    # ── errors ────────────────────────────────────────────────
    Problem:
      type: object
      description: RFC 9457 problem details, extended with the ISO reason code.
      required: [type, title, status]
      properties:
        type: { type: string, format: uri }
        title: { type: string }
        status: { type: integer }
        detail: { type: string }
        instance: { type: string }
        correlationId: { type: string }
        uetr: { type: [string, 'null'] }
        isoReasonCode:
          oneOf:
            - $ref: '#/components/schemas/IsoReasonCode'
            - type: 'null'
        routingDecision: { $ref: '#/components/schemas/RoutingDecision' }
        errors:
          type: array
          items:
            type: object
            required: [pointer, code]
            properties:
              pointer:
                type: string
                description: JSON Pointer to the offending element.
              code: { $ref: '#/components/schemas/IsoReasonCode' }
              detail: { type: string }
              requirementsUrl:
                type: string
                description: The corridor requirement that produced this error.
