Primary sale API
The three primary-sale rails, the quote lifecycle both on-chain rails share, and the refusal codes an integrator branches on.
A primary sale is a buyer's first acquisition of an asset, and it settles one of three ways. Which one applies is a property of the market, not a choice the caller makes at request time. Read the market first, then call the rail it names.
| Rail | What it is | Where the buyer's money goes | Endpoints |
|---|---|---|---|
Bank transfer (offchain_fixed_price) | The buyer pays by transfer and the units are delivered once the payment is reconciled. | an Assetera client account | /offchain-instruments, /offchain-purchases |
Third-party venue (onchain_rfq_atomic) | A supplier's own settlement contract fills the order atomically on chain. | the supplier's contract | /rfq/... |
Assetera issuance (onchain_primary_mint) | The units are minted into existence at purchase, by the offering's own sale contract. | the offering's venue contract | /primary-mint/... |
The bank-transfer rail has its own journey page: Off-chain sale. This page covers the two on-chain rails, which are deliberately the same shape.
How you read the rail
The market carries primaryExecutionKind, and it is on the customer-facing catalog read, not only on
the administrative one, precisely so a storefront can route the buyer to the right flow. Branch on it.
primaryExecutionKind | Rail | Also carried on the market |
|---|---|---|
offchain_fixed_price | bank transfer | |
onchain_rfq_atomic | third-party venue | rfqVenueContract, rfqInstrumentId, and two independent switches, rfqQuotesEnabled and rfqExecutionEnabled |
onchain_primary_mint | Assetera issuance | issuanceVenueContract |
null | the market has no primary sale at all |
Do not infer the rail from primaryMarket
primaryMarket tells you a primary sale exists. It does not tell you how that sale executes, and the
three rails need three different sets of calls. A front end that guesses from primaryMarket reproduces
one layer up the exact defect this field exists to prevent.
On an onchain_rfq_atomic market the two switches move independently, and the asymmetry is deliberate:
quoting off with execution still on is a wind-down that lets an intent already signed by a buyer complete.
Both off at once strands it.
The quote lifecycle
Both on-chain rails run the same four calls in the same order. Nothing is signed until step three has persisted a firm quote, and nothing is spent until the buyer submits the transaction themselves.
Step 4 is the last server-side check
Handing over the signed bundle is the final moment Assetera can refuse. Every eligibility rule that applies to a trade is applied here, in the same order: market lifecycle and visibility, the trading window, the buyer's own eligibility axis, appropriateness per asset class, and the trade-size limits. A quote that returned happily at step three can still be refused at step four, and that is correct behaviour rather than a fault.
Endpoints
The two rails mirror each other. Substitute the prefix and everything else is the same shape.
| Step | Assetera issuance | Third-party venue | Auth |
|---|---|---|---|
| Indicative price, binds nothing | POST /primary-mint/quotes/soft | POST /rfq/quotes/soft | tenant + KYC |
| Is the wallet ready to settle | POST /primary-mint/allowance | POST /rfq/allowance | tenant + KYC |
| Firm quote, persisted | POST /primary-mint/quotes | POST /rfq/quotes | tenant + KYC |
| The signed settlement bundle | POST /primary-mint/quotes/:reference/intent | POST /rfq/quotes/:reference/intent | tenant + KYC |
| Read one quote, with its freshness | GET /primary-mint/quotes/:reference | GET /rfq/quotes/:reference | tenant |
| List the caller's own quotes | GET /primary-mint/quotes | GET /rfq/quotes | tenant |
Exact request and response bodies, including every field name and status code, are in the OpenAPI contract and Swagger UI. This page covers the shape, the rules and the refusals, which the schema cannot express.
Requests, in order
Soft quote. An indicative price. It binds nothing, persists nothing, and is safe to call on every keystroke.
POST /primary-mint/quotes/soft
Authorization: Bearer <access_token>
Content-Type: application/json
{ "pairId": 42, "cashAmount": "100000000" }Allowance. Asks whether this wallet can actually settle: its balance, and its allowance against the settlement router. Call it before you show a buy button, not after the buyer presses it.
POST /primary-mint/allowance
{ "pairId": 42, "walletAddress": "0x...", "cashAmount": "100000000" }Firm quote. Persisted before any signature is requested, and it returns a reference. For the
issuance rail a reference matches MINT followed by up to 32 hexadecimal characters.
POST /primary-mint/quotes
{ "pairId": 42, "walletAddress": "0x...", "cashAmount": "100000000" }walletAddress must be registered and screening-approved in Compliance, and it is the only
address that can submit the settlement: the router asserts that the intent's buyer equals the
meta-transaction sender, so a quote drawn for one wallet cannot be settled by another.
Intent. Exchanges the reference for the signed settlement bundle: the settlement intent, the opaque venue calldata, and the three signatures Assetera produces (the settlement operator's, the compliance attestation and the fee attestation). The buyer then signs the same EIP-712 digest.
POST /primary-mint/quotes/MINT7F3A/intentThis call is idempotent. Asking twice for the same reference returns the same bundle rather than minting a second one, so a retry after a timeout is safe.
Two rules that cause most first-integration failures
Amounts are base units, always
cashAmount is a string of digits only: no decimal point, no sign, no exponent, no 0x
prefix. "100000000" is 100 units of a 6-decimal currency, not one hundred million. Never build
this value by multiplying a float. See Tokens and pairs for where
the decimals come from.
An unknown property is rejected, not ignored
Every request body is validated with additionalProperties: false, so a misspelled or extra field
returns 400 instead of being silently dropped. On a route that hands out signatures, a
quietly-discarded field would mean a caller believing they asked for something they did not. Do not
send fields that are not in the schema, including ones you expect to be added later.
Refusals
Every refusal returns the same envelope, an error code and a human-readable detail. Branch on the
code, never on the message text. The codes below are the ones the issuance rail returns. The
third-party rail mirrors them under an rfq_ prefix; take its exact list from the
OpenAPI contract rather than assuming a
one-to-one mapping.
The market is not sellable
| Code | What it means |
|---|---|
mint_market_not_found | No market with that pairId is visible to your tenant. |
mint_not_a_mint_market | The market exists but settles on a different rail. Read its execution kind and call that rail instead. |
mint_market_not_configured | The market names this rail but its configuration is incomplete. This is an Assetera-side gap, not a caller error. |
mint_market_paused | Trading on this market is suspended. |
The quote
| Code | What it means |
|---|---|
mint_quote_not_found | No such reference for this caller. References are caller-scoped. |
mint_quote_expired | The firm quote's freshness window has passed. Take a new quote; do not retry this one. |
mint_price_moved | The offering's on-chain price changed between the quote and the intent. Re-quote and show the buyer the new price. |
mint_intent_digest_mismatch | The bundle no longer matches the persisted quote. Re-quote. |
The buyer or the wallet
| Code | What it means |
|---|---|
mint_wallet_not_registered | The wallet is not registered and screening-approved for this customer. |
mint_kyc_attestation_refused | Compliance declined to attest this settlement. The buyer is not eligible for it. |
mint_purchase_cap_exceeded | The purchase exceeds the offering's per-purchase cap. Note that a cap of zero means the offering is closed, not unlimited. |
mint_nothing_to_mint | The amount rounds down to zero units at this price. |
A dependency is unavailable
These are transient and safe to retry with backoff. They are Assetera-side, so do not surface them to the buyer as a rejection.
| Code | Dependency |
|---|---|
mint_price_read_failed | The on-chain price could not be read. |
mint_venue_unavailable, mint_venue_mismatch | The offering's venue contract. |
mint_signer_unavailable, mint_signer_declined | The settlement signer. |
mint_wallet_registry_unavailable | The wallet registry. |
mint_kyc_attestation_unavailable | Compliance could not be reached. |
mint_kyc_attestation_unsupported | Compliance cannot attest this action on this contract. Configuration, not load. |
Next
Primary issuance
What happens below the API boundary: the settlement intent, the four signatures, and the event to read.
Issuance venue
Where the price lives on the issuance rail, and the decimals trap to read before you integrate.
Off-chain sale
The bank-transfer rail, end to end.
Marketplace API
Authentication, tenant scoping and the other resource groups.
Marketplace API
The live, tenant-scoped Marketplace REST API for catalog, markets, trading, activity, and primary-sale integrations.
Compliance API
The customer-scoped API for onboarding status, KYC SDK sessions, and registered-wallet status. Every route resolves the caller from the bearer token, never from a parameter.