curl --request POST \
--url https://api.sandbox.capa.fi/api/partner/v2/off-ramp \
--header 'Content-Type: application/json' \
--header 'partner-api-key: <api-key>' \
--data '
{
"userId": "8374f327-38bd-4b0b-b8a7-2524599eb903",
"userBankInformation": {
"accountIdentifier": "<string>",
"bankName": "<string>",
"documentIdentifier": "<string>",
"documentType": "<string>",
"routingNumber": "<string>",
"bic": "<string>",
"iban": "<string>"
},
"quoteId": "<string>",
"fiatAmount": 123,
"cryptoAmount": 123,
"premiumSpread": 123
}
'import requests
url = "https://api.sandbox.capa.fi/api/partner/v2/off-ramp"
payload = {
"userId": "8374f327-38bd-4b0b-b8a7-2524599eb903",
"userBankInformation": {
"accountIdentifier": "<string>",
"bankName": "<string>",
"documentIdentifier": "<string>",
"documentType": "<string>",
"routingNumber": "<string>",
"bic": "<string>",
"iban": "<string>"
},
"quoteId": "<string>",
"fiatAmount": 123,
"cryptoAmount": 123,
"premiumSpread": 123
}
headers = {
"partner-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'partner-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
userId: '8374f327-38bd-4b0b-b8a7-2524599eb903',
userBankInformation: {
accountIdentifier: '<string>',
bankName: '<string>',
documentIdentifier: '<string>',
documentType: '<string>',
routingNumber: '<string>',
bic: '<string>',
iban: '<string>'
},
quoteId: '<string>',
fiatAmount: 123,
cryptoAmount: 123,
premiumSpread: 123
})
};
fetch('https://api.sandbox.capa.fi/api/partner/v2/off-ramp', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.sandbox.capa.fi/api/partner/v2/off-ramp",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'userId' => '8374f327-38bd-4b0b-b8a7-2524599eb903',
'userBankInformation' => [
'accountIdentifier' => '<string>',
'bankName' => '<string>',
'documentIdentifier' => '<string>',
'documentType' => '<string>',
'routingNumber' => '<string>',
'bic' => '<string>',
'iban' => '<string>'
],
'quoteId' => '<string>',
'fiatAmount' => 123,
'cryptoAmount' => 123,
'premiumSpread' => 123
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"partner-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.sandbox.capa.fi/api/partner/v2/off-ramp"
payload := strings.NewReader("{\n \"userId\": \"8374f327-38bd-4b0b-b8a7-2524599eb903\",\n \"userBankInformation\": {\n \"accountIdentifier\": \"<string>\",\n \"bankName\": \"<string>\",\n \"documentIdentifier\": \"<string>\",\n \"documentType\": \"<string>\",\n \"routingNumber\": \"<string>\",\n \"bic\": \"<string>\",\n \"iban\": \"<string>\"\n },\n \"quoteId\": \"<string>\",\n \"fiatAmount\": 123,\n \"cryptoAmount\": 123,\n \"premiumSpread\": 123\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("partner-api-key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.sandbox.capa.fi/api/partner/v2/off-ramp")
.header("partner-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"userId\": \"8374f327-38bd-4b0b-b8a7-2524599eb903\",\n \"userBankInformation\": {\n \"accountIdentifier\": \"<string>\",\n \"bankName\": \"<string>\",\n \"documentIdentifier\": \"<string>\",\n \"documentType\": \"<string>\",\n \"routingNumber\": \"<string>\",\n \"bic\": \"<string>\",\n \"iban\": \"<string>\"\n },\n \"quoteId\": \"<string>\",\n \"fiatAmount\": 123,\n \"cryptoAmount\": 123,\n \"premiumSpread\": 123\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.sandbox.capa.fi/api/partner/v2/off-ramp")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["partner-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"userId\": \"8374f327-38bd-4b0b-b8a7-2524599eb903\",\n \"userBankInformation\": {\n \"accountIdentifier\": \"<string>\",\n \"bankName\": \"<string>\",\n \"documentIdentifier\": \"<string>\",\n \"documentType\": \"<string>\",\n \"routingNumber\": \"<string>\",\n \"bic\": \"<string>\",\n \"iban\": \"<string>\"\n },\n \"quoteId\": \"<string>\",\n \"fiatAmount\": 123,\n \"cryptoAmount\": 123,\n \"premiumSpread\": 123\n}"
response = http.request(request)
puts response.read_body{
"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
}
}
}Create Off-Ramp
curl --request POST \
--url https://api.sandbox.capa.fi/api/partner/v2/off-ramp \
--header 'Content-Type: application/json' \
--header 'partner-api-key: <api-key>' \
--data '
{
"userId": "8374f327-38bd-4b0b-b8a7-2524599eb903",
"userBankInformation": {
"accountIdentifier": "<string>",
"bankName": "<string>",
"documentIdentifier": "<string>",
"documentType": "<string>",
"routingNumber": "<string>",
"bic": "<string>",
"iban": "<string>"
},
"quoteId": "<string>",
"fiatAmount": 123,
"cryptoAmount": 123,
"premiumSpread": 123
}
'import requests
url = "https://api.sandbox.capa.fi/api/partner/v2/off-ramp"
payload = {
"userId": "8374f327-38bd-4b0b-b8a7-2524599eb903",
"userBankInformation": {
"accountIdentifier": "<string>",
"bankName": "<string>",
"documentIdentifier": "<string>",
"documentType": "<string>",
"routingNumber": "<string>",
"bic": "<string>",
"iban": "<string>"
},
"quoteId": "<string>",
"fiatAmount": 123,
"cryptoAmount": 123,
"premiumSpread": 123
}
headers = {
"partner-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'partner-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
userId: '8374f327-38bd-4b0b-b8a7-2524599eb903',
userBankInformation: {
accountIdentifier: '<string>',
bankName: '<string>',
documentIdentifier: '<string>',
documentType: '<string>',
routingNumber: '<string>',
bic: '<string>',
iban: '<string>'
},
quoteId: '<string>',
fiatAmount: 123,
cryptoAmount: 123,
premiumSpread: 123
})
};
fetch('https://api.sandbox.capa.fi/api/partner/v2/off-ramp', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.sandbox.capa.fi/api/partner/v2/off-ramp",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'userId' => '8374f327-38bd-4b0b-b8a7-2524599eb903',
'userBankInformation' => [
'accountIdentifier' => '<string>',
'bankName' => '<string>',
'documentIdentifier' => '<string>',
'documentType' => '<string>',
'routingNumber' => '<string>',
'bic' => '<string>',
'iban' => '<string>'
],
'quoteId' => '<string>',
'fiatAmount' => 123,
'cryptoAmount' => 123,
'premiumSpread' => 123
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"partner-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.sandbox.capa.fi/api/partner/v2/off-ramp"
payload := strings.NewReader("{\n \"userId\": \"8374f327-38bd-4b0b-b8a7-2524599eb903\",\n \"userBankInformation\": {\n \"accountIdentifier\": \"<string>\",\n \"bankName\": \"<string>\",\n \"documentIdentifier\": \"<string>\",\n \"documentType\": \"<string>\",\n \"routingNumber\": \"<string>\",\n \"bic\": \"<string>\",\n \"iban\": \"<string>\"\n },\n \"quoteId\": \"<string>\",\n \"fiatAmount\": 123,\n \"cryptoAmount\": 123,\n \"premiumSpread\": 123\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("partner-api-key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.sandbox.capa.fi/api/partner/v2/off-ramp")
.header("partner-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"userId\": \"8374f327-38bd-4b0b-b8a7-2524599eb903\",\n \"userBankInformation\": {\n \"accountIdentifier\": \"<string>\",\n \"bankName\": \"<string>\",\n \"documentIdentifier\": \"<string>\",\n \"documentType\": \"<string>\",\n \"routingNumber\": \"<string>\",\n \"bic\": \"<string>\",\n \"iban\": \"<string>\"\n },\n \"quoteId\": \"<string>\",\n \"fiatAmount\": 123,\n \"cryptoAmount\": 123,\n \"premiumSpread\": 123\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.sandbox.capa.fi/api/partner/v2/off-ramp")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["partner-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"userId\": \"8374f327-38bd-4b0b-b8a7-2524599eb903\",\n \"userBankInformation\": {\n \"accountIdentifier\": \"<string>\",\n \"bankName\": \"<string>\",\n \"documentIdentifier\": \"<string>\",\n \"documentType\": \"<string>\",\n \"routingNumber\": \"<string>\",\n \"bic\": \"<string>\",\n \"iban\": \"<string>\"\n },\n \"quoteId\": \"<string>\",\n \"fiatAmount\": 123,\n \"cryptoAmount\": 123,\n \"premiumSpread\": 123\n}"
response = http.request(request)
puts response.read_body{
"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
}
}
}Idempotency
Retry safely without creating duplicate transactions: pass an optionalIdempotency-Key header (UUIDv4). A retry with the same key and body replays the original response. See the Idempotency Guide for the full contract.
curl --request POST \
--url https://api.sandbox.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
quoteIdOR (fiatCurrency+blockchainSymbol+tokenSymbol). When a quote is used, currency/token fields are inherited from the quote. - Provide either
fiatAmountorcryptoAmount, not both. - Provide either
userBankInformation(inline bank details) oruserBankInformationId(reference to a saved bank account).
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
Create a user
Complete KYC verification
Get a quote (optional)
Create off-ramp transaction
POST /api/partner/v2/off-ramp (this endpoint) — returns a destinationWalletAddressUser sends crypto
destinationWalletAddress from the response.Execute the off-ramp
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. - 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.
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 cannot be operated as OTC, only stable coins are allowed” |
| 400 | BAD_REQUEST | ”Either userBankInformationId or userBankInformation must be provided” |
| 400 | INVALID_FIAT_AMOUNT_ERROR | ”Fiat amount is outside of the allowed range for OFF_RAMP. Should be between and “ |
| 400 | INVALID_TOKEN_AMOUNT_ERROR | ”Token amount is outside of the allowed range for OFF_RAMP. Should be between and “ |
| 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 | USER_BANK_INFO_NOT_FOUND | ”User bank information not found.” |
Authorizations
API key for the affiliated partner performing the request.
Headers
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.
16 - 128"550e8400-e29b-41d4-a716-446655440000"
Body
Identifier for the user who's submitting the off-ramp order
"8374f327-38bd-4b0b-b8a7-2524599eb903"
Inline bank account details for the off-ramp.
Show child attributes
Show child attributes
Identifier for the quote to be used for the transaction.
Amount of fiat currency to be received in conversion.
Amount of crypto currency to be converted to fiat currency.
Identifier for the fiat currency which the user will rec. Required when quoteId is not provided.
MXN, DOP, USD, EUR 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.
POL, SOL, ETH, BSC Identifier for the token from which the conversion will be made. Required when quoteId is not provided.
USDC, USDT Spread percentage to be applied to the exchange rate