openapi: 3.0.3
info:
  title: Moneva Platform API
  version: 1.0.0-draft.1
  description: |
    Embedded fiat accounts, KYC-gated customer onboarding, off-ramp and
    remittance on stablecoin rails. One API key, one integration.

    ## Authentication
    All requests use a secret API key in the Authorization header:
    `Authorization: Bearer mk_test_...` or `Authorization: Bearer mk_live_...`.
    Test keys operate against a fully simulated environment.

    ## Idempotency
    All money-moving POST requests require an `Idempotency-Key` header.
    Replays with the same key return the original response.

    ## Errors
    Errors use a stable machine-readable `code`, an HTTP status, and a
    human-readable `message`. Codes are documented per endpoint and never
    change meaning within a major version.

    ## Webhooks
    State changes are delivered as signed webhook events (HMAC-SHA256 over
    the raw body, timestamped, replay-protected) and are queryable via
    `GET /v1/events`. See spec/events.md for the catalog.
  contact:
    name: Moneva
    url: https://moneva.io
servers:
  - url: https://platform.moneva.io/v1
    description: Live
  - url: https://sandbox.platform.moneva.io/v1
    description: Sandbox

security:
  - apiKey: []

tags:
  - name: Customers
    description: End users of your product. KYC state is managed here.
  - name: Accounts
    description: Virtual fiat accounts (deposit details) per customer.
  - name: Corridors
    description: Supported payout corridors and beneficiary form schemas.
  - name: Beneficiaries
    description: External bank destinations for payouts and remittances.
  - name: Quotes
    description: Deterministic pricing for transfers before execution.
  - name: Transfers
    description: Money movement - deposits, payouts, remittances.
  - name: Events
    description: Durable event log backing webhook delivery.
  - name: Webhooks
    description: Webhook endpoint management.
  - name: Test
    description: Sandbox-only lifecycle simulation.

paths:
  /customers:
    post:
      tags: [Customers]
      operationId: createCustomer
      summary: Create a customer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomerCreate'
      responses:
        '201':
          description: Customer created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
        '409':
          description: A customer with this email already exists in your org
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    get:
      tags: [Customers]
      operationId: listCustomers
      summary: List customers
      parameters:
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/cursor'
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/CustomerStatus'
      responses:
        '200':
          description: Paginated customer list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomerList'

  /customers/{customer_id}:
    get:
      tags: [Customers]
      operationId: getCustomer
      summary: Retrieve a customer
      parameters:
        - $ref: '#/components/parameters/customer_id'
      responses:
        '200':
          description: Customer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Customers]
      operationId: updateCustomerProfile
      summary: Update a customer's profile
      description: |
        Submits KYC profile data collected through your own form. Fields
        are validated against the residence country's profile schema
        (GET /jurisdictions/{cc}/profile_schema); an identifier from
        another jurisdiction is rejected with `field_not_allowed`.
        Sensitive identifiers are delivered to the KYC provider and stored
        masked (kind plus last four). Identity-bearing fields lock once
        verification passes. An empty body is a no-op read: nothing is
        stored and no `customer.updated` event is emitted.
      parameters:
        - $ref: '#/components/parameters/customer_id'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomerProfileUpdate'
      responses:
        '200':
          description: Updated customer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Identity fields are locked after verification (profile_locked)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Validation failed or field not accepted for this jurisdiction (field_not_allowed)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /customers/{customer_id}/kyc_session:
    post:
      tags: [Customers]
      operationId: createKycSession
      summary: Start a hosted KYC session
      description: |
        Returns a short-lived hosted URL where the customer completes
        identity verification. Progress is delivered via
        `customer.kyc.*` events. No document handling on your side.
      parameters:
        - $ref: '#/components/parameters/customer_id'
      responses:
        '201':
          description: Hosted KYC session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KycSession'
        '409':
          description: Customer is already verified
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /customers/{customer_id}/wallet_sessions:
    post:
      tags: [Customers]
      operationId: createWalletSessionChallenge
      summary: Create a wallet session challenge
      description: |
        Returns a message for the customer's wallet to sign client-side.
        Self-custody only: the platform prepares and verifies, never signs.
      parameters:
        - $ref: '#/components/parameters/customer_id'
      responses:
        '201':
          description: Challenge (valid 10 minutes)
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    enum: [wallet_session_challenge]
                  message:
                    type: string
                  expires_at:
                    type: string
                    format: date-time

  /customers/{customer_id}/wallet_sessions/verify:
    post:
      tags: [Customers]
      operationId: verifyWalletSession
      summary: Verify a signed wallet session
      parameters:
        - $ref: '#/components/parameters/customer_id'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [signature]
              properties:
                signature:
                  type: string
                  description: |
                    Hex signature over the challenge message. Both wallet
                    kinds are accepted: a 65-byte ECDSA signature from an
                    EOA, or a variable-length ERC-1271 / ERC-6492 signature
                    from a smart-contract account (Safe, Biconomy Nexus,
                    including counterfactual not-yet-deployed accounts).
                  pattern: '^0x(?:[0-9a-fA-F]{2}){65,32768}$'
      responses:
        '200':
          description: Session active (6 days)
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    enum: [wallet_session]
                  status:
                    type: string
                    enum: [active]
                  expires_at:
                    type: string
                    format: date-time
        '409':
          description: No pending challenge to verify (no_pending_challenge)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Invalid signature or expired challenge
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /customers/{customer_id}/accounts:
    post:
      tags: [Accounts]
      operationId: createAccount
      summary: Create a virtual fiat account
      description: |
        Provisions deposit details (IBAN / account and routing / local
        format) in the requested currency. Provisioning is asynchronous:
        the account is returned in status `provisioning` and flips to
        `active` via `account.activated`.
      parameters:
        - $ref: '#/components/parameters/customer_id'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [currency]
              properties:
                currency:
                  $ref: '#/components/schemas/FiatCurrency'
      responses:
        '202':
          description: Account provisioning started
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Account'
        '403':
          description: Currency not eligible for this residency (residency_ineligible)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Customer already has an account in this currency (account_exists)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: KYC not passed (kyc_not_passed)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    get:
      tags: [Accounts]
      operationId: listAccounts
      summary: List a customer's accounts
      parameters:
        - $ref: '#/components/parameters/customer_id'
      responses:
        '200':
          description: Accounts
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Account'

  /corridors:
    get:
      tags: [Corridors]
      operationId: listCorridors
      summary: List supported payout corridors
      description: |
        Public, key-less. Live corridor availability including current
        per-corridor minimums. Derived continuously from upstream
        capability, not a static table.
      security: []
      responses:
        '200':
          description: Corridors
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Corridor'

  /corridors/{currency}/beneficiary_schema:
    get:
      tags: [Corridors]
      operationId: getBeneficiarySchema
      summary: Get the beneficiary form schema for a corridor
      description: |
        Public, key-less. Returns the exact field set, types, validation
        rules, and display labels required to collect a valid beneficiary
        for this corridor. Render your bank form from this response;
        submitted values are validated against the same schema server-side.
      security: []
      parameters:
        - name: currency
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/PayoutCurrency'
      responses:
        '200':
          description: Form schema
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BeneficiarySchema'
        '404':
          $ref: '#/components/responses/NotFound'

  /jurisdictions:
    get:
      tags: [Corridors]
      operationId: listJurisdictions
      summary: List supported residence jurisdictions
      description: |
        Public, key-less reference data: supported residence countries with
        their residency rules and currency availability (which currencies can
        be held as virtual accounts and paid out). Per-customer eligibility is
        enforced automatically at KYC.
      security: []
      responses:
        '200':
          description: Residency rules and currency availability
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    required: [residency_rules, currencies, onboarding]
                    properties:
                      residency_rules:
                        type: array
                        items:
                          type: object
                          required: [residency, on_ramp, off_ramp]
                          properties:
                            residency:
                              type: string
                              example: US
                            on_ramp:
                              type: array
                              items:
                                type: string
                            off_ramp:
                              type: array
                              items:
                                type: string
                            notes:
                              type: string
                      currencies:
                        type: array
                        items:
                          type: object
                          required: [code, on_ramp, off_ramp, status]
                          properties:
                            code:
                              type: string
                              example: BRL
                            on_ramp:
                              type: boolean
                            off_ramp:
                              type: boolean
                            status:
                              type: string
                              enum: [active, coming_soon]
                      onboarding:
                        type: object
                        required: [count, countries]
                        properties:
                          count:
                            type: integer
                            example: 185
                          individual:
                            type: boolean
                          corporate:
                            type: boolean
                          countries:
                            type: array
                            items:
                              type: string
                              description: ISO 3166-1 alpha-2

  /jurisdictions/{cc}/profile_schema:
    get:
      tags: [Customers]
      operationId: getJurisdictionProfileSchema
      summary: Get the per-country KYC profile schema
      description: |
        Public, key-less. Returns the profile fields ("tell us about
        yourself" data) required for a residence country so you can render
        your own KYC form. Submitted values are validated against the same
        schema server-side.
      security: []
      parameters:
        - name: cc
          in: path
          required: true
          description: ISO 3166-1 alpha-2 residence country
          schema:
            type: string
            example: US
      responses:
        '200':
          description: Per-country profile field schema
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProfileSchema'
        '404':
          $ref: '#/components/responses/NotFound'

  /customers/{customer_id}/beneficiaries:
    post:
      tags: [Beneficiaries]
      operationId: createBeneficiary
      summary: Create a beneficiary
      description: |
        Field values are normalized (case, diacritics, format) and
        validated against the corridor schema before submission upstream.
      parameters:
        - $ref: '#/components/parameters/customer_id'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BeneficiaryCreate'
      responses:
        '201':
          description: Beneficiary created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Beneficiary'
        '422':
          description: Field validation failed against the corridor schema
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
    get:
      tags: [Beneficiaries]
      operationId: listBeneficiaries
      summary: List a customer's beneficiaries
      parameters:
        - $ref: '#/components/parameters/customer_id'
      responses:
        '200':
          description: Beneficiaries
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Beneficiary'

  /quotes:
    post:
      tags: [Quotes]
      operationId: createQuote
      summary: Create a quote
      description: |
        Deterministic pricing for a prospective transfer: rate, itemized
        fees, net receivable, and the live corridor minimum. Quotes are
        served from a warmed cache; no upstream round trip per request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QuoteCreate'
      responses:
        '201':
          description: Quote
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Quote'
        '422':
          description: Amount below corridor minimum
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /transfers:
    post:
      tags: [Transfers]
      operationId: createTransfer
      summary: Create a transfer
      description: |
        Executes a payout or remittance. Requires an `Idempotency-Key`
        header. Quoted values are frozen onto the transfer at creation;
        progress is delivered via `transfer.*` events.
      parameters:
        - $ref: '#/components/parameters/idempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferCreate'
      responses:
        '202':
          description: Transfer accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Transfer'
        '409':
          description: Idempotency key replay with a different payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Below minimum, ineligible corridor, or KYC not passed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    get:
      tags: [Transfers]
      operationId: listTransfers
      summary: List transfers
      parameters:
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/cursor'
        - name: customer_id
          in: query
          schema:
            type: string
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/TransferStatus'
      responses:
        '200':
          description: Paginated transfer list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransferList'

  /transfers/{transfer_id}:
    get:
      tags: [Transfers]
      operationId: getTransfer
      summary: Retrieve a transfer
      parameters:
        - name: transfer_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Transfer with full timeline
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Transfer'
        '404':
          $ref: '#/components/responses/NotFound'

  /events:
    get:
      tags: [Events]
      operationId: listEvents
      summary: List events
      description: |
        Durable, queryable event log. Use for reconciliation or webhook
        recovery. Events are retained for 90 days.
      parameters:
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/cursor'
        - name: type
          in: query
          schema:
            type: string
            example: transfer.completed
      responses:
        '200':
          description: Paginated event list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EventList'

  /events/{event_id}:
    get:
      tags: [Events]
      operationId: getEvent
      summary: Retrieve an event
      parameters:
        - name: event_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Event
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Event'
        '404':
          $ref: '#/components/responses/NotFound'

  /webhook_endpoints:
    post:
      tags: [Webhooks]
      operationId: createWebhookEndpoint
      summary: Register a webhook endpoint
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEndpointCreate'
      responses:
        '201':
          description: Endpoint registered; signing secret returned once
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpoint'
    get:
      tags: [Webhooks]
      operationId: listWebhookEndpoints
      summary: List webhook endpoints
      responses:
        '200':
          description: Endpoints
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookEndpoint'

  /webhook_endpoints/{endpoint_id}:
    delete:
      tags: [Webhooks]
      operationId: deleteWebhookEndpoint
      summary: Delete a webhook endpoint
      parameters:
        - name: endpoint_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Deleted

  /notification_settings:
    get:
      tags: [Notifications]
      operationId: getNotificationSettings
      summary: Get white-label email settings
      description: |
        Org-level branding for end-customer status emails. When enabled,
        the platform sends lifecycle emails (KYC result, deposit settled,
        transfer delivered) rendered with your logo, color, and sender
        name. Every send appears in the event stream as
        notification.email.sent. Sandbox delivers no real mail.
      responses:
        '200':
          description: Current settings
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationSettings'
    patch:
      tags: [Notifications]
      operationId: updateNotificationSettings
      summary: Update white-label email settings
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NotificationSettingsUpdate'
      responses:
        '200':
          description: Updated settings
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationSettings'
        '422':
          description: Validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /notification_settings/templates/{event_type}:
    put:
      tags: [Notifications]
      operationId: setNotificationTemplate
      summary: Set a fully custom email template
      description: |
        Replaces the default layout for one event with your own subject
        and complete HTML: your design end to end. Use {{variables}} for
        dynamic values ({{sender_name}} everywhere; receipt values like
        {{amount_sent}}, {{delivered}}, {{fee}}, {{reference}} on money
        events; snake_cased from the receipt row labels). Unknown
        variables are left visible so mistakes surface in preview.
        Script tags are rejected.
      parameters:
        - name: event_type
          in: path
          required: true
          schema:
            type: string
            example: transfer.completed
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [html]
              properties:
                subject:
                  type: string
                  maxLength: 200
                html:
                  type: string
                  maxLength: 65536
      responses:
        '200':
          description: Updated settings
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationSettings'
        '422':
          description: Invalid event type or template (invalid_event_type, validation_failed)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      tags: [Notifications]
      operationId: deleteNotificationTemplate
      summary: Remove a custom template
      description: Reverts the event to the default branded layout.
      parameters:
        - name: event_type
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Updated settings
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationSettings'

  /notification_settings/domains:
    post:
      tags: [Notifications]
      operationId: addNotificationDomain
      summary: Add a sending domain
      description: |
        Send status emails from your own domain so the end-to-end
        experience is yours. Returns the DNS records (ownership TXT,
        DKIM CNAMEs, return-path) to add at your DNS host, then verify.
        Until a domain verifies, sends fall back to the shared domain;
        an unverified domain is never used.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain:
                  type: string
                  example: acme.com
      responses:
        '201':
          description: Domain added, pending verification
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationDomain'
        '200':
          description: Domain already added
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationDomain'
    get:
      tags: [Notifications]
      operationId: listNotificationDomains
      summary: List sending domains
      responses:
        '200':
          description: Domains
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/NotificationDomain'

  /notification_settings/domains/{domain_id}/verify:
    post:
      tags: [Notifications]
      operationId: verifyNotificationDomain
      summary: Verify a sending domain
      description: |
        Checks the DNS records at your DNS host. Test mode verifies
        immediately so the flow is testable end to end; live mode
        resolves the real records. Emits notification.domain.verified.
      parameters:
        - name: domain_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Domain state after the check
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationDomain'
        '404':
          $ref: '#/components/responses/NotFound'

  /notification_settings/domains/{domain_id}:
    delete:
      tags: [Notifications]
      operationId: deleteNotificationDomain
      summary: Remove a sending domain
      description: |
        Removes the domain; a from_email pointing at it is cleared and
        sends fall back to the shared domain.
      parameters:
        - name: domain_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Deleted
        '404':
          $ref: '#/components/responses/NotFound'

  /notification_settings/preview:
    get:
      tags: [Notifications]
      operationId: previewNotificationEmail
      summary: Preview a branded email
      description: |
        Renders the email for one notifiable event with your current
        branding so you can review it before enabling sends.
      parameters:
        - name: event_type
          in: query
          required: true
          schema:
            type: string
            example: customer.kyc.approved
      responses:
        '200':
          description: Rendered subject and HTML
          content:
            application/json:
              schema:
                type: object
                required: [object, event_type, subject, html]
                properties:
                  object:
                    type: string
                    enum: [email_preview]
                  event_type:
                    type: string
                  subject:
                    type: string
                  html:
                    type: string
        '422':
          description: Not a notifiable event (invalid_event_type)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /test/customers/{customer_id}/kyc:
    post:
      tags: [Test]
      operationId: simulateKycOutcome
      summary: 'Sandbox: force a KYC outcome'
      parameters:
        - $ref: '#/components/parameters/customer_id'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [outcome]
              properties:
                outcome:
                  type: string
                  enum: [approved, rejected, resubmission_required]
      responses:
        '200':
          description: Outcome applied; corresponding events emitted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'

  /test/accounts/{account_id}/deposits:
    post:
      tags: [Test]
      operationId: simulateDeposit
      summary: 'Sandbox: simulate an inbound fiat deposit'
      parameters:
        - name: account_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount]
              properties:
                amount:
                  type: string
                  example: '250.00'
                sender_name:
                  type: string
                  example: ACME GmbH
      responses:
        '201':
          description: Deposit simulated; deposit events emitted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Transfer'

  /test/transfers/{transfer_id}:
    post:
      tags: [Test]
      operationId: simulateTransferOutcome
      summary: 'Sandbox: force a transfer outcome'
      parameters:
        - name: transfer_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [outcome]
              properties:
                outcome:
                  type: string
                  enum: [completed, failed, returned]
      responses:
        '200':
          description: Outcome applied; corresponding events emitted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Transfer'

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: Secret API key (mk_test_... or mk_live_...)

  parameters:
    customer_id:
      name: customer_id
      in: path
      required: true
      schema:
        type: string
        example: cus_9f3k2m1x
    idempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        maxLength: 255
    limit:
      name: limit
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
    cursor:
      name: cursor
      in: query
      schema:
        type: string

  responses:
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

  schemas:
    FiatCurrency:
      type: string
      enum: [USD, EUR, GBP, NGN]
      description: Currencies for which virtual accounts can be provisioned.

    PayoutCurrency:
      type: string
      description: |
        Payout corridor currency. The live set is served by GET /corridors.
      example: BRL

    CustomerStatus:
      type: string
      enum: [created, kyc_required, kyc_pending, active, rejected, suspended]
      description: |
        created: record exists, KYC not started.
        kyc_required: KYC must be completed before money movement.
        kyc_pending: verification in progress.
        active: fully verified; all features available.
        rejected: verification failed permanently.
        suspended: temporarily blocked by compliance.

    CustomerCreate:
      type: object
      required: [email, first_name, last_name, country, wallet]
      properties:
        email:
          type: string
          format: email
        first_name:
          type: string
        last_name:
          type: string
        country:
          type: string
          description: ISO 3166-1 alpha-2 residence country
          example: DE
        wallet:
          type: object
          required: [address]
          description: |
            The customer's own wallet (external or the integrator's embedded
            wallet). Self-custody only: the platform never generates or
            stores keys; all proceeds settle to this address.
          properties:
            address:
              type: string
        metadata:
          type: object
          maxProperties: 20
          additionalProperties:
            type: string
            maxLength: 500
          description: |
            Up to 20 free-form keys, returned on every read. Keys must
            match ^[A-Za-z0-9_-]{1,40}$ (no dots or dollar signs).

    Customer:
      type: object
      properties:
        id:
          type: string
          example: cus_9f3k2m1x
        object:
          type: string
          enum: [customer]
        status:
          $ref: '#/components/schemas/CustomerStatus'
        email:
          type: string
        first_name:
          type: string
        last_name:
          type: string
        country:
          type: string
        settlement_address:
          type: string
          nullable: true
        wallet:
          type: object
          description: The customer's self-custodial wallet.
          properties:
            custody:
              type: string
              enum: [self]
            address:
              type: string
              nullable: true
        profile:
          $ref: '#/components/schemas/CustomerProfile'
        metadata:
          type: object
          additionalProperties:
            type: string
        created_at:
          type: string
          format: date-time

    CustomerProfile:
      type: object
      nullable: true
      description: |
        API-submitted profile data, null until something is submitted.
        Sensitive identifiers appear masked only (kind plus last four);
        raw values are never stored or returned.
      properties:
        date_of_birth:
          type: string
          format: date
          nullable: true
        occupation:
          type: string
          nullable: true
        address:
          type: object
          nullable: true
          properties:
            line1:
              type: string
            city:
              type: string
              nullable: true
            postal_code:
              type: string
              nullable: true
            country:
              type: string
              nullable: true
        phone:
          type: string
          nullable: true
        national_id:
          type: object
          nullable: true
          properties:
            kind:
              type: string
              enum: [ssn, bvn]
            last4:
              type: string

    CustomerProfileUpdate:
      type: object
      description: |
        All fields optional; submit what your form collected. Which fields
        are accepted depends on the customer's residence country
        (GET /jurisdictions/{cc}/profile_schema).
      properties:
        date_of_birth:
          type: string
          format: date
        occupation:
          type: string
        address:
          type: object
          required: [line1, city, postal_code, country]
          properties:
            line1:
              type: string
            city:
              type: string
            postal_code:
              type: string
            country:
              type: string
              description: ISO 3166-1 alpha-2
        phone:
          type: string
          description: E.164, e.g. +14155550123
        ssn:
          type: string
          description: US only. Delivered to the KYC provider, stored masked.
        bvn:
          type: string
          description: Nigeria only. Delivered to the KYC provider, stored masked.
        metadata:
          type: object
          maxProperties: 20
          description: |
            Keys must match ^[A-Za-z0-9_-]{1,40}$ (no dots or dollar
            signs); values up to 500 characters.
          additionalProperties:
            type: string
            maxLength: 500

    CustomerList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Customer'
        next_cursor:
          type: string
          nullable: true

    KycSession:
      type: object
      properties:
        id:
          type: string
          example: kyc_7h2n4p
        object:
          type: string
          enum: [kyc_session]
        url:
          type: string
          description: Hosted verification URL, valid 30 minutes
        expires_at:
          type: string
          format: date-time

    Account:
      type: object
      properties:
        id:
          type: string
          example: acct_2b8v5q
        object:
          type: string
          enum: [account]
        customer_id:
          type: string
        currency:
          $ref: '#/components/schemas/FiatCurrency'
        status:
          type: string
          enum: [provisioning, active, closed]
        deposit_details:
          type: object
          nullable: true
          description: Present once status is active
          properties:
            iban:
              type: string
              nullable: true
            bic:
              type: string
              nullable: true
            account_number:
              type: string
              nullable: true
            routing_number:
              type: string
              nullable: true
            sort_code:
              type: string
              nullable: true
            bank_name:
              type: string
              nullable: true
        created_at:
          type: string
          format: date-time

    Corridor:
      type: object
      properties:
        currency:
          type: string
          example: BRL
        country:
          type: string
          example: BR
        transfer_methods:
          type: array
          items:
            type: string
          example: [pix]
        minimum_usd:
          type: string
          description: Live minimum in USD equivalent
          example: '5.00'
        status:
          type: string
          enum: [active, degraded, unavailable]

    NotificationSettings:
      type: object
      required: [object, branded_emails, event_types, notifiable_events]
      properties:
        object:
          type: string
          enum: [notification_settings]
        branded_emails:
          type: boolean
          description: Master switch; off by default
        sender_name:
          type: string
          nullable: true
        from_email:
          type: string
          nullable: true
          description: |
            From address on one of your verified sending domains. Used
            only while that domain is verified; otherwise sends fall back
            to the shared domain.
        logo_url:
          type: string
          nullable: true
        brand_color:
          type: string
          nullable: true
          example: '#0E8C82'
        reply_to:
          type: string
          nullable: true
        footer_text:
          type: string
          nullable: true
        event_types:
          type: array
          description: Events that trigger an email; empty means all notifiable events
          items:
            type: string
        notifiable_events:
          type: array
          description: The full set of events that can trigger an email
          items:
            type: string
        custom_templates:
          type: array
          description: Events whose default layout is replaced by a custom template
          items:
            type: string

    NotificationDomain:
      type: object
      required: [id, object, domain, status, dns_records]
      properties:
        id:
          type: string
          example: nd_8f2k1m
        object:
          type: string
          enum: [notification_domain]
        domain:
          type: string
          example: acme.com
        status:
          type: string
          enum: [pending_verification, verified]
        dns_records:
          type: array
          items:
            type: object
            required: [type, host, value]
            properties:
              type:
                type: string
                enum: [TXT, CNAME]
              host:
                type: string
              value:
                type: string
        verified_at:
          type: string
          format: date-time
          nullable: true

    NotificationSettingsUpdate:
      type: object
      properties:
        branded_emails:
          type: boolean
        sender_name:
          type: string
          maxLength: 60
        from_email:
          type: string
          format: email
          description: Must belong to an added sending domain (422 domain_not_added otherwise)
        logo_url:
          type: string
          description: https URL of your logo
          maxLength: 500
        brand_color:
          type: string
          pattern: '^#[0-9a-fA-F]{6}$'
        reply_to:
          type: string
          format: email
        footer_text:
          type: string
          maxLength: 300
        event_types:
          type: array
          items:
            type: string

    ProfileSchema:
      type: object
      required: [country, fields]
      properties:
        country:
          type: string
          example: US
        note:
          type: string
          description: Human-readable guidance for rendering the form
        fields:
          type: array
          items:
            type: object
            required: [key, label, type, required, sensitive]
            properties:
              key:
                type: string
                description: |
                  Dotted keys (address.line1) denote nesting: submit them
                  as a nested object in the PATCH body, e.g.
                  {"address": {"line1": ...}}.
                example: date_of_birth
              label:
                type: string
                description: Human-readable field label for rendering forms
                example: Date of birth
              type:
                type: string
                enum: [text, date, phone, digits, select]
              required:
                type: boolean
              sensitive:
                type: boolean
                description: Stored masked (kind plus last four) when true
              pattern:
                type: string
                nullable: true
                description: Validation regex where applicable
              options:
                type: array
                nullable: true
                items:
                  type: object
                  properties:
                    value:
                      type: string
                    label:
                      type: string

    BeneficiarySchema:
      type: object
      properties:
        currency:
          type: string
        country:
          type: string
        transfer_methods:
          type: array
          items:
            type: string
        fields:
          type: array
          items:
            type: object
            properties:
              key:
                type: string
                example: pix_key
              label:
                type: string
                example: Pix key
              type:
                type: string
                enum: [text, select, digits]
              required:
                type: boolean
              pattern:
                type: string
                nullable: true
                description: Validation regex where applicable
              options:
                type: array
                nullable: true
                items:
                  type: object
                  properties:
                    value:
                      type: string
                    label:
                      type: string

    BeneficiaryCreate:
      type: object
      required: [currency, account_details]
      properties:
        currency:
          type: string
        transfer_method:
          type: string
          nullable: true
        account_details:
          type: object
          additionalProperties: true
          description: Keys per the corridor's beneficiary schema

    Beneficiary:
      type: object
      properties:
        id:
          type: string
          example: ben_5j8w2e
        object:
          type: string
          enum: [beneficiary]
        customer_id:
          type: string
        currency:
          type: string
        display_name:
          type: string
          description: Normalized holder name plus masked account reference
        status:
          type: string
          enum: [active, rejected]
        created_at:
          type: string
          format: date-time

    QuoteCreate:
      type: object
      required: [customer_id, source, destination_currency]
      properties:
        customer_id:
          type: string
        source:
          type: object
          required: [currency, amount]
          properties:
            currency:
              type: string
              example: USDC
            amount:
              type: string
              example: '100.00'
        destination_currency:
          type: string
          example: BRL

    Quote:
      type: object
      properties:
        id:
          type: string
          example: qt_8d3f6a
        object:
          type: string
          enum: [quote]
        rate:
          type: string
          description: Mid-market rate, no embedded spread
        fees:
          type: array
          items:
            type: object
            properties:
              type:
                type: string
                example: transfer_fee
              amount:
                type: string
              currency:
                type: string
        net_receivable:
          type: object
          properties:
            amount:
              type: string
            currency:
              type: string
        minimum_usd:
          type: string
        expires_at:
          type: string
          format: date-time

    TransferStatus:
      type: string
      enum: [created, processing, completed, failed, returned]

    TransferCreate:
      type: object
      required: [customer_id, type]
      properties:
        customer_id:
          type: string
        type:
          type: string
          enum: [payout, remittance]
        quote_id:
          type: string
          description: Quoted values are frozen onto the transfer
        beneficiary_id:
          type: string
        source:
          type: object
          properties:
            currency:
              type: string
              example: USDC
            amount:
              type: string
        metadata:
          type: object
          additionalProperties:
            type: string

    Transfer:
      type: object
      properties:
        id:
          type: string
          example: tr_4c7m9k
        object:
          type: string
          enum: [transfer]
        customer_id:
          type: string
        type:
          type: string
          enum: [payout, remittance, deposit]
        status:
          $ref: '#/components/schemas/TransferStatus'
        source:
          type: object
          properties:
            currency:
              type: string
            amount:
              type: string
        destination:
          type: object
          properties:
            currency:
              type: string
            amount:
              type: string
              description: Frozen from the quote at creation
            beneficiary_id:
              type: string
              nullable: true
        fees:
          type: array
          items:
            type: object
            properties:
              type:
                type: string
              amount:
                type: string
              currency:
                type: string
        timeline:
          type: array
          description: Append-only status history with timestamps
          items:
            type: object
            properties:
              status:
                type: string
              at:
                type: string
                format: date-time
        failure_reason:
          type: string
          nullable: true
          description: Stable machine-readable code when status is failed
        metadata:
          type: object
          additionalProperties:
            type: string
        created_at:
          type: string
          format: date-time

    TransferList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Transfer'
        next_cursor:
          type: string
          nullable: true

    Event:
      type: object
      properties:
        id:
          type: string
          example: evt_1a9s8d
        object:
          type: string
          enum: [event]
        type:
          type: string
          example: transfer.completed
        data:
          type: object
          description: The full resource at event time
        created_at:
          type: string
          format: date-time

    EventList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Event'
        next_cursor:
          type: string
          nullable: true

    WebhookEndpointCreate:
      type: object
      required: [url]
      properties:
        url:
          type: string
          format: uri
        event_types:
          type: array
          description: Omit to receive all event types
          items:
            type: string

    WebhookEndpoint:
      type: object
      properties:
        id:
          type: string
          example: we_6p2r8t
        object:
          type: string
          enum: [webhook_endpoint]
        url:
          type: string
        event_types:
          type: array
          items:
            type: string
        secret:
          type: string
          description: Signing secret. Returned only on creation.
        status:
          type: string
          enum: [active, disabled]
        created_at:
          type: string
          format: date-time

    Error:
      type: object
      properties:
        code:
          type: string
          example: amount_below_minimum
        message:
          type: string
        details:
          type: object
          nullable: true
          description: 'Structured context, for example {"minimum_usd": "5.00"}'

    ValidationError:
      type: object
      properties:
        code:
          type: string
          enum: [validation_failed]
        message:
          type: string
        fields:
          type: array
          items:
            type: object
            properties:
              key:
                type: string
              code:
                type: string
                example: invalid_format
              message:
                type: string
