Trading

Trading on the Partner API is a two-step flow — calculate a quote to lock a price, then execute it to place the order. This page explains asset pairs, the quote token model, buy/sell sides, and how trading interacts with client balances.

Trading lets your clients buy and sell crypto at a price locked in real time. The Partner API uses a two-step model — first calculate a quote, then execute it — so your clients always trade at a guaranteed price with no slippage.


Overview

Trading on behalf of a client is always two steps:

  1. Calculate a quote — Call POST /v1/partner/quotes with the client, asset pair, side, and amount. CoinMENA returns a locked market price, fee breakdown, and a short-lived JWT token.
  2. Execute the quote — Call POST /v1/partner/quotes/execute with the token from step 1. The order is placed at the locked price and returned as an order object.

The token is the bridge between the two steps. It is single-use and time-limited — if it expires or is reused, execution fails with error 4008.

📘

Why two steps?

The two-step model lets you show the client the exact price, fees, and totals before they commit. They see the final numbers, confirm, and only then do you execute. This avoids slippage and gives the client full visibility into what they are about to pay or receive.

The Core Rule

Quote → Execute — the rule you must not break

A quote must be executed within its lifetime using the token returned by calculation. If the quote expires, execution will fail with 4008 and a new quote must be requested. Tokens are single-use — once executed, they cannot be reused.

Price Lock & Slippage

Quotes lock the price for the duration specified in the lifetime field (in seconds). Inside that window, the price returned in the quote is exactly what the client pays or receives — there is no slippage.

After lifetime expires, the market price may have moved. Execution with the expired token is rejected, and you must re-quote to get the current price.


End-to-End Trading Flow

 1. List pairs              2. Calculate quote             3. Execute quote
    GET asset-pairs            POST quotes                    POST quotes/execute
    → price, limits            → locked price + token         → order created
                               (valid for `lifetime` sec)

A typical trade end-to-end:

  1. Discover pairs — Call GET /v1/partner/asset-pairs to list tradeable pairs and their current buy/sell prices.
  2. Calculate a quote — Submit the client, pair, side, and amount. Receive a locked price, fees, and a token valid for lifetime seconds.
  3. Show the client the final numbers — Price, fees, total. Let the client confirm.
  4. Execute — Send the token back before it expires. Optionally include a partner_order_ref for reconciliation.
  5. Receive the order — The API returns the created order object. Persist the order ID (and your partner_order_ref if you used one).

The resulting order is covered in the Orders guide.

📘

Execution is synchronous

A successful execute response means the order has been placed and returned in its final state. Check the status field on the returned order to confirm the outcome — typically completed on success, or failed / rejected if something went wrong during placement. You do not need to poll for completion.


Asset Pairs

An asset pair is a tradeable combination of a base asset and a quote asset, written in BASE-QUOTE format — e.g. BTC-USD.

Reading the Pair Format

asset_pair: BTC-USD
            │   │
            │   └── quote asset (USD) — the pricing/fiat asset
            └────── base asset (BTC) — the asset being bought or sold
TermMeaning
Base assetThe asset being bought or sold. For BTC-USD, the base is BTC
Quote assetThe asset used to price the trade. For BTC-USD, the quote is USD
buy pricePrice per unit of base asset when the client buys
sell pricePrice per unit of base asset when the client sells
tradableBoolean — whether the pair is currently available for trading

Asset Pair Object

Fetched via GET /v1/partner/asset-pairs or GET /v1/partner/asset-pairs/{asset_pair_id}:

FieldTypeDescription
idstringPair identifier in BASE-QUOTE format (e.g. BTC-USD)
typestringPair type (e.g. crypto)
base_idstringBase asset identifier
quote_idstringQuote asset identifier
decimal_placesintegerDecimal precision for quote asset amounts
inv_decimal_placesintegerDecimal precision for base asset amounts
min_volumestringMinimum tradeable volume in base asset units
max_volumestring, nullableMaximum tradeable volume in base asset units. null if no upper limit
trading_feestringTrading fee percentage applied to this pair
tradablebooleanWhether this pair is currently available for trading
buystringCurrent buy (ask) price in quote asset
sellstringCurrent sell (bid) price in quote asset
daily_change_pctstring24-hour price change percentage

Example

{
  "id": "BTC-USD",
  "type": "crypto",
  "base_id": "BTC",
  "quote_id": "USD",
  "decimal_places": 2,
  "inv_decimal_places": 8,
  "min_volume": "0.0001",
  "max_volume": "100",
  "trading_fee": "0.5",
  "tradable": true,
  "buy": "67550.75",
  "sell": "67450.25",
  "daily_change_pct": "2.35"
}
⚠️

Check tradable before calculating a quote

If tradable is false, quote calculation returns error 4000. Always check the flag when surfacing pairs to your users so they do not attempt trades that will be rejected.

Precision Per Pair

Every pair carries its own decimal precision:

  • inv_decimal_places — precision for base asset amounts
  • decimal_places — precision for quote asset amounts

For the BTC-USD example above:

  • base_amount is accepted up to 8 decimal places (e.g. 0.00100000)
  • quote_amount is accepted up to 2 decimal places (e.g. 67.50)

These values vary per pair — always read them from the pair object rather than hardcoding.

Trading Limits

Every pair has min_volume and max_volume (nullable) in base asset units. Quotes outside these bounds are rejected:

Error CodeTrigger
4004Requested amount is below the pair's min_volume
4003Requested amount is above the pair's max_volume

For 4003 and 4004, the error response includes both the applicable limit and a quote object in msg_detail — useful for showing the client the current rate and why their trade was rejected. See Error Codes.


Buy vs Sell

The side field controls the direction of the trade. Both sides use the same endpoints and schemas.

SidePlain-English MeaningClient SpendsClient Receives
buyClient is buying the base asset using the quote asset (e.g. buy BTC with USD)Quote asset (USD)Base asset (BTC)
sellClient is selling the base asset for the quote asset (e.g. sell BTC for USD)Base asset (BTC)Quote asset (USD)

If the client does not have enough balance in the asset they need to spend, quote calculation returns error 3005Insufficient funds. The error response includes a quote object in msg_detail with the calculated price, so you can still show the client the market rate.

📘

Funding before trading

For buy orders, the client needs a quote asset (fiat) balance — fund them first via POST /v1/partner/deposits. For sell orders, the client needs a base asset (crypto) balance. See the Funding guide.


Calculating a Quote

A quote locks a price for the trade the client is about to make. Submit to POST /v1/partner/quotes:

FieldRequiredDescription
partner_client_idYesThe client on whose behalf the quote is calculated
asset_pairYesThe pair identifier (e.g. BTC-USD)
sideYesbuy to purchase the base asset, sell to sell the base asset
base_amountConditionalAmount in the base asset (e.g. "0.001" for BTC). Provide either this or quote_amount
quote_amountConditionalAmount in the quote asset (e.g. "100.00" for USD). Provide either this or base_amount
⚠️

base_amount or quote_amount — pick one

Provide exactly one of the two. Sending both, or neither, returns a validation error. Use base_amount when the client wants to buy/sell a specific quantity of the crypto; use quote_amount when the client wants to spend/receive a specific amount of fiat.

Request Example

{
  "partner_client_id": "user_12345",
  "asset_pair": "BTC-USD",
  "side": "buy",
  "base_amount": "0.001"
}

Response — The Quote Object

FieldTypeDescription
market_pricestring (decimal)The locked price for this quote, with partner spread applied
base_amountstring (decimal)Trade amount in base asset
quote_amountstring (decimal)Trade amount in quote asset
fee_amountstring (decimal)Total fee charged for this trade
vat_amountstring (decimal)VAT amount applied to the fee
vat_percentagestring (decimal)VAT rate percentage applied
lifetimeintegerNumber of seconds this quote remains valid
tokenstringJWT token required to execute this quote. Pass to the execute endpoint

Example

{
  "result": {
    "market_price": "67500.50",
    "base_amount": "0.00100000",
    "quote_amount": "67.50",
    "fee_amount": "0.34",
    "vat_amount": "0.00",
    "vat_percentage": null,
    "lifetime": 15,
    "token": "eyJhbGciOiJIUzI1NiJ9..."
  }
}

The Quote Token

PropertyBehavior
FormatJWT string
LifetimeThe lifetime field (in seconds) tells you how long the token is valid
Single-useOnce a token is executed successfully, it cannot be reused
Client-boundThe token is bound to the partner_client_id that calculated the quote — you cannot execute it for a different client
On expiryUsing an expired token returns error 4008Trading token is invalid
On reuseUsing an already-executed token returns error 4008
📘

Design your UI around the token lifetime

Quote lifetimes are short (typically a few seconds). If you show the client a confirmation screen, either:

  • Auto-refresh the quote in the background as the client reads, or
  • Submit the execute call immediately upon confirmation and handle any 4008 by re-quoting and retrying.

Executing a Quote

Once you have a valid token, execute it to place the order via POST /v1/partner/quotes/execute.

Request

FieldRequiredDescription
partner_client_idYesMust match the client used when calculating the quote
tokenYesThe JWT token from the calculate response
partner_order_refOptionalYour own internal reference for the resulting order. Must be unique per partner. Use it to retrieve the order later via GET /v1/partner/orders/ref/{partner_order_ref}

Request Example

{
  "partner_client_id": "user_12345",
  "token": "eyJhbGciOiJIUzI1NiJ9...",
  "partner_order_ref": "order-abc-123"
}

Response

The execute response returns the created order object — see the Orders guide for the full order schema and status semantics.

partner_order_ref Uniqueness

⚠️

partner_order_ref must be unique per partner

If you supply a partner_order_ref, the value must be unique across all orders under your partner account. Reusing a value returns error 2020 with HTTP 409 Conflict. This is a reconciliation-uniqueness rule, not a duplicate-submission guard — always use a fresh, unique reference for every new order.

📘

Use partner_order_ref for reconciliation

If you pass a partner_order_ref, you can later fetch the order using your own reference rather than CoinMENA's internal order ID. This simplifies reconciliation when your system is the system of record. See Orders for retrieval patterns.


Why Execute Can Fail

Beyond transport-level errors (timeouts, 5xx, etc.), execute can fail for these business reasons:

ReasonError CodeFix
Token is expired, already used, or client-mismatched4008Re-calculate a fresh quote and use the new token
Duplicate partner_order_ref2020Use a new unique reference, or check whether the original order already succeeded
Client is not verified2009Only verified clients can trade
Client is deactivated2010Activate the client via PUT /v1/partner/clients/{partner_client_id}/activation
Invalid client ID2008Verify partner_client_id matches the client from the quote step

Failures specific to the quote step (4000, 4001, 4003, 4004, 3005, 2013) cannot occur at execute — those are already resolved by the time you have a valid token. If quote succeeded, the trade is priced, within limits, and the client has funds.


Client Trading Eligibility

Before any trade can go through, the client must meet these conditions:

RequirementCheckError If Not Met
Client existspartner_client_id belongs to your partner account2008
Client is verifiedstatus: verified2009
Client is activeactive: true2010
Sufficient balanceFor buy: quote asset balance ≥ total cost. For sell: base asset balance ≥ amount3005
Pair is tradeabletradable: true on the asset pair4000
Trading is not pausedPrice feed is available4001
Amount within limitsmin_volumebase_amountmax_volume4003 / 4004

See Clients for the active and status model, and Error Codes for full error details.


Amount Fields — Decimals and Precision

Amount fields (base_amount, quote_amount, market_price, fee_amount, etc.) are returned as JSON strings and may use scientific notation for very small values (e.g. "0E-8", "1e-8").

⚠️

Do not parse decimals as numbers

Parsing amount fields as JavaScript number or Python float causes precision loss that compounds across trades. Always use a decimal-aware type — Decimal in Python, decimal.js in JavaScript, or equivalent.

Submit amounts as strings in your requests too — e.g. "base_amount": "0.001", not 0.001. This preserves precision on the way in.

Match the pair's inv_decimal_places and decimal_places when submitting amounts — see Precision Per Pair.


Common Mistakes

The most frequent issues partners hit when implementing trading:

MistakeFix
Sending both base_amount and quote_amountSend exactly one. Both or neither returns a validation error
Parsing amounts as float / NumberUse a decimal-aware type — Decimal in Python, decimal.js in JavaScript
Using a token after lifetime secondsRe-calculate a fresh quote — tokens are short-lived
Re-using a token after a successful executeTokens are single-use — each execute requires a new quote
Executing with a different partner_client_id than the quoteThe token is bound to the original client — the call returns 4008
Calculating a quote on a tradable: false pairReturns 4000. Filter pairs by tradable: true before showing them to users
Showing an expired quote price in the UIRefresh or re-quote before the lifetime elapses
Assuming buy and sell prices are the samebuy and sell differ by the partner spread — show the correct side's price based on the client's action
Reusing a partner_order_ref across ordersEach reference must be unique per partner. Duplicates return 2020

Related Error Codes

The errors you are most likely to see during trading:

CodeTriggerEndpoint
2008Invalid client IDQuotes, Execute
2009Client is not yet verifiedQuotes, Execute
2010Client is deactivatedQuotes, Execute
2013Invalid asset pairQuotes
2020Duplicate partner_order_refExecute
3005Insufficient fundsQuotes
4000Trading is not available (pair is not tradeable)Quotes
4001Trading is temporarily unavailable (price feed down)Quotes
4003Amount exceeds the maximum allowedQuotes
4004Amount is below the minimum allowedQuotes
4008Trading token is invalid (expired, reused, or client mismatch)Execute

See the Error Codes guide for the full reference.


What’s Next
Next StepDescription
OrdersThe order object, statuses, and retrieval patterns
FundingDeposit and withdraw fiat balances for your clients
Error CodesFull reference for every error the API returns