> For the complete documentation index, see [llms.txt](https://developers.unigox.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developers.unigox.com/api-reference/off-ramp.md).

# Off Ramp

Off-ramp operations (crypto to fiat) — sell crypto and receive fiat

## Get off-ramp price estimate

> Get indicative off-ramp pricing (crypto to fiat) without creating a quote.\
> Pricing can be driven from either side: provide \`crypto\_amount\` to compute the fiat payout, or \`fiat\_amount\` to compute the crypto required (exactly one of the two).\
> This endpoint is lightweight and does not reserve liquidity.\
> \
> Use this when you need quick pricing discovery before calling \`/partner/offramp/quote\`.\
> Optional filters: \`payment\_method\_slug\`, \`payment\_network\_slug\`, \`provider\_scope\`, \`country\_code\`.\
> This endpoint is indicative and does not apply user/payment-details ownership checks; for executable pricing use quote (which still requires an API key).\
> \
> \*\*Authentication:\*\* None. This is a public endpoint — no API key required (same as \`/partner/liquidity\`).\
> \
> \*\*Note:\*\* EUR, AUD, and GBP off-ramp payouts require the user's physical\
> address. Ensure the user's KYC includes \`address\`, \`city\`, and \`postal\_code\` before initiating.\
> See \`PATCH /partner/users/{user\_uuid}/kyc\` to update.<br>

```json
{"openapi":"3.0.3","info":{"title":"Unigox API Gateway","version":"1.0.0"},"tags":[{"name":"Off-Ramp","description":"Off-ramp operations (crypto to fiat) — sell crypto and receive fiat"}],"servers":[{"url":"https://api-staging.unigox.com","description":"Sandbox server"},{"url":"https://api.unigox.com","description":"Production server"}],"security":[],"paths":{"/api/v1/partner/offramp/estimate":{"post":{"tags":["Off-Ramp"],"summary":"Get off-ramp price estimate","description":"Get indicative off-ramp pricing (crypto to fiat) without creating a quote.\nPricing can be driven from either side: provide `crypto_amount` to compute the fiat payout, or `fiat_amount` to compute the crypto required (exactly one of the two).\nThis endpoint is lightweight and does not reserve liquidity.\n\nUse this when you need quick pricing discovery before calling `/partner/offramp/quote`.\nOptional filters: `payment_method_slug`, `payment_network_slug`, `provider_scope`, `country_code`.\nThis endpoint is indicative and does not apply user/payment-details ownership checks; for executable pricing use quote (which still requires an API key).\n\n**Authentication:** None. This is a public endpoint — no API key required (same as `/partner/liquidity`).\n\n**Note:** EUR, AUD, and GBP off-ramp payouts require the user's physical\naddress. Ensure the user's KYC includes `address`, `city`, and `postal_code` before initiating.\nSee `PATCH /partner/users/{user_uuid}/kyc` to update.\n","operationId":"getOffRampEstimate","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerPriceEstimateRequest"}}}},"responses":{"200":{"description":"Estimate retrieved successfully","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/APIResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PartnerPriceEstimateResponse"}}}]}}}},"400":{"description":"Bad request — invalid parameters, missing required fields, \nunsupported cryptocurrency, or invalid provider_scope.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponse"}}}},"409":{"description":"No offers available for the requested pair/amount","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponse"}}}},"500":{"description":"Internal Server Error"}}}}},"components":{"schemas":{"PartnerPriceEstimateRequest":{"type":"object","required":["crypto_currency","fiat_currency"],"oneOf":[{"required":["crypto_amount"]},{"required":["fiat_amount"]}],"properties":{"crypto_currency":{"type":"string","description":"Cryptocurrency code (e.g., USDT, USDC)"},"fiat_currency":{"type":"string","description":"Target fiat currency code (e.g., NGN, USD, EUR)"},"crypto_amount":{"type":"string","description":"Crypto amount to estimate from. Provide exactly one of crypto_amount or fiat_amount."},"fiat_amount":{"type":"string","description":"Fiat amount to estimate from. Provide exactly one of crypto_amount or fiat_amount."},"payment_method_slug":{"type":"string","description":"Optional payment method slug filter to narrow routing"},"payment_network_slug":{"type":"string","description":"Optional payment network slug filter to narrow routing"},"provider_scope":{"type":"string","description":"Optional provider scope filter; defaults to all","enum":["all","licensed_only","p2p_only"]},"country_code":{"type":"string","description":"Optional ISO country code (alpha-2) used for fee/routing context"},"partner_fee_pct":{"type":"string","pattern":"^[0-9]+(\\.[0-9]+)?$","description":"Optional per-order markup you charge your end user, applied on the same crypto base\nas the platform fee (non-compounding). Percent units: `1` means 1%. Omitting the field\nfrom this request payload — or sending `0` — means no markup; the markup cannot be\nnegative. Fiat-anchored offramp orders have no cap — the deposit grows to cover the\nmarkup. Every other case hits a formula domain bound, not a business cap: onramp\nrejects `partner_fee_pct >= 100` (the withheld or grossed-up fee would consume the\nentire buyer amount, leaving nothing delivered); crypto-anchored offramp rejects it\nwhen the fee would consume the entire pinned deposit.\n\nNote: you send this value as a JSON **string**, but it round-trips as a JSON **number**\nin `fee_breakdown.partner_fee_pct` and in the webhook `data.partner_fee_pct` field —\ndon't parse those as strings.\n\n**Rejection reference:**\n\n| Rejection | `error.code` | HTTP status |\n|---|---|---|\n| `partner_fee_pct` is non-numeric, negative, `NaN`, or `Inf` | `INVALID_REQUEST` | 400 |\n| Onramp: fee would consume the entire buyer amount (`partner_fee_pct >= 100`) | `INVALID_REQUEST` | 400 |\n| Crypto-anchored offramp: fee would consume the entire pinned deposit | `INVALID_REQUEST` | 400 |\n| Fiat-anchored offramp: the computed fee is not a finite amount | `INVALID_REQUEST` | 400 |\n| Crypto-anchored offramp: the carved fee-exclusive gross falls outside the matched offer's min/max | `NO_OFFERS_AVAILABLE` | 409 |\n","default":"0"}}},"APIResponse":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean","description":"Indicates if the request was successful"},"data":{"description":"Response data (structure varies by endpoint) or error information if success is false"}}},"PartnerPriceEstimateResponse":{"type":"object","properties":{"rate":{"type":"string","description":"Indicative crypto-to-fiat exchange rate"},"crypto_amount":{"type":"string","description":"Estimated total crypto amount — the fee-exclusive gross: the user's amount plus the platform\nfee. Partner-fee-exclusive, except for a crypto-anchored onramp estimate (request declares\n`crypto_amount`): there the amount is sized up so the delivered amount stays exact after the\npartner fee is withheld, so this field includes the markup. A crypto-anchored offramp\nestimate carves both fees out of the pinned deposit instead, so this field and\n`fee_breakdown.platform_fee` both shrink relative to a no-markup request, even though the\nmarkup itself stays out of this field. Every other case is unaffected — the markup is\nreported separately in `fee_breakdown.partner_fee`.\n\nOn-ramp: the end user would receive `crypto_amount − fee_breakdown.platform_fee −\nfee_breakdown.partner_fee` for a fiat-anchored estimate, or exactly this `crypto_amount` for a\ncrypto-anchored estimate. Off-ramp: the amount you would actually fund at initiation is\n`crypto_amount + fee_breakdown.partner_fee`.\n"},"fiat_amount":{"type":"string","description":"Estimated fiat amount"},"fee_breakdown":{"$ref":"#/components/schemas/FeeBreakdown"},"payment_method_slug":{"type":"string","description":"Matched payment method slug used for estimate"},"payment_network_slug":{"type":"string","description":"Matched payment network slug used for estimate"},"provider_scope":{"type":"string","description":"Applied provider scope"},"is_indicative":{"type":"boolean","description":"True when response is an estimate and not a reserving quote"}}},"FeeBreakdown":{"type":"object","description":"Transparent breakdown of all fee components (amounts in crypto)","properties":{"platform_fee":{"type":"string","description":"Platform fee in crypto"},"platform_fee_pct":{"type":"number","format":"double","description":"Platform fee as a percentage"},"partner_fee":{"type":"string","description":"Your markup in crypto, computed from the `partner_fee_pct` you declared on the\nquote/estimate request, on the same base as the platform fee. `0` when no markup was declared.\n"},"partner_fee_pct":{"type":"number","format":"double","description":"Your markup percentage as declared on the request (`1` means 1%); `0` when none."},"total_fee":{"type":"string","description":"Total fee in crypto (platform_fee + partner_fee)"}}},"PartnerErrorResponse":{"type":"object","required":["success","error"],"properties":{"success":{"type":"boolean"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","description":"Machine-readable error code. Possible values:\n- `QUOTE_EXPIRED` — quote TTL has passed\n- `QUOTE_NOT_FOUND` — quote ID does not exist\n- `QUOTE_ALREADY_USED` — quote was already consumed by initiate\n- `PRICE_CHANGED_REQUOTE` — price changed beyond slippage tolerance, partner should request a new quote\n- `ORDER_NOT_FOUND` — order ID does not exist or belongs to another partner\n- `NO_OFFERS_AVAILABLE` — no vendor offers for the requested pair\n- `INVALID_REQUEST` — malformed request or missing fields\n- `INVALID_PAYMENT_DETAILS` — payment details not found or invalid\n- `RAIL_ROUTE_MISMATCH` — rail or route does not match payment details\n- `UNAUTHORIZED` — authentication failed\n- `INTERNAL_ERROR` — unexpected server error\n- `INVALID_STATUS` — order is not in the correct status for the requested action\n- `TRANSACTOR_ERROR` — on-chain transaction relay failed (e.g. simulation reverted)\n- `INSUFFICIENT_BALANCE` — partner wallet lacks tokens to fund escrow\n- `OPERATION_NOT_ALLOWED` — action is not permitted for the current order state\n- `KYC_NOT_CLEARED` — the customer is not KYC-cleared for this transaction; `error.details.kyc_status` carries their current partner-facing status\n","enum":["QUOTE_EXPIRED","QUOTE_NOT_FOUND","QUOTE_ALREADY_USED","PRICE_CHANGED_REQUOTE","ORDER_NOT_FOUND","NO_OFFERS_AVAILABLE","INVALID_REQUEST","INVALID_PAYMENT_DETAILS","RAIL_ROUTE_MISMATCH","UNAUTHORIZED","INTERNAL_ERROR","INVALID_STATUS","TRANSACTOR_ERROR","INSUFFICIENT_BALANCE","OPERATION_NOT_ALLOWED","KYC_NOT_CLEARED"]},"message":{"type":"string","description":"Human-readable error message"},"details":{"type":"object","nullable":true,"additionalProperties":true,"description":"Additional context (e.g., quoted_rate, current_rate for slippage errors)"}}}}}}}}
```

## Get off-ramp quote

> Get a price quote for an off-ramp operation (crypto to fiat). Returns the exchange rate,\
> amounts, and a quote ID that can be used to initiate the order. Quotes expire after 60 seconds.\
> \
> The \`user\_uuid\` is the public UUID returned by \`POST /partner/users\` (the \`id\` field).\
> The \`payment\_details\_id\` is the ID of a previously created payment profile for the user.\
> The payment profile must belong to the same beneficiary represented by \`user\_uuid\`; third-party payouts are not supported.\
> \
> \*\*Anchor mode — provide exactly one of \`crypto\_amount\` or \`fiat\_amount\`:\*\*\
> \
> \| Field | Anchor | Guarantee |\
> \|---|---|---|\
> \| \`crypto\_amount\` | Crypto-anchored | Exact crypto cost; fiat payout varies by the rate at settlement |\
> \| \`fiat\_amount\` | Fiat-anchored | Exact fiat delivery to recipient; crypto cost may vary within a 1.20× safety bound |\
> \
> The response \`anchor\_type\` field tells you which side is guaranteed: \`"crypto"\` or \`"fiat"\`.\
> \
> \*\*Fiat-anchored notes:\*\*\
> \- Useful for exact-payout use cases (e.g. deliver exactly 500,000 NGN).\
> \- If the crypto cost we quote exceeds 1.20× the expected amount, the order moves to \`failed\` status\
> &#x20; and the deposit is automatically refunded from escrow back to the partner wallet. Create a new quote\
> &#x20; to retry.\
> \- \`slippage\_tolerance.type=amount\` in fiat-anchored mode is evaluated as crypto-side delta, not fiat-side\
> &#x20; (fiat is always exact by construction).\
> \- Integer-minor-unit currencies (NGN, UGX, TZS, KES, GHS): \`fiat\_amount\` must be a whole number.\
> \
> \*\*Fee structure:\*\* The \`fee\_breakdown\` object shows all fee components transparently:\
> \`platform\_fee\` (our fee in crypto), \`partner\_fee\` (your per-order markup in crypto — declare it\
> by passing \`partner\_fee\_pct\` on the quote/estimate request, where \`1\` means 1%), \`partner\_fee\_pct\`\
> (the percentage applied), and \`total\_fee\` (sum of both). The partner fee is computed on the same\
> crypto base as the platform fee (non-compounding) and is settled on-chain to your partner wallet\
> on successful release; cancelled or failed orders refund the full amount, markup included.\
> \
> Apply your fee on the \*\*same side\*\* the platform does, or your receipts won't reconcile with our\
> orders. \`crypto\_amount\` in responses and webhooks stays partner-fee-exclusive, with two\
> exceptions: a crypto-anchored onramp order (request declares \`crypto\_amount\`) is sized up so the\
> delivered amount stays exact after the markup is withheld, so \`crypto\_amount\` there includes the\
> markup; a crypto-anchored offramp order (request declares \`crypto\_amount\`) carves both fees out of\
> the pinned deposit, so \`crypto\_amount\` and \`platform\_fee\` both shrink relative to a no-markup\
> request even though the markup itself stays out of \`crypto\_amount\`. Every other case is unaffected\
> — the markup shows only in \`fee\_breakdown\` (and in the funding amount returned by transfer\
> authorization for offramp — \`crypto\_amount + fee\_breakdown.partner\_fee\` is what you actually fund).\
> Fiat-anchored offramp orders have no cap on the markup — the deposit grows to cover it. Every\
> other case hits a formula domain bound, not a business cap: onramp rejects \`partner\_fee\_pct >= 100\`\
> (the withheld or grossed-up fee would consume the entire buyer amount, leaving nothing delivered);\
> crypto-anchored offramp rejects it when the fee would consume the entire pinned deposit.\
> \
> \*\*Address requirement for EUR/AUD/GBP:\*\* EUR, AUD, and GBP payouts require the\
> user's physical address (street, city, postal code). If the user's\
> KYC does not include address data, the quote response will include a \`beneficiary\_requirements\`\
> field indicating which fields are needed. You can update the user's KYC with\
> \`PATCH /partner/users/{user\_uuid}/kyc\` to provide the address before or after initiating the order.\
> If you initiate without address, the order will be created with status \`pending\_address\` until\
> the address is provided.\
> \
> \*\*KYC requirement:\*\* The customer must be KYC-cleared before you can quote or transact for them.\
> If the customer is not cleared, this endpoint returns \`422 KYC\_NOT\_CLEARED\` and\
> \`error.details.kyc\_status\` carries their current partner-facing KYC status. The same check is\
> re-applied as the order moves toward payout. A customer still in compliance review is reported as\
> \`IN\_PROGRESS\` — treat \`IN\_PROGRESS\` as "not cleared yet" and keep polling\
> \`GET /api/v1/partner/users/{user\_uuid}/verification-status\`.<br>

```json
{"openapi":"3.0.3","info":{"title":"Unigox API Gateway","version":"1.0.0"},"tags":[{"name":"Off-Ramp","description":"Off-ramp operations (crypto to fiat) — sell crypto and receive fiat"}],"servers":[{"url":"https://api-staging.unigox.com","description":"Sandbox server"},{"url":"https://api.unigox.com","description":"Production server"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Partner API key for authentication. Required for all partner account endpoints."}},"schemas":{"PartnerQuoteRequest":{"type":"object","required":["user_uuid","payment_details_id","crypto_currency","fiat_currency"],"oneOf":[{"required":["crypto_amount"]},{"required":["fiat_amount"]}],"properties":{"user_uuid":{"type":"string","format":"uuid","description":"Public UUID of the end-user (returned by `POST /partner/users` as `id`)"},"payment_details_id":{"type":"integer","format":"int64","description":"ID of a previously created payment profile for the user"},"crypto_currency":{"type":"string","description":"Cryptocurrency code (e.g., USDT, USDC)"},"crypto_amount":{"type":"string","description":"Amount of crypto to sell (standard units, e.g., \"100.00\" for 100 USDT).\nProvide exactly one of `crypto_amount` or `fiat_amount`.\nWhen provided, the order is **crypto-anchored** — exact crypto cost is guaranteed, fiat payout varies by rate at settlement.\n"},"fiat_amount":{"type":"string","description":"Exact fiat amount the recipient will receive (e.g., \"500000\" for 500,000 NGN).\nProvide exactly one of `crypto_amount` or `fiat_amount`.\nWhen provided, the order is **fiat-anchored** — exact fiat delivery is guaranteed, crypto cost may vary within a 1.20× safety bound.\nFor integer-minor-unit currencies (NGN, UGX, TZS, KES, GHS), must be a whole number.\n"},"fiat_currency":{"type":"string","description":"Target fiat currency code (e.g., NGN, USD, EUR)"},"rail":{"type":"string","description":"Optional rail hint; must be compatible with payment details if provided"},"route":{"type":"string","description":"Optional route hint; must be compatible with payment details if provided"},"provider_scope":{"type":"string","description":"Optional provider scope filter; defaults to all","enum":["all","licensed_only","p2p_only"]},"slippage_tolerance":{"allOf":[{"$ref":"#/components/schemas/SlippageTolerance"}],"description":"Optional. Maximum tolerated price movement between `/quote` and `/initiate`,\nand between `/initiate` and vendor acceptance. When omitted, the default is\n`percent` / `0` — any rate movement returns `PRICE_CHANGED_REQUOTE` and the\npartner must request a fresh quote. Set this to opt in to silent execution\nfor small market movements.\n"},"has_fiat_settlement_notification":{"type":"boolean","description":"When `true`, only match liquidity providers that send fiat settlement\nnotifications. Orders from these providers complete automatically when\nfiat is delivered. When omitted or `false`, all providers are eligible.\n"},"partner_fee_pct":{"type":"string","pattern":"^[0-9]+(\\.[0-9]+)?$","description":"Optional per-order markup you charge your end user, applied on the same crypto base\nas the platform fee (non-compounding). Percent units: `1` means 1%. Omitting the field\nfrom this request payload — or sending `0` — means no markup; the markup cannot be\nnegative. Fiat-anchored offramp orders have no cap — the deposit grows to cover the\nmarkup. Every other case hits a formula domain bound, not a business cap: onramp\nrejects `partner_fee_pct >= 100` (the withheld or grossed-up fee would consume the\nentire buyer amount, leaving nothing delivered); crypto-anchored offramp rejects it\nwhen the fee would consume the entire pinned deposit.\n\nNote: you send this value as a JSON **string**, but it round-trips as a JSON **number**\nin `fee_breakdown.partner_fee_pct` and in the webhook `data.partner_fee_pct` field —\ndon't parse those as strings.\n\n**Rejection reference:**\n\n| Rejection | `error.code` | HTTP status |\n|---|---|---|\n| `partner_fee_pct` is non-numeric, negative, `NaN`, or `Inf` | `INVALID_REQUEST` | 400 |\n| Onramp: fee would consume the entire buyer amount (`partner_fee_pct >= 100`) | `INVALID_REQUEST` | 400 |\n| Crypto-anchored offramp: fee would consume the entire pinned deposit | `INVALID_REQUEST` | 400 |\n| Fiat-anchored offramp: the computed fee is not a finite amount | `INVALID_REQUEST` | 400 |\n| Crypto-anchored offramp: the carved fee-exclusive gross falls outside the matched offer's min/max | `NO_OFFERS_AVAILABLE` | 409 |\n","default":"0"}}},"SlippageTolerance":{"type":"object","properties":{"type":{"type":"string","enum":["percent","amount","fiat"],"description":"Tolerance type:\n- `percent` — percentage of the quoted rate (e.g., 1.5 = 1.5%)\n- `amount` — absolute fiat amount (e.g., 50.00 = 50 NGN)\n- `fiat` — alias of `amount`\n"},"value":{"type":"number","format":"double","description":"Tolerance value (percentage or fiat amount depending on type)"}}},"APIResponse":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean","description":"Indicates if the request was successful"},"data":{"description":"Response data (structure varies by endpoint) or error information if success is false"}}},"PartnerQuoteResponse":{"type":"object","properties":{"quote_id":{"type":"string","format":"uuid","description":"Unique quote identifier — use this to initiate the order"},"anchor_type":{"type":"string","enum":["crypto","fiat"],"description":"Indicates which amount is guaranteed at settlement.\n- `crypto` — `crypto_amount` is exact; `fiat_amount` may vary by the rate at settlement.\n- `fiat` — `fiat_amount` is exact; `crypto_amount` may vary within a 1.20× safety bound.\n"},"rate":{"type":"string","description":"Crypto-to-fiat exchange rate at quote time"},"crypto_amount":{"type":"string","description":"Total crypto amount — the fee-exclusive gross: the sell amount plus the platform fee.\nPartner-fee-exclusive: your markup is reported separately in `fee_breakdown.partner_fee` and\nis not added here. This is not the total you fund: the amount you actually fund at initiation\nis `crypto_amount + fee_breakdown.partner_fee` (see the transfer authorization endpoint).\n"},"fiat_amount":{"type":"string","description":"Fiat amount the recipient receives"},"fee_breakdown":{"$ref":"#/components/schemas/FeeBreakdown"},"payment_details_id":{"type":"integer","format":"int64","description":"Payment details ID used for this quote"},"payment_method_id":{"type":"integer","format":"int64","description":"Matched payment method ID"},"payment_network_id":{"type":"integer","format":"int64","description":"Matched payment network ID"},"expires_at":{"type":"string","format":"date-time","description":"Quote expiration timestamp (30 seconds from creation)"},"has_fiat_settlement_notification":{"type":"boolean","description":"Whether the matched liquidity provider sends fiat settlement notifications.\nWhen `true`, the order will complete automatically when fiat is delivered\nto the recipient. When `false`, you must call `confirm-fiat-received`.\nYou can call `confirm-fiat-received` at any time regardless of this value.\n"}}},"FeeBreakdown":{"type":"object","description":"Transparent breakdown of all fee components (amounts in crypto)","properties":{"platform_fee":{"type":"string","description":"Platform fee in crypto"},"platform_fee_pct":{"type":"number","format":"double","description":"Platform fee as a percentage"},"partner_fee":{"type":"string","description":"Your markup in crypto, computed from the `partner_fee_pct` you declared on the\nquote/estimate request, on the same base as the platform fee. `0` when no markup was declared.\n"},"partner_fee_pct":{"type":"number","format":"double","description":"Your markup percentage as declared on the request (`1` means 1%); `0` when none."},"total_fee":{"type":"string","description":"Total fee in crypto (platform_fee + partner_fee)"}}},"PartnerErrorResponse":{"type":"object","required":["success","error"],"properties":{"success":{"type":"boolean"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","description":"Machine-readable error code. Possible values:\n- `QUOTE_EXPIRED` — quote TTL has passed\n- `QUOTE_NOT_FOUND` — quote ID does not exist\n- `QUOTE_ALREADY_USED` — quote was already consumed by initiate\n- `PRICE_CHANGED_REQUOTE` — price changed beyond slippage tolerance, partner should request a new quote\n- `ORDER_NOT_FOUND` — order ID does not exist or belongs to another partner\n- `NO_OFFERS_AVAILABLE` — no vendor offers for the requested pair\n- `INVALID_REQUEST` — malformed request or missing fields\n- `INVALID_PAYMENT_DETAILS` — payment details not found or invalid\n- `RAIL_ROUTE_MISMATCH` — rail or route does not match payment details\n- `UNAUTHORIZED` — authentication failed\n- `INTERNAL_ERROR` — unexpected server error\n- `INVALID_STATUS` — order is not in the correct status for the requested action\n- `TRANSACTOR_ERROR` — on-chain transaction relay failed (e.g. simulation reverted)\n- `INSUFFICIENT_BALANCE` — partner wallet lacks tokens to fund escrow\n- `OPERATION_NOT_ALLOWED` — action is not permitted for the current order state\n- `KYC_NOT_CLEARED` — the customer is not KYC-cleared for this transaction; `error.details.kyc_status` carries their current partner-facing status\n","enum":["QUOTE_EXPIRED","QUOTE_NOT_FOUND","QUOTE_ALREADY_USED","PRICE_CHANGED_REQUOTE","ORDER_NOT_FOUND","NO_OFFERS_AVAILABLE","INVALID_REQUEST","INVALID_PAYMENT_DETAILS","RAIL_ROUTE_MISMATCH","UNAUTHORIZED","INTERNAL_ERROR","INVALID_STATUS","TRANSACTOR_ERROR","INSUFFICIENT_BALANCE","OPERATION_NOT_ALLOWED","KYC_NOT_CLEARED"]},"message":{"type":"string","description":"Human-readable error message"},"details":{"type":"object","nullable":true,"additionalProperties":true,"description":"Additional context (e.g., quoted_rate, current_rate for slippage errors)"}}}}}}},"paths":{"/api/v1/partner/offramp/quote":{"post":{"tags":["Off-Ramp"],"summary":"Get off-ramp quote","description":"Get a price quote for an off-ramp operation (crypto to fiat). Returns the exchange rate,\namounts, and a quote ID that can be used to initiate the order. Quotes expire after 60 seconds.\n\nThe `user_uuid` is the public UUID returned by `POST /partner/users` (the `id` field).\nThe `payment_details_id` is the ID of a previously created payment profile for the user.\nThe payment profile must belong to the same beneficiary represented by `user_uuid`; third-party payouts are not supported.\n\n**Anchor mode — provide exactly one of `crypto_amount` or `fiat_amount`:**\n\n| Field | Anchor | Guarantee |\n|---|---|---|\n| `crypto_amount` | Crypto-anchored | Exact crypto cost; fiat payout varies by the rate at settlement |\n| `fiat_amount` | Fiat-anchored | Exact fiat delivery to recipient; crypto cost may vary within a 1.20× safety bound |\n\nThe response `anchor_type` field tells you which side is guaranteed: `\"crypto\"` or `\"fiat\"`.\n\n**Fiat-anchored notes:**\n- Useful for exact-payout use cases (e.g. deliver exactly 500,000 NGN).\n- If the crypto cost we quote exceeds 1.20× the expected amount, the order moves to `failed` status\n  and the deposit is automatically refunded from escrow back to the partner wallet. Create a new quote\n  to retry.\n- `slippage_tolerance.type=amount` in fiat-anchored mode is evaluated as crypto-side delta, not fiat-side\n  (fiat is always exact by construction).\n- Integer-minor-unit currencies (NGN, UGX, TZS, KES, GHS): `fiat_amount` must be a whole number.\n\n**Fee structure:** The `fee_breakdown` object shows all fee components transparently:\n`platform_fee` (our fee in crypto), `partner_fee` (your per-order markup in crypto — declare it\nby passing `partner_fee_pct` on the quote/estimate request, where `1` means 1%), `partner_fee_pct`\n(the percentage applied), and `total_fee` (sum of both). The partner fee is computed on the same\ncrypto base as the platform fee (non-compounding) and is settled on-chain to your partner wallet\non successful release; cancelled or failed orders refund the full amount, markup included.\n\nApply your fee on the **same side** the platform does, or your receipts won't reconcile with our\norders. `crypto_amount` in responses and webhooks stays partner-fee-exclusive, with two\nexceptions: a crypto-anchored onramp order (request declares `crypto_amount`) is sized up so the\ndelivered amount stays exact after the markup is withheld, so `crypto_amount` there includes the\nmarkup; a crypto-anchored offramp order (request declares `crypto_amount`) carves both fees out of\nthe pinned deposit, so `crypto_amount` and `platform_fee` both shrink relative to a no-markup\nrequest even though the markup itself stays out of `crypto_amount`. Every other case is unaffected\n— the markup shows only in `fee_breakdown` (and in the funding amount returned by transfer\nauthorization for offramp — `crypto_amount + fee_breakdown.partner_fee` is what you actually fund).\nFiat-anchored offramp orders have no cap on the markup — the deposit grows to cover it. Every\nother case hits a formula domain bound, not a business cap: onramp rejects `partner_fee_pct >= 100`\n(the withheld or grossed-up fee would consume the entire buyer amount, leaving nothing delivered);\ncrypto-anchored offramp rejects it when the fee would consume the entire pinned deposit.\n\n**Address requirement for EUR/AUD/GBP:** EUR, AUD, and GBP payouts require the\nuser's physical address (street, city, postal code). If the user's\nKYC does not include address data, the quote response will include a `beneficiary_requirements`\nfield indicating which fields are needed. You can update the user's KYC with\n`PATCH /partner/users/{user_uuid}/kyc` to provide the address before or after initiating the order.\nIf you initiate without address, the order will be created with status `pending_address` until\nthe address is provided.\n\n**KYC requirement:** The customer must be KYC-cleared before you can quote or transact for them.\nIf the customer is not cleared, this endpoint returns `422 KYC_NOT_CLEARED` and\n`error.details.kyc_status` carries their current partner-facing KYC status. The same check is\nre-applied as the order moves toward payout. A customer still in compliance review is reported as\n`IN_PROGRESS` — treat `IN_PROGRESS` as \"not cleared yet\" and keep polling\n`GET /api/v1/partner/users/{user_uuid}/verification-status`.\n","operationId":"getOffRampQuote","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerQuoteRequest"}}}},"responses":{"200":{"description":"Quote retrieved successfully","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/APIResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PartnerQuoteResponse"}}}]}}}},"400":{"description":"Bad request — invalid parameters or missing required fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponse"}}}},"401":{"description":"Unauthorized — invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponse"}}}},"404":{"description":"User or payment details not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponse"}}}},"409":{"description":"No offers available for the requested pair","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponse"}}}},"422":{"description":"The customer is not KYC-cleared. `error.details.kyc_status` carries the customer's current partner-facing KYC status — one of `IN_PROGRESS`, `UNDER_REVIEW`, `PENDING_DOCUMENTS`, `VERIFICATION_REJECTED`, or `NOT_INITIATED`. `UNDER_REVIEW` means a manual compliance review (up to 24h); keep the customer waiting and re-check. The message stays generic.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponse"}}}}}}}}}
```

## Initiate off-ramp order

> Initiate an off-ramp order using a previously obtained quote. The quote must still be active\
> (not expired or already used). The system refreshes the price and checks slippage tolerance\
> before creating the order.\
> \
> If the price has moved beyond the slippage tolerance, a \`PRICE\_CHANGED\_REQUOTE\` error is returned\
> with details about the old and new rates, so the partner can request a new quote.\
> \
> On success, returns the created order with status \`awaiting\_liquidity\_provider\`.\
> \
> \*\*Address requirement for EUR/AUD/GBP:\*\* For EUR/AUD/GBP orders, if the\
> user's KYC does not include a physical address, the order will be created with status\
> \`pending\_address\`. The response will include \`required\_fields: \["address", "city", "postal\_code"]\`\
> and \`next\_action: "provide\_address"\`. Call \`PATCH /partner/users/{user\_uuid}/kyc\` to add the\
> address, and the order will automatically advance.\
> \
> \*\*Recommended flow for EUR/AUD/GBP:\*\*\
> 1\. Submit KYC with address/city/postal\_code (\`POST /kyc-submissions\`)\
> 2\. Create payment details (\`POST /payment-details\`) — address auto-populated from KYC\
> 3\. Get quote (\`POST /offramp/quote\`)\
> 4\. Initiate (\`POST /offramp/initiate\`) — proceeds without delays\
> \
> \*\*KYC requirement:\*\* The customer must be KYC-cleared, exactly as described on\
> \`POST /offramp/quote\`. If they are not, this endpoint returns \`422 KYC\_NOT\_CLEARED\` with the\
> customer's partner-facing status in \`error.details.kyc\_status\`. The check is re-applied as the\
> order moves toward payout.<br>

```json
{"openapi":"3.0.3","info":{"title":"Unigox API Gateway","version":"1.0.0"},"tags":[{"name":"Off-Ramp","description":"Off-ramp operations (crypto to fiat) — sell crypto and receive fiat"}],"servers":[{"url":"https://api-staging.unigox.com","description":"Sandbox server"},{"url":"https://api.unigox.com","description":"Production server"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Partner API key for authentication. Required for all partner account endpoints."}},"schemas":{"PartnerInitiateRequest":{"type":"object","required":["quote_id"],"properties":{"quote_id":{"type":"string","format":"uuid","description":"Quote ID from the quote endpoint. Payment details are taken from the quote."},"reference_id":{"type":"string","description":"Optional partner-supplied external reference ID (e.g. your internal order ID). Stored on the order and echoed back in responses."}}},"APIResponse":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean","description":"Indicates if the request was successful"},"data":{"description":"Response data (structure varies by endpoint) or error information if success is false"}}},"PartnerInitiateResponse":{"type":"object","properties":{"order_id":{"type":"string","format":"uuid","description":"Unique order identifier for tracking"},"status":{"$ref":"#/components/schemas/PartnerOrderStatus"},"quote_id":{"type":"string","format":"uuid","description":"Quote ID that was consumed"},"reference_id":{"type":"string","nullable":true,"description":"Partner-supplied external reference ID (echoed from initiate request)"},"order_type":{"type":"string","enum":["offramp","onramp"],"description":"Direction of the order"},"amounts":{"$ref":"#/components/schemas/PartnerOrderAmounts"},"fee_breakdown":{"$ref":"#/components/schemas/FeeBreakdown"},"timeline":{"type":"array","items":{"$ref":"#/components/schemas/TimelineEntry"}},"created_at":{"type":"string","format":"date-time"}}},"PartnerOrderStatus":{"type":"string","description":"Order status.\n\n**Off-ramp statuses (crypto → fiat):**\n- `created` — order created, quote locked\n- `awaiting_liquidity_provider` — order initiated, waiting for vendor acceptance\n- `awaiting_crypto_transfer_authorization` — partner must authorize crypto transfer via EIP-712 signature\n- `crypto_transfer_authorization_pending` — authorization submitted, awaiting on-chain confirmation\n- `crypto_received` — crypto locked in escrow\n- `fiat_payment_started` — buyer submitted fiat payment proof\n- `awaiting_fiat_received_confirmation` — waiting for partner to confirm fiat receipt\n- `fiat_received_confirmed` — fiat confirmed, crypto release pending\n- `crypto_release_pending` — escrow release in progress\n- `completed` — order fully completed\n- `cancelled` — order cancelled\n- `failed` — terminal error\n- `price_changed_requote_needed` — slippage exceeded at initiation (not a trade status)\n\n**On-ramp statuses (fiat → crypto):**\n- `awaiting_liquidity_provider` — order initiated, waiting for vendor acceptance\n- `awaiting_vendor_escrow_funding` — vendor accepted, funding escrow with crypto\n- `awaiting_fiat_transfer` — crypto locked in escrow, partner must confirm payment sent\n- `fiat_transfer_pending` — fiat payment confirmed, awaiting vendor confirmation of receipt\n- `completed` — vendor confirmed fiat received, crypto released to end-user wallet\n- `cancelled` — order cancelled\n- `failed` — escrow error\n\n**On-ramp send-out statuses (optional post-trade cross-chain transfer):**\n\n> After an on-ramp order reaches `completed`, the partner may optionally initiate a\n> **send-out** — a cross-chain transfer of the received crypto to a destination network\n> (e.g. XAI → Arbitrum). This is an additional flow and does not affect the core trade lifecycle.\n> The `completed` status reflects that the trade itself is done; send-out statuses track\n> the subsequent cross-chain transfer only.\n\n- `send_out_pending` — send-out initiated, cross-chain transfer in progress\n- `send_out_completed` — cross-chain transfer successfully delivered to destination\n- `send_out_failed` — cross-chain transfer failed; the on-ramp trade itself remains completed\n","enum":["created","awaiting_liquidity_provider","awaiting_crypto_transfer_authorization","crypto_transfer_authorization_pending","crypto_received","fiat_payment_started","awaiting_fiat_received_confirmation","fiat_received_confirmed","crypto_release_pending","awaiting_vendor_escrow_funding","awaiting_fiat_transfer","fiat_transfer_pending","completed","cancelled","failed","price_changed_requote_needed","send_out_pending","send_out_completed","send_out_failed"]},"PartnerOrderAmounts":{"type":"object","properties":{"crypto_amount":{"type":"string","description":"Total crypto amount (standard units) — the fee-exclusive gross: the buy/sell amount plus the\nplatform fee. Partner-fee-exclusive, except for a crypto-anchored onramp order (originally\nquoted with `crypto_amount`): there the amount was sized up so the delivered amount stays\nexact after the partner fee is withheld, so this field includes the markup. A crypto-anchored\nofframp order carves both fees out of the pinned deposit instead, so this field and\n`fee_breakdown.platform_fee` both shrink relative to a no-markup request, even though the\nmarkup itself stays out of this field. Every other case is unaffected — the markup is\nreported separately in `fee_breakdown.partner_fee`.\n\nOn-ramp — what the end user receives: `crypto_amount − fee_breakdown.platform_fee −\nfee_breakdown.partner_fee` for a fiat-anchored order, or exactly this `crypto_amount` for a\ncrypto-anchored order (both fees are withheld at bridge send-out without changing this\nfield). Off-ramp — what the partner funds at transfer authorization:\n`crypto_amount + fee_breakdown.partner_fee`.\n"},"fiat_amount":{"type":"string","description":"Fiat amount the user receives"},"crypto_currency":{"type":"string","description":"Cryptocurrency code"},"fiat_currency":{"type":"string","description":"Fiat currency code"},"rate":{"type":"string","description":"Crypto-to-fiat exchange rate"}}},"FeeBreakdown":{"type":"object","description":"Transparent breakdown of all fee components (amounts in crypto)","properties":{"platform_fee":{"type":"string","description":"Platform fee in crypto"},"platform_fee_pct":{"type":"number","format":"double","description":"Platform fee as a percentage"},"partner_fee":{"type":"string","description":"Your markup in crypto, computed from the `partner_fee_pct` you declared on the\nquote/estimate request, on the same base as the platform fee. `0` when no markup was declared.\n"},"partner_fee_pct":{"type":"number","format":"double","description":"Your markup percentage as declared on the request (`1` means 1%); `0` when none."},"total_fee":{"type":"string","description":"Total fee in crypto (platform_fee + partner_fee)"}}},"TimelineEntry":{"type":"object","properties":{"status":{"$ref":"#/components/schemas/PartnerOrderStatus"},"timestamp":{"type":"string","format":"date-time","description":"When this status was reached"},"description":{"type":"string","description":"Human-readable description of the status change"}}},"PartnerErrorResponse":{"type":"object","required":["success","error"],"properties":{"success":{"type":"boolean"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","description":"Machine-readable error code. Possible values:\n- `QUOTE_EXPIRED` — quote TTL has passed\n- `QUOTE_NOT_FOUND` — quote ID does not exist\n- `QUOTE_ALREADY_USED` — quote was already consumed by initiate\n- `PRICE_CHANGED_REQUOTE` — price changed beyond slippage tolerance, partner should request a new quote\n- `ORDER_NOT_FOUND` — order ID does not exist or belongs to another partner\n- `NO_OFFERS_AVAILABLE` — no vendor offers for the requested pair\n- `INVALID_REQUEST` — malformed request or missing fields\n- `INVALID_PAYMENT_DETAILS` — payment details not found or invalid\n- `RAIL_ROUTE_MISMATCH` — rail or route does not match payment details\n- `UNAUTHORIZED` — authentication failed\n- `INTERNAL_ERROR` — unexpected server error\n- `INVALID_STATUS` — order is not in the correct status for the requested action\n- `TRANSACTOR_ERROR` — on-chain transaction relay failed (e.g. simulation reverted)\n- `INSUFFICIENT_BALANCE` — partner wallet lacks tokens to fund escrow\n- `OPERATION_NOT_ALLOWED` — action is not permitted for the current order state\n- `KYC_NOT_CLEARED` — the customer is not KYC-cleared for this transaction; `error.details.kyc_status` carries their current partner-facing status\n","enum":["QUOTE_EXPIRED","QUOTE_NOT_FOUND","QUOTE_ALREADY_USED","PRICE_CHANGED_REQUOTE","ORDER_NOT_FOUND","NO_OFFERS_AVAILABLE","INVALID_REQUEST","INVALID_PAYMENT_DETAILS","RAIL_ROUTE_MISMATCH","UNAUTHORIZED","INTERNAL_ERROR","INVALID_STATUS","TRANSACTOR_ERROR","INSUFFICIENT_BALANCE","OPERATION_NOT_ALLOWED","KYC_NOT_CLEARED"]},"message":{"type":"string","description":"Human-readable error message"},"details":{"type":"object","nullable":true,"additionalProperties":true,"description":"Additional context (e.g., quoted_rate, current_rate for slippage errors)"}}}}}}},"paths":{"/api/v1/partner/offramp/initiate":{"post":{"tags":["Off-Ramp"],"summary":"Initiate off-ramp order","description":"Initiate an off-ramp order using a previously obtained quote. The quote must still be active\n(not expired or already used). The system refreshes the price and checks slippage tolerance\nbefore creating the order.\n\nIf the price has moved beyond the slippage tolerance, a `PRICE_CHANGED_REQUOTE` error is returned\nwith details about the old and new rates, so the partner can request a new quote.\n\nOn success, returns the created order with status `awaiting_liquidity_provider`.\n\n**Address requirement for EUR/AUD/GBP:** For EUR/AUD/GBP orders, if the\nuser's KYC does not include a physical address, the order will be created with status\n`pending_address`. The response will include `required_fields: [\"address\", \"city\", \"postal_code\"]`\nand `next_action: \"provide_address\"`. Call `PATCH /partner/users/{user_uuid}/kyc` to add the\naddress, and the order will automatically advance.\n\n**Recommended flow for EUR/AUD/GBP:**\n1. Submit KYC with address/city/postal_code (`POST /kyc-submissions`)\n2. Create payment details (`POST /payment-details`) — address auto-populated from KYC\n3. Get quote (`POST /offramp/quote`)\n4. Initiate (`POST /offramp/initiate`) — proceeds without delays\n\n**KYC requirement:** The customer must be KYC-cleared, exactly as described on\n`POST /offramp/quote`. If they are not, this endpoint returns `422 KYC_NOT_CLEARED` with the\ncustomer's partner-facing status in `error.details.kyc_status`. The check is re-applied as the\norder moves toward payout.\n","operationId":"initiateOffRamp","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerInitiateRequest"}}}},"responses":{"201":{"description":"Off-ramp order created successfully","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/APIResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PartnerInitiateResponse"}}}]}}}},"400":{"description":"Bad request — missing required fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponse"}}}},"401":{"description":"Unauthorized — invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponse"}}}},"404":{"description":"Quote not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponse"}}}},"409":{"description":"Quote expired, already used, or slippage exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponse"}}}},"422":{"description":"The customer is not KYC-cleared. `error.details.kyc_status` carries the customer's current partner-facing KYC status — one of `IN_PROGRESS`, `UNDER_REVIEW`, `PENDING_DOCUMENTS`, `VERIFICATION_REJECTED`, or `NOT_INITIATED`. `UNDER_REVIEW` means a manual compliance review (up to 24h); keep the customer waiting and re-check. The message stays generic.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerErrorResponse"}}}}}}}}}
```
