> ## Documentation Index
> Fetch the complete documentation index at: https://docs.capa.fi/llms.txt
> Use this file to discover all available pages before exploring further.

# Create Off-Ramp

Creates an off-ramp transaction on behalf of a **verified** user. This converts crypto to fiat and initiates a withdrawal to the user's registered bank account.

<Note>
  For staging transaction testing, use the [Mock Testing Guide](/reference/transactionpartnerv2controller_getmocktestingguide).
</Note>

***

## Idempotency

Retry safely without creating duplicate transactions: pass an optional **`Idempotency-Key`** header (UUIDv4). A retry with the same key and body replays the original response. See the [Idempotency Guide](/docs/idempotency) for the full contract.

```shell theme={null}
curl --request POST \
  --url https://staging-api.capa.fi/api/partner/v2/off-ramp \
  --header 'Content-Type: application/json' \
  --header 'partner-api-key: <partner-api-key>' \
  --header 'Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000' \
  --data '...'
```

***

## Field Relationships

* Provide `quoteId` **OR** (`fiatCurrency` + `blockchainSymbol` + `tokenSymbol`). When a quote is used, currency/token fields are inherited from the quote.
* Provide either `fiatAmount` or `cryptoAmount`, not both.
* Provide either `userBankInformation` (inline bank details) or `userBankInformationId` (reference to a saved bank account).
* `receiverId` is optional — when provided, the off-ramp is processed for that receiver instead of the user directly.

***

## Bank Account Requirements by Country

| Country  | Required Fields                                                                      |
| -------- | ------------------------------------------------------------------------------------ |
| **MX**   | `accountIdentifier` (18-digit CLABE)                                                 |
| **DO**   | `accountIdentifier`, `bankName`, `accountType`, `documentIdentifier`, `documentType` |
| **US**   | `accountIdentifier`, `bankName`, `routingNumber`, `accountHolder`, `address`         |
| **SEPA** | `iban`, `bic`, `bankName`, `accountHolder`                                           |

***

## Integration Flow

<Steps>
  <Step title="Create a user">
    [POST /api/partner/v2/users](/reference/userpartnercontrollerv2_createuser)
  </Step>

  <Step title="Complete KYC verification">
    [POST /api/partner/v2/users/{userId}/kyc/verification-link](/reference/userpartnercontrollerv2_createverificationlink)
  </Step>

  <Step title="Get a quote (optional)">
    [POST /api/partner/v2/quotes](/reference/quotepartnerv2controller_createpartnerquote) — locks the exchange rate
  </Step>

  <Step title="Create off-ramp transaction">
    `POST /api/partner/v2/off-ramp` (this endpoint) — returns a `destinationWalletAddress`
  </Step>

  <Step title="User sends crypto">
    The user sends crypto to the `destinationWalletAddress` from the response.
  </Step>

  <Step title="Execute the off-ramp">
    [POST /api/partner/v2/off-ramp/execute](/reference/offramppartnerv2controller_executeofframp) — triggers bank payout after crypto is received
  </Step>
</Steps>

***

## Important Notes

* **User must be KYC-verified** before creating transactions.
* **Mexico bank verification**: First-time bank accounts trigger a penny test to verify the KYC'd user is the account owner. If verification fails, the transaction is cancelled.
* **CLABE validation (Mexico)**: Must match `^\d{18}$`. Consider using [clabe-validator](https://www.npmjs.com/package/clabe-validator).
* **Amount limits**: Fiat and crypto amounts must fall within the min/max thresholds defined in your partner agreement.
* **Destination wallet**: The response includes a unique `destinationWalletAddress` — the user must send crypto to this address.

***

## Use Cases

* **User Payout**: Convert crypto to fiat and withdraw to a verified bank account.
* **B2C Integration**: Let individual users off-ramp from within your app.
* **Third-Party Payments**: Use `receiverId` to send fiat payouts to receivers on behalf of a user.

***

## Error Codes

### Common Errors

| HTTP Status | Code                   | Message                            |
| ----------- | ---------------------- | ---------------------------------- |
| 401         | `UNAUTHORIZED`         | "API Key is missing"               |
| 401         | `UNAUTHORIZED`         | "Invalid API Key format"           |
| 401         | `UNAUTHORIZED`         | "Invalid API Key"                  |
| 403         | `INVALID_PARTNER_FLOW` | "The partner has an invalid flow." |

### Verified User Errors

| HTTP Status | Code                      | Message                                                                                         |
| ----------- | ------------------------- | ----------------------------------------------------------------------------------------------- |
| 400         | `REQUIRED_USER_ID_ERROR`  | "This endpoint requires a user id to be provided"                                               |
| 403         | `USER_NOT_VERIFIED_ERROR` | "User is not allowed to perform the operations because has not completed the KYC verification." |

### Endpoint-Specific Errors

| HTTP Status | Code                                   | Message                                                                                                                          |
| ----------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| 400         | `INVALID_USER_INPUT_ERROR`             | "Invalid User Input"                                                                                                             |
| 400         | `BAD_REQUEST`                          | "Either quoteId or fiatCurrency, blockchainSymbol, tokenSymbol, and at least one of fiatAmount or cryptoAmount must be provided" |
| 400         | `BAD_REQUEST`                          | "Fiat currency is disabled or does not exist"                                                                                    |
| 400         | `BAD_REQUEST`                          | "This blockchain and token combination is disabled or does not exist"                                                            |
| 400         | `QUOTE_EXPIRED`                        | "Quote has expired"                                                                                                              |
| 400         | `BAD_REQUEST`                          | "Quote is not valid for OFF\_RAMP"                                                                                               |
| 400         | `INVALID_TOKEN_OPERATION_ERROR`        | "Token {symbol} cannot be operated as OTC, only stable coins are allowed"                                                        |
| 400         | `BAD_REQUEST`                          | "Either userBankInformationId or userBankInformation must be provided"                                                           |
| 400         | `BAD_REQUEST`                          | "The receiver user account is not verified"                                                                                      |
| 400         | `INVALID_FIAT_AMOUNT_ERROR`            | "Fiat amount {amount} is outside of the allowed range for OFF\_RAMP. Should be between {min} and {max}"                          |
| 400         | `INVALID_TOKEN_AMOUNT_ERROR`           | "Token amount {amount} is outside of the allowed range for OFF\_RAMP. Should be between {min} and {max}"                         |
| 403         | `RECEIVER_CANNOT_INITIATE_TRANSACTION` | "Receiver accounts cannot initiate transactions directly."                                                                       |
| 403         | `RECEIVER_USER_MISMATCH`               | "The receiver does not belong to the specified user."                                                                            |
| 403         | `USER_BANK_INFO_ACCESS_DENIED`         | "The bank information does not belong to the specified user."                                                                    |
| 403         | `USER_BANK_INFO_ACCESS_DENIED`         | "The bank information belongs to a different partner."                                                                           |
| 404         | `RECEIVER_NOT_FOUND`                   | "Receiver not found."                                                                                                            |
| 404         | `USER_BANK_INFO_NOT_FOUND`             | "User bank information not found."                                                                                               |


## OpenAPI

````yaml reference/openapi/OffRampPartnerV2Controller_createPartnerOffRamp.json POST /api/partner/v2/off-ramp
openapi: 3.0.0
info:
  title: Capa Partner API - V2
  description: >-
    Partner API for integrating cryptocurrency on-ramp and off-ramp services
    using Capa's infrastructure. Enable your users to seamlessly convert between
    fiat and crypto.
  version: v2
  contact: {}
servers:
  - url: https://staging-api.capa.fi
  - url: https://production-api.capa.fi
security: []
paths:
  /api/partner/v2/off-ramp:
    post:
      tags:
        - off-ramp
      operationId: OffRampPartnerV2Controller_createPartnerOffRamp
      parameters:
        - name: Idempotency-Key
          in: header
          description: >-
            Optional. Unique key (UUIDv4 recommended) to safely retry this
            request without creating a duplicate transaction. A retry with the
            same key and body replays the original response. See
            /docs/idempotency.
          required: false
          schema:
            type: string
            minLength: 16
            maxLength: 128
            example: 550e8400-e29b-41d4-a716-446655440000
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                userBankInformation:
                  type: object
                  description: Inline bank account details for the off-ramp.
                  properties:
                    accountIdentifier:
                      type: string
                      description: Unique identifier for bank account. CLABE if country=MX.
                    country:
                      type: string
                      description: Required. Determines validation rules
                      enum:
                        - MX
                        - DO
                        - US
                        - AT
                        - BE
                        - BG
                        - HR
                        - CY
                        - CZ
                        - DK
                        - EE
                        - FI
                        - FR
                        - DE
                        - GR
                        - HU
                        - IE
                        - IT
                        - LV
                        - LT
                        - LU
                        - MT
                        - NL
                        - PL
                        - PT
                        - RO
                        - SK
                        - SI
                        - ES
                        - SE
                        - IS
                        - LI
                        - 'NO'
                        - CH
                        - GB
                        - MC
                        - SM
                        - AD
                        - VA
                    accountType:
                      type: string
                      description: Required for DO
                      enum:
                        - SAVINGS
                        - CHECKING
                    bankName:
                      type: string
                      description: Required for DO / US / SEPA. Not allowed for MX.
                    documentIdentifier:
                      type: string
                      description: Required only for DO
                    documentType:
                      type: string
                      description: Required only for DO
                    routingNumber:
                      type: string
                      description: Required only for US (9-digit ABA).
                    address:
                      type: object
                      description: Required only for US (account address).
                      properties:
                        streetLine1:
                          type: string
                          description: Street address line 1. Required.
                          example: 123 Main St
                        streetLine2:
                          type: string
                          description: Street address line 2. Optional.
                        city:
                          type: string
                          description: City. Required.
                          example: New York
                        state:
                          type: string
                          description: State/Province. Optional.
                        postalCode:
                          type: string
                          description: Postal/ZIP code. Required.
                          example: '10001'
                        country:
                          type: string
                          description: >-
                            ISO 3166-1 alpha-2 country code. Required. Must be
                            US.
                          enum:
                            - US
                          example: US
                      required:
                        - streetLine1
                        - city
                        - postalCode
                        - country
                    bic:
                      type: string
                      description: Required only for SEPA (EU).
                    iban:
                      type: string
                      description: Required only for SEPA (EU).
                    accountHolder:
                      type: object
                      description: Required for US & SEPA (INDIVIDUAL | BUSINESS).
                      properties:
                        type:
                          type: string
                          enum:
                            - INDIVIDUAL
                            - BUSINESS
                          description: Type of account holder. Required.
                          example: INDIVIDUAL
                        firstName:
                          type: string
                          description: First name. Required when type=INDIVIDUAL.
                        lastName:
                          type: string
                          description: Last name. Required when type=INDIVIDUAL.
                        businessName:
                          type: string
                          description: Business name. Required when type=BUSINESS.
                      required:
                        - type
                quoteId:
                  type: string
                  description: Identifier for the quote to be used for the transaction.
                userId:
                  type: string
                  description: Identifier for the user who's submitting the off-ramp order
                  example: 8374f327-38bd-4b0b-b8a7-2524599eb903
                fiatAmount:
                  type: number
                  description: Amount of fiat currency to be received in conversion.
                cryptoAmount:
                  type: number
                  description: Amount of crypto currency to be converted to fiat currency.
                fiatCurrency:
                  description: |-
                    Identifier for the fiat currency which the user will rec.
                    Required when quoteId is not provided.
                  enum:
                    - MXN
                    - DOP
                    - USD
                    - EUR
                  type: string
                blockchainSymbol:
                  description: >-
                    Identifier for the blockchain to token from which the
                    conversion will be made.

                    Required when quoteId is not provided.

                    BSC is only available for transactions over 50,000 MXN.
                  enum:
                    - POL
                    - SOL
                    - ETH
                    - BSC
                  type: string
                tokenSymbol:
                  description: >-
                    Identifier for the token from which the conversion will be
                    made.

                    Required when quoteId is not provided.
                  enum:
                    - USDC
                    - USDT
                  type: string
                premiumSpread:
                  type: number
                  description: Spread percentage to be applied to the exchange rate
                receiverId:
                  type: string
                  description: >-
                    Identifier for a previously created receiver (via
                    /api/partner/v2/receivers).
              required:
                - userId
              x-readme-ref-name: CreatePartnerOffRampTransactionV2RequestBody
      responses:
        '201':
          description: >-
            Off-ramp transaction created successfully. Returns the transaction
            details, including its id and current status.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        description: Transaction identifier
                        example: 63f51f11-6869-47b0-a109-ddb50ef20efb
                      userId:
                        type: string
                        description: User identifier
                        example: 8374f327-38bd-4b0b-b8a7-2524599eb903
                      status:
                        type: string
                        description: Transaction status
                        example: PENDING_PAYMENT
                      cryptoAmount:
                        type: number
                        description: Amount of crypto currency involved in the transaction
                        example: 2500
                      fiatAmount:
                        type: number
                        description: Amount of fiat currency involved in the transaction
                        example: 50000
                      crossFiatAmount:
                        type: number
                      exchangeRate:
                        type: number
                        description: Exchange rate used for the transaction
                        example: 19.53964594161554
                      tokenSymbol:
                        description: Token symbol
                        example: USDC
                        enum:
                          - USDC
                          - USDT
                        type: string
                      blockchainSymbol:
                        description: Blockchain symbol
                        example: POL
                        enum:
                          - POL
                          - SOL
                          - ETH
                          - BSC
                        type: string
                      fiatCurrency:
                        description: Fiat currency symbol
                        example: MXN
                        enum:
                          - MXN
                          - DOP
                          - USD
                          - EUR
                        type: string
                      premiumSpread:
                        type: number
                        description: >-
                          Premium spread applied to the transaction (0 if not
                          provided)
                        example: 0.01
                      forwardingDays:
                        type: number
                        description: >-
                          Number of T+N forwarding days for settlement (0-4).
                          Only for MXN OTC transactions.
                        example: 2
                      effectiveForwardingDays:
                        type: number
                        description: >-
                          Effective forwarding days for settlement (may differ
                          from requested due to holidays/weekends).
                        example: 4
                      forwardingSettlementDate:
                        format: date-time
                        type: string
                        description: >-
                          Calculated settlement date based on effective
                          forwarding days offset.
                        example: '2026-03-14T03:25:27.495Z'
                      createdAt:
                        type: string
                        description: Transaction creation date
                        example: '2025-05-14T10:00:00Z'
                      completedAt:
                        type: string
                        nullable: true
                        description: Transaction completion date (null if not completed)
                      destinationWalletAddress:
                        type: string
                        description: Destination wallet address for the transaction
                        example: '0x7796d4f304bd84171ee6730ad0f69c07a47e786d'
                      bankAccount:
                        description: Bank account information
                        allOf:
                          - type: object
                            properties:
                              country:
                                type: string
                                description: The country of the bank account
                                example: MX
                              accountIdentifier:
                                type: string
                                description: >-
                                  The account identifier for the bank account
                                  (CLABE in MX, account number elsewhere)
                                example: '014680260346007120'
                              bankName:
                                type: string
                                description: The name of the bank
                                example: Santander
                              accountType:
                                type: string
                                description: The type of the account
                                example: SAVINGS
                              isVerified:
                                type: boolean
                                description: Whether the account is verified
                                example: true
                              documentIdentifier:
                                type: string
                                description: >-
                                  The document identifier used for verification
                                  (e.g. national tax / ID number)
                                example: 123abc
                              documentType:
                                type: string
                                description: >-
                                  Type of the document bound to the account
                                  (country specific)
                                example: RNC
                              iban:
                                type: string
                                description: >-
                                  International Bank Account Number (SEPA / EU
                                  accounts)
                                example: DE89370400440532013000
                              bic:
                                type: string
                                description: >-
                                  Bank Identifier Code (SWIFT BIC) for SEPA / EU
                                  accounts
                                example: DEUTDEFF
                              routingNumber:
                                type: string
                                description: ABA routing number (US only, 9 digits)
                                example: '021000021'
                            required:
                              - country
                              - accountIdentifier
                              - isVerified
                            x-readme-ref-name: BankAccountResponse
                    required:
                      - id
                      - userId
                      - status
                      - cryptoAmount
                      - fiatAmount
                      - exchangeRate
                      - tokenSymbol
                      - blockchainSymbol
                      - fiatCurrency
                      - createdAt
                      - completedAt
                      - destinationWalletAddress
                      - bankAccount
                    x-readme-ref-name: OffRampPartnerV2Response
              example:
                success: true
                data:
                  id: 63f51f11-6869-47b0-a109-ddb50ef20efb
                  userId: 8374f327-38bd-4b0b-b8a7-2524599eb903
                  status: PENDING_PAYMENT
                  cryptoAmount: 2500
                  fiatAmount: 50000
                  crossFiatAmount: 0
                  exchangeRate: 19.53964594161554
                  tokenSymbol: USDC
                  blockchainSymbol: POL
                  fiatCurrency: MXN
                  premiumSpread: 0.01
                  forwardingDays: 2
                  effectiveForwardingDays: 4
                  forwardingSettlementDate: '2026-03-14T03:25:27.495Z'
                  createdAt: '2025-05-14T10:00:00Z'
                  completedAt: null
                  destinationWalletAddress: '0x7796d4f304bd84171ee6730ad0f69c07a47e786d'
                  bankAccount:
                    country: MX
                    accountIdentifier: '014680260346007120'
                    bankName: Santander
                    isVerified: true
        '400':
          description: Bad Request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                REQUIRED_USER_ID_ERROR:
                  value:
                    success: false
                    code: REQUIRED_USER_ID_ERROR
                    message: This endpoint requires a user id to be provided
                INVALID_USER_INPUT_ERROR:
                  value:
                    success: false
                    code: INVALID_USER_INPUT_ERROR
                    message: Invalid User Input
                BAD_REQUEST:
                  value:
                    success: false
                    code: BAD_REQUEST
                    message: >-
                      Either quoteId or fiatCurrency, blockchainSymbol,
                      tokenSymbol, and at least one of fiatAmount or
                      cryptoAmount must be provided
                BAD_REQUEST_2:
                  value:
                    success: false
                    code: BAD_REQUEST
                    message: Fiat currency is disabled or does not exist
                BAD_REQUEST_3:
                  value:
                    success: false
                    code: BAD_REQUEST
                    message: >-
                      This blockchain and token combination is disabled or does
                      not exist
                BAD_REQUEST_4:
                  value:
                    success: false
                    code: BAD_REQUEST
                    message: Quote is not valid for OFF_RAMP
                QUOTE_EXPIRED:
                  value:
                    success: false
                    code: QUOTE_EXPIRED
                    message: Quote has expired
                INVALID_FIAT_AMOUNT_ERROR:
                  value:
                    success: false
                    code: INVALID_FIAT_AMOUNT_ERROR
                    message: >-
                      Fiat amount {amount} is outside of the allowed range for
                      OFF_RAMP. Should be between {min} and {max}
                INVALID_TOKEN_AMOUNT_ERROR:
                  value:
                    success: false
                    code: INVALID_TOKEN_AMOUNT_ERROR
                    message: >-
                      Token amount {amount} is outside of the allowed range for
                      OFF_RAMP. Should be between {min} and {max}
                INVALID_TOKEN_OPERATION_ERROR:
                  value:
                    success: false
                    code: INVALID_TOKEN_OPERATION_ERROR
                    message: >-
                      Token {symbol} cannot be operated as OTC, only stable
                      coins are allowed
                BAD_REQUEST_5:
                  value:
                    success: false
                    code: BAD_REQUEST
                    message: >-
                      Either userBankInformationId or userBankInformation must
                      be provided
                BAD_REQUEST_6:
                  value:
                    success: false
                    code: BAD_REQUEST
                    message: The receiver user account is not verified
                BAD_REQUEST_7:
                  value:
                    success: false
                    code: BAD_REQUEST
                    message: >-
                      forwardingDays can only be specified for partners with
                      manual off-ramp enabled
                INVALID_USER_INPUT_ERROR_2:
                  value:
                    success: false
                    code: INVALID_USER_INPUT_ERROR
                    message: >-
                      The bank "{bankName}" is not supported for {country}.
                      Allowed banks: {list}
                UNSUPPORTED_BANK:
                  value:
                    success: false
                    code: UNSUPPORTED_BANK
                    message: >-
                      The provided bank: {bankName} is not currently supported
                      for this transaction.
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                UNAUTHORIZED:
                  value:
                    success: false
                    code: UNAUTHORIZED
                    message: API Key is missing
                UNAUTHORIZED_2:
                  value:
                    success: false
                    code: UNAUTHORIZED
                    message: Invalid API Key format
                UNAUTHORIZED_3:
                  value:
                    success: false
                    code: UNAUTHORIZED
                    message: Invalid API Key
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                USER_NOT_VERIFIED_ERROR:
                  value:
                    success: false
                    code: USER_NOT_VERIFIED_ERROR
                    message: >-
                      User is not allowed to perform the operations because has
                      not completed the KYC verification.
                RECEIVER_CANNOT_INITIATE_TRANSACTION:
                  value:
                    success: false
                    code: RECEIVER_CANNOT_INITIATE_TRANSACTION
                    message: Receiver accounts cannot initiate transactions directly.
                RECEIVER_USER_MISMATCH:
                  value:
                    success: false
                    code: RECEIVER_USER_MISMATCH
                    message: The receiver does not belong to the specified user.
                USER_BANK_INFO_ACCESS_DENIED:
                  value:
                    success: false
                    code: USER_BANK_INFO_ACCESS_DENIED
                    message: >-
                      The bank information does not belong to the specified
                      user.
        '404':
          description: Not Found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                RECEIVER_NOT_FOUND:
                  value:
                    success: false
                    code: RECEIVER_NOT_FOUND
                    message: Receiver not found.
                USER_BANK_INFO_NOT_FOUND:
                  value:
                    success: false
                    code: USER_BANK_INFO_NOT_FOUND
                    message: User bank information not found.
                NOT_FOUND_ERROR:
                  value:
                    success: false
                    code: NOT_FOUND_ERROR
                    message: Requested resource was not found
        '409':
          description: Conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                code: CONFLICT
                message: >-
                  A request with this idempotency key is currently being
                  processed
        '422':
          description: Unprocessable Entity
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                code: INVALID_USER_INPUT_ERROR
                message: Idempotency-Key was already used with a different request body
        '500':
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                code: INTERNAL_SERVER_ERROR
                message: Internal server error
      security:
        - PartnerApiKey: []
components:
  schemas:
    ErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          example: false
        code:
          type: string
          description: Machine-readable error code.
        message:
          type: string
          description: Human-readable error message.
        errors:
          type: array
          description: Optional per-field validation details.
          items:
            type: object
            properties:
              field:
                type: string
              message:
                type: string
      required:
        - success
        - code
        - message
  securitySchemes:
    PartnerApiKey:
      type: apiKey
      in: header
      name: partner-api-key
      description: API key for the affiliated partner performing the request.

````