Telegram Stars API — MyStars Fulfilment API documentation
Changelog
Every change to the public API, newest first.
- v2.0.0breaking
payment_currency is required
POST /v1/orders, GET /v1/pricing and GET /v1/pricing/batch no longer default payment_currency to `ton` (the native coin GRAM, not USDT): a request without it is rejected with 400 bad_request, so no order or quote is ever made in a currency you did not choose. The `warnings` array and the `payment_currency_defaulted` warning are gone with the default. An idempotent retry must carry the currency too — for an order you created without it, retry with `payment_currency: "ton"`. Pass `payment_currency` on every create and pricing call.
- v1.15.0deprecation
The payment block names the asset; omitting payment_currency is deprecated
`payment_currency` defaults to `ton` — the native coin GRAM (ex TON), not USDT — and a payment in the wrong asset used to go unhandled until the order expired. Three changes. (1) The `payment` block on POST /v1/orders now also says what to send: `asset` (`GRAM` or `USDT`), `decimals` (9 or 6), `amount_smallest_unit` (the exact integer to send), `transfer` (`native` or `jetton`) and `jetton_master` (the USDT jetton master, `null` for GRAM). GET /v1/orders/:id and GET /v1/orders now carry `payment_currency` on every order, and the `payment` block while the order is `awaiting_payment` (`null` otherwise). Existing fields are unchanged, and `amount_ton` is now documented accurately: the order's GRAM price, which is the amount due only on a `ton` order. (2) Omitting `payment_currency` is deprecated and it will become required: POST /v1/orders, GET /v1/pricing and GET /v1/pricing/batch now return `warnings: [{ code: "payment_currency_defaulted" }]` when they applied the default — pass the currency explicitly. (3) A payment that names your order but arrives in the other asset is now reversed to the sender in the asset received, and the order is normally marked `failed` with the new `failure_reason` value `wrong_currency` once the payment has been returned (reference in `reversal_tx`, `failed` webhook sent). If that step cannot complete, the order expires and the payment is still returned.
- v1.14.0feature
failure_reason now describes an order that is held, not only one that finished
GET /v1/orders/:id could already return a `held` order, but `failure_reason` only documented the terminal values, so a held order carried a reason the spec never defined. Two values are now part of the contract. `reversal_pending` — the goods could not be delivered and your funds are still being returned; the order reaches `reversed` when that lands. `reversal_unsettled` — the return itself could NOT be delivered to your address (it kept refusing the transfer), so you do NOT have your money back yet and a person is settling it by hand; the order stays `held` until they do. Read the second one carefully against `undeliverable`: they share a word and point in opposite directions — `undeliverable` is about the GOODS and ends with you made whole, `reversal_unsettled` is about the RETURN and does not. No field was added or removed, and no terminal value changed.
- v1.13.0feature
USDT (TON) orders can carry a network-gas-only processing fee
For `usdt_ton`, the `fee` object on GET /v1/pricing, GET /v1/pricing/batch and the POST /v1/orders `payment` block now comes in one of two forms. A swap-backed order keeps the fee it has today — the 1% swap fee plus GRAM swap gas, labelled "1% swap + <gas> GRAM gas". An order settled without a swap is charged network gas alone, labelled "TON network gas": its `processing_fee` is a few cents, so its `amount` is lower for the same item. To tell the two apart, every `fee` object now carries a `kind` field — `swap` or `network` — so you can choose your own wording without parsing `description`, which stays a display label. The change is additive: no existing field changed, and `subtotal + processing_fee == total == amount` still holds, so keep paying exactly `amount`. An order whose fee is network gas alone never passes through the `swapping` status.
- v1.12.0feature
Payment window extended to 2 hours
An order now stays open for payment for 2 hours instead of 1. Nothing changed in the shape of any request or response: expires_at on the order is still the single source of truth for the deadline, it simply lands further out. If you read expires_at rather than assuming a duration, your integration needs no change — and the spec no longer quotes a duration anywhere, precisely so it cannot go stale again. One thing worth re-checking: the SDK helpers that wait for an order to settle default to a 30-minute budget, which is now a quarter of the window rather than half, so raise it if you rely on the default to cover a full payment window.
- v1.11.0feature
Recipient checks now tell you when they could not decide
POST /v1/recipients/check always returns an `indeterminate` boolean. It is `true` when the probe could not reach a verdict and the endpoint failed open — meaning `eligible: true` is a permissive default, not a measurement — and `false` on every real verdict. Previously the two were indistinguishable on the wire, so a degraded probe looked exactly like a confirmed-deliverable recipient. Treat indeterminate as "unknown", never as "yes": proceeding to POST /v1/orders is safe (it runs its own authoritative check), but do not show the buyer a confirmed recipient. Strictly additive — the field was previously absent, and absent means the same as `false`.
- v1.10.0feature
Batch pricing + a dedicated order-lifecycle rate-limit bucket
New GET /v1/pricing/batch quotes up to 200 Stars quantities in ONE request (deduped, sorted; per-entry amount + fee match GET /v1/pricing cent-for-cent) — built for storefronts refreshing a whole pack catalog. Also: POST /v1/orders, GET /v1/orders/{id} and POST /v1/orders/{id}/cancel are now metered by a dedicated per-minute bucket of the same size as your general budget, isolated from read traffic — a burst of pricing or catalog reads can no longer 429 an order create, status poll, or cancel (and vice versa). Both changes are strictly additive; keep reading the RateLimit-* headers.
- v1.9.0feature
Payment window extended to 1 hour
An order now stays open for payment for 1 hour instead of 15 minutes. The order's expires_at field is unchanged in shape and remains the single source of truth for the deadline — it simply lands further out, giving payers more time. No request or response field changed; if you read expires_at (rather than assuming 15 minutes) nothing in your integration needs to change.
- v1.8.2feature
Order create response echoes what was ordered
POST /v1/orders now returns type plus quantity (for Stars) or months (for Premium) — the field that doesn't apply is null — so the created order is self-describing, just like GET /v1/pricing and the order view. You no longer have to correlate the response back to your request. Purely additive; existing fields are unchanged.
- v1.8.1fix
Clearer docs: request-flow diagram + recipient-check guidance
The reference now leads with the recommended call order (pricing → recipient check → order create) and a request-flow diagram in Quick start. POST /v1/recipients/check explains why type is required and warns that a recipient who already has an active Premium subscription can't be gifted Premium — Telegram blocks it (eligible:false, reason already_subscribed; the order would 422). Docs only — no contract change.
- v1.8.0feature
USDT payments show a processing-fee breakdown
For usdt_ton, GET /v1/pricing and the order-create payment block now include a fee object — subtotal, processing_fee (the 1% swap + 0.5 GRAM gas), total and a description — so you can see exactly what the swap costs. The fee was ALWAYS included in amount; this only itemises it (fee.total equals amount, never adds to it). GRAM quotes return fee: null. Purely additive.
- v1.7.1feature
Pricing response echoes what it priced
GET /v1/pricing now returns type plus quantity (for Stars) or months (for Premium) — the field that doesn't apply is null — so the amount is self-describing and you no longer have to correlate the quote back to your request. Purely additive; existing fields are unchanged.
- v1.7.0feature
New: list available products
A new GET /v1/products endpoint returns the product catalog — the two product types (stars, premium) and the buyable shape of each (stars: a continuous quantity range 50–1000000; premium: the fixed tiers 3/6/12 months). It's price-free metadata so you can build your own catalog/UI without guessing the bounds; keep using GET /v1/pricing for the price.
- v1.6.1fix
Reversal terminology standardised
The docs now use 'reversal' consistently to match the reversed order status and the reversal_tx field — the section is now 'Reversals & delivery' and adds a 'When a reversal happens' breakdown of the exact cases. No behaviour change: when and how your funds are returned is unchanged.
- v1.6.0feature
Unmatched payments are auto-refunded
A payment that arrives with no order reference (a missing or unrecognised memo) is now returned to the sender minus the network fee. If it can be attributed unambiguously to one of your pending orders, that order ends failed with failure_reason no_memo / wrong_memo and the refund reference in reversal_tx.
- v1.5.0feature
Rate limits documented; probe cap raised to 30/min
The overview now documents the per-minute request budget, the RateLimit-* / Retry-After headers, the daily order cap and the per-recipient guard. The tighter pricing & recipient-check probe cap was raised from 10 to 30 requests/min.
- v1.4.0feature
Pricing response adds quote freshness + GRAM↔USDT rate
GET /v1/pricing now returns quoted_at and valid_until (a re-quote hint) plus usdt_per_ton (the current public GRAM↔USDT rate; the field name is frozen for wire compatibility).
- v1.3.0feature
Terminal failed and expired order-status webhooks now fire
If you set a callback_url, you now also receive the terminal failed and expired webhook events — previously only delivered and reversed were delivered.
- v1.2.0breaking
Pricing response returns amount + currency only
GET /v1/pricing now returns just the final amount and currency. The previous breakdown object has been removed — amount is already the full, all-in total to pay, so no change is needed unless you read breakdown fields.
- v1.1.0feature
Pay in USDT (TON), plus a price breakdown
Orders and quotes now accept payment_currency: usdt_ton alongside ton (GRAM). The pricing response gained a breakdown object (Fragment cost, markup, gas, and DEX fee + swap gas for USDT).
- v1.0.0breaking
recipient is now an object, not a bare string
Create-order and recipient-check requests take recipient: { username } instead of a top-level username string. Update your request bodies before this version — the old shape is rejected with 400 bad_request.