---
title: "Create an order"
method: POST
path: "/v1/orders"
tags: ["Orders"]
---

# Create an order

`POST /v1/orders`

Pre-flight the recipient, quote the price, and create an order in
`awaiting_payment`. The response `payment` block tells you exactly how
much to send, to which address, and with which `memo` (the order id).
An ineligible recipient returns `422 recipient_ineligible` and creates
**no** order (you are never charged for an undeliverable recipient). If
eligibility cannot be verified right now (a transient upstream blip), the
call returns a **retryable `503`** and creates no order — retry shortly
with the same `Idempotency-Key`.

## Headers

- `Idempotency-Key` string, required

## Request body

- CreateOrderRequest
  - `type` 'stars' | 'premium', required
  - `recipient` Recipient, required
    - `username` string, required — Telegram @username (the leading `@` is optional, case-insensitive). After canonicalisation (strip `@`, lowercase) must match `[a-z0-9_]{1,32}` — invalid or oversized handles return 400.
  - `quantity` integer — Number of Stars. Required when `type=stars`. Must be in [50, 1000000].
  - `months` 3 | 6 | 12 — Premium subscription length in months. Required when `type=premium`. Must be 3, 6, or 12.
  - `payment_currency` 'ton' | 'usdt_ton' — `ton` = GRAM (ex TON), `usdt_ton` = USDT (TON).
  - `callback_url` string, uri — Optional HTTPS URL for the signed order-status webhook. Must be a publicly reachable `https://` URL — loopback addresses, private-network hosts, and non-HTTPS schemes are rejected with `400 bad_request`.

## Response `200`

Idempotent replay — the same key + body returns the original order.

- CreatedOrder
  - `order_id` string, uuid, required
  - `status` 'received' | 'awaiting_payment' | 'paid' | 'reserved' | 'swapping' | 'funding' | 'purchasing' | 'fulfilling' | 'completed' | 'delivered' | 'failed' | 'reversed' | 'expired' | 'held' | 'cancelled', required — Lifecycle status. `awaiting_payment` is the only cancellable state.
  - `type` 'stars' | 'premium', required — The product this order is for — echoed back from your request.
  - `quantity` integer, nullable, required — The number of Stars ordered (when `type=stars`); `null` for Premium.
  - `months` integer, nullable, required — The Premium subscription length in months ordered (when `type=premium`); `null` for Stars.
  - `payment` PaymentInstruction, required — How to pay for the order. Send EXACTLY `amount` with `memo`.
    - `currency` 'ton' | 'usdt_ton', required — `ton` = GRAM (ex TON), `usdt_ton` = USDT (TON).
    - `chain` string, required
    - `pay_to_address` string, required — The treasury wallet to pay. The SAME address is returned for both `ton` and `usdt_ton` — a USDT jetton transfer routes by owner, so its destination is this (owner) address, not a derived jetton-wallet address.
    - `memo` string, required — The required transfer memo (equals the order id).
    - `amount` string, required — Exact amount to send, as a decimal string.
    - `amount_units` 'ton' | 'usdt', required
    - `fee` FeeBreakdown, required — Itemisation of the `usdt_ton` processing fee that is ALREADY part of the all-in amount. `subtotal + processing_fee == total == amount`. Pass-through swap cost only — it does not reveal our cost basis or markup. Only `total` (= `amount`) is binding — the `subtotal`/`processing_fee` split is informational and may shift by a cent with the live FX rate.
      - `subtotal` string, required — The item price before the processing fee, as a decimal string (USDT).
      - `processing_fee` string, required — The 1% DEX swap fee + 0.5 GRAM swap gas, combined and rounded up to the cent, as a decimal string (USDT).
      - `total` string, required — subtotal + processing_fee — equals `amount`. Decimal string (USDT).
      - `description` string, required — Human-readable label for the fee components.
      - `currency` 'usdt', required — The unit of the fee amounts (always `usdt`).
  - `expires_at` string, date-time, required — After this, an unpaid order expires and is cleaned up.

## Other responses

- `201` — Order created (or replayed on an idempotent retry → 200).
- `400` — Malformed request.
- `401` — Missing or invalid `X-Api-Key`.
- `409` — Idempotency-Key reused with a different body.
- `422` — The recipient cannot receive this item. No order is created.
- `429` — A rate limit was reached — the per-minute request budget, the tighter pricing/recipient-check probe cap (60 req/min), the daily order cap, or the per-recipient flood guard. See **Rate limits** in the overview. The per-minute-budget responses also carry `RateLimit-*` + `Retry-After` headers.
- `503` — A required source was temporarily unavailable — either the price source, or recipient eligibility could not be verified right now. **Retryable**: reuse the same `Idempotency-Key` and try again shortly. No order is created and you are not charged.

---

[API](https://skmtc.net/mystars/apis/mystars-faas-fulfilment-api.md) · [All operations](https://skmtc.net/mystars/apis/mystars-faas-fulfilment-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/mystars/mystars-faas-fulfilment-api/revisions/3c24cb3dfa12/schema)
