Payments API

The Payments API allows you to create payment sessions and retrieve their status. Each payment generates a hosted checkout URL that you redirect your customer to.

Telegram Stars uses a separate endpoint — it is not part of Vexutopia's standard hosted checkout flow for fiat and on-ramp payments. See Payment Providers for context.

Endpoints

POST/v1/payments

Create a new payment session (fiat, card, and on-ramp options on the hosted checkout)

POST/v1/payments/telegram-stars

Create a Telegram Stars payment (XTR) — optional; same /pay checkout, then Telegram app

GET/v1/payments/:id

Retrieve a payment by ID

POST/v1/payments/:id/resend-webhook

Re-fire the payment.completed webhook for a completed transaction

The Payment Object

JSON
{
  "id": "tx_b84050910b493a1e",
  "status": "pending",
  "amount": "20",
  "currency": "USD",
  "checkout_url": "https://checkout.vexutopia.com/pay?session=tx_b84050910b493a1e",
  "return_url": "https://yoursite.com/order/success",
  "provider": "card",
  "customer_email": "[email protected]",
  "customer_name": "Jane Smith",
  "failure_reason": null,
  "metadata": {
    "order_id": "1234"
  },
  "created_at": "2026-04-06T19:47:36.807Z",
  "updated_at": "2026-04-06T19:47:39.020Z",
  "expires_at": "2026-04-07T19:47:38.000Z",
  "completed_at": null
}
Attributes
ParameterTypeDescription
idstringUnique identifier for the payment (tx_ prefix)
statusstringPayment status: pending, completed, failed, cancelled, expired, refunded, or partially_refunded. See the Payment Statuses table below for the meaning of each.
amountstringDecimal amount as a string (e.g., "20" or "19.99"). For an underpaid payment you accepted, the amount actually received.
amount_requestedstringOnly present when you accepted an underpaid payment (crypto, or a gift card order whose codes fell short): the original invoice amount. See Partial payments.
currencystringThree-letter ISO currency code. Supported: USD, EUR, GBP, CAD, AUD, JPY, BRL, MXN, ARS, RUB, CNY, IDR, PHP
checkout_urlstringHosted checkout URL — redirect your customer here to complete payment
return_urlstringURL the customer is returned to after payment
providerstringDeprecated alias of processor — kept for backward compatibility. Use processor in new integrations.
processorstringBrand-neutral rail code for the underlying processor: crypto_onramp, crypto_direct, regional, china_pay, or stars. Populated after the customer completes checkout — not a request field.
payment_methodstringMethod the customer paid with. Possible values: card (crypto on-ramp — customer pays with a card, you settle in USDT), pix, c2c, sbp, qris, gcash, maya, telegram_stars, crypto, alipay, wxpay, gift_card, cashapp, or mastercard. See the rails section below for which countries each supports.
customer_emailstringCustomer email if provided
customer_namestringCustomer name if provided
failure_reasonstringReason for failure if status is failed
metadataobjectCustom key-value pairs you passed at creation
created_atstringISO 8601 timestamp of creation
updated_atstringISO 8601 timestamp of the most recent status change
expires_atstringISO 8601 timestamp when the checkout link expires (1 hour)
completed_atstringISO 8601 timestamp when payment was completed, or null

Create a Payment

POST/v1/payments

Create a new payment session and get a hosted checkout URL

Request Body

ParameterTypeDescription
amountrequiredstringDecimal amount as a string — e.g., "20" or "19.99". Not cents.
currencyrequiredstringThree-letter ISO currency code. Supported: USD, EUR, GBP, CAD, AUD, JPY, BRL, MXN, ARS, RUB, CNY, IDR, PHP. Most rails convert to USDC at the live rate. CNY, IDR, and PHP stay in the local currency and settle to your merchant balance.
return_urlrequiredstringURL to redirect the customer after payment completes
customer_emailstringCustomer email address
customer_namestringCustomer full name
customer_phonestringCustomer mobile. Required when currency is IDR — Indonesian mobile in 628… form (08… and +62… are accepted and normalized). Missing or invalid numbers return 422 CUSTOMER_PHONE_REQUIRED. Also required when currency is PHP — Philippine mobile in 639… form (09… and +63… are accepted and normalized). On a USD hosted checkout the pay page collects it when the customer picks QRIS, GCash or Maya.
customer_idstringYour internal customer ID for reference
webhook_urlstringURL to receive payment status notifications
metadataobjectCustom key-value pairs (string values only)
descriptionstringWhat the customer is buying, shown on the hosted checkout under the amount — e.g. "Yearly Subscription". Max 120 characters. Display only.
detailsstringOne extra line under the description — e.g. "2,000 initial coins, then 1,000 coins per month". Max 240 characters. Display only.
subscriptionobjectShows a renewal notice on the hosted checkout, translated into the checkout language — e.g. "After 365 days, your subscription renews automatically and costs €113.99 every 365 days. You can cancel anytime." Fields: interval ("day" | "week" | "month" | "year"), interval_count (integer, default 1), amount (string, defaults to the payment amount), currency (defaults to the payment currency). Display only — it does not start recurring billing.
countrystringTwo-letter ISO 3166-1 alpha-2 country code of the customer. Used to filter region-restricted providers and to pre-select the customer's region on the hosted checkout page. Optional — if omitted, we fall back to IP-based geolocation; the customer can still change the region themselves.
localestringOptional UI language for the hosted checkout (BCP-47) — e.g. "fr", "de", "ja", "pt-BR", "ar". The whole checkout (amounts, payment options, help, and the success screen) renders in this language, with right-to-left layout for Arabic and Hebrew. Forgiving about format: "FR", "fr-FR", "pt" (→pt-BR), and "zh-Hant" (→Traditional Chinese) all resolve. If omitted or unrecognized, the checkout follows the customer's region (e.g. Portuguese in Brazil) and otherwise falls back to English. You can also override per-link by appending ?locale= to the returned checkout_url. Supported: en, pt-BR, es, ru, zh-CN, zh-TW, fr, de, it, nl, pl, tr, ja, ko, hi, id, vi, th, uk, ro, cs, sv, el, ar, he.
directbooleanSkip the multi-method hosted checkout: checkout_url is then a Vexutopia-hosted page on vexutopia.com for the one local method (you redirect your customer there yourself). v1 supports BRL (PIX), ARS (C2C), and RUB (SBP). Your site must be approved for regional payments. See "Direct mode" below.
cryptobooleanRoute this payment to the direct-crypto checkout (customer pays in BTC, ETH, stablecoins, or other supported assets). Returns checkout_url on vexutopia.com/pay/crypto/… — not the multi-method /pay page. Minimum invoice USD 3 equivalent. See "Crypto (direct)" below.
mastercardbooleanSend the customer straight to a Mastercard-only checkout. Returns checkout_url on vexutopia.com/pay/mastercard/… — not the multi-method /pay page. USD or EUR; charged in EUR. Needs an approved site (otherwise 403 MASTERCARD_NOT_ENABLED) with the method switched on under Settings → Payment Methods (otherwise 403 MASTERCARD_DISABLED). Works regardless of whether you show Mastercard on your main checkout. Cannot be combined with crypto, direct, payment_method or gift_card_amount. See "Mastercard" below.
payment_methodstringPicks the local rail when a currency has more than one. CNY: "ALIPAY" or "WXPAY" (defaults to Alipay; 1–1,000 CNY). While WeChat Pay is temporarily unavailable, a WXPAY payment is created as Alipay and reports payment_method alipay. IDR: "QRIS". PHP: "GCASH" or "MAYA" (defaults to GCash; 100–50,000 PHP). IDR range is 10,000–10,000,000. Outside those windows: 422 AMOUNT_OUT_OF_RANGE (or the China Pay amount codes for CNY). Your site must be approved for China Pay or regional payments as appropriate. Passing "mastercard" opens the Mastercard checkout — the same as mastercard: true.
gift_card_amountnumberUSD gift-card face to show on hosted checkout: 5, 10, 15, 20, 25, 30, 40, 50, 75, 100, 250, 500. The invoice amount must sit in that face's window (90% of face up to face). A $14 invoice uses 15; an $8 product cannot use 5 or 10 — the request is rejected with GIFT_CARD_AMOUNT_MISMATCH. USD only. Omit to let checkout pick a matching face when the invoice already sits in a window. See Gift cards below.
preferred_methodstringWhich method leads on the hosted checkout for this payment: it is pre-selected when the page opens and sits first in the method list. Nothing is hidden — the customer can still pick any other method you have on. One of: card (crypto on-ramp), card_direct, mastercard, cashapp, gift_card, telegram_stars. An unknown value returns 422 INVALID_PREFERRED_METHOD. A method that is not available for that customer (wrong country, amount out of range, switched off) is skipped and the usual order applies. Local methods — Alipay/WeChat in China, PIX, C2C, SBP, QRIS, GCash and Maya in their own countries — still lead for customers there.
allowed_methodsstring[]Restrict the hosted checkout to these methods for this payment — e.g. ["gift_card"] for a gift-card-only button on your site. Every other method is hidden, and refused if the customer tries to start it another way. Omit to allow every method you have on. Accepted: card (crypto on-ramp), card_direct, mastercard, cashapp, gift_card, telegram_stars, pix, c2c, sbp, qris, gcash, maya, alipay, wxpay. Unlike preferred_method, local methods are restricted too: with ["gift_card"] a customer in Brazil does not see PIX. USD hosted checkouts only (or with mastercard: true, which it must then include); cannot be combined with crypto, direct, card_direct, card_payment or payment_method (422 ALLOWED_METHODS_CONFLICT). An unknown value or an empty list returns 422 INVALID_ALLOWED_METHODS; if none of the listed methods is switched on for your account you get 422 NO_ALLOWED_METHOD_ENABLED. With a single method, customers who can't use it (wrong country, amount out of range) see no way to pay — keep a second method in the list, or offer an unrestricted button too. payment.completed still reports the method actually used in payment_method.

Example Request

cURL
curl -X POST https://vexutopia.com/api/v1/payments \
  -H "X-API-Key: vex_test_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "20",
    "currency": "USD",
    "locale": "fr",
    "return_url": "https://yoursite.com/order/success",
    "customer_email": "[email protected]",
    "metadata": {
      "order_id": "1234"
    }
  }'
JavaScript
const response = await fetch('https://vexutopia.com/api/v1/payments', {
  method: 'POST',
  headers: {
    'X-API-Key': 'vex_test_your_api_key',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    amount: '20',
    currency: 'USD',
    return_url: 'https://yoursite.com/order/success',
    customer_email: '[email protected]',
    metadata: { order_id: '1234' }
  })
});

const payment = await response.json();

// Redirect the customer to complete payment
window.location.href = payment.checkout_url;

Response

JSON
{
  "id": "tx_b84050910b493a1e",
  "status": "pending",
  "checkout_url": "https://checkout.vexutopia.com/pay?session=tx_b84050910b493a1e",
  "expires_at": "2026-04-07T20:16:49.470Z",
  "amount": "20",
  "currency": "USD",
  "created_at": "2026-04-06T20:16:47.932Z"
}

How it works

After creating a payment, redirect your customer to checkout_url. They will choose how to pay from the options shown at checkout (these vary by the customer's country and amount) and complete payment there. Once paid, Vexutopia fires a webhook to your webhook_url and your payout is sent instantly to your configured wallet. The checkout link expires after 1 hour.

Wallet requirement: On-ramp payments arrive directly in this wallet, in the coin and network the customer's on-ramp sends — for example USDC or POL on Polygon, PYUSD, USDC or ETH on Ethereum, USDC on Base, or BNB on BNB Chain. Use a self-custody EVM wallet (MetaMask, Rabby, etc.): the same 0x address receives on all of these networks. Exchange deposit addresses often credit only some coins and networks, and funds sent in any other coin or network can be lost.

Provider selection is automatic

Do not include a provider field in your request — it will cause an error. The payment provider is chosen by your customer on the checkout page. The provider field only appears in the response and webhooks after checkout to tell you which provider was used. Exception: when you set crypto: true, routing is fixed to the direct-crypto flow — customers do not pick a card/bank on-ramp provider for that session.

Multi-currency

Supported currencies: USD, EUR, GBP, CAD, AUD, JPY, BRL, MXN, ARS, RUB, CNY, IDR, PHP. For most rails, the amount is automatically converted to USDC at the live rate before the customer sees the checkout, and your payout is delivered in USDC. CNY, IDR, and PHP stay in the local currency and settle to your Vexutopia merchant balance instead of being converted on the fly. Unsupported currency codes return a 422 validation error.

Crypto (direct)

Pass crypto: true on POST /v1/payments to start a cryptocurrency checkout. The response checkout_url is on vexutopia.com/pay/crypto/<id> (embedded hosted flow; your customer completes payment there). This rail is not available as a choice on the standard multi-method /pay page — your backend must opt in per payment.

  • Fees — one all-in percentage, shown as a single rate on the invoice (see Payment Providers)
  • Minimum — USD 3 equivalent in your requested currency
  • Underpayment tolerance — sends within 1% of the quoted amount still settle as completed, for the full amount. A larger shortfall leaves the payment partially paid (see below)
  • return_url — may include {payment_id}, {order_id}, {transaction_id}, and {status}; these are replaced when the customer is sent back to your site
  • Webhooks — same events as other rails; payment_method is crypto

Example request

cURL
curl -X POST https://vexutopia.com/api/v1/payments \
  -H "X-API-Key: vex_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "10.00",
    "currency": "USD",
    "crypto": true,
    "return_url": "https://yoursite.com/pay/return?id={payment_id}&status={status}",
    "webhook_url": "https://yoursite.com/api/webhooks/vexutopia"
  }'

Store the returned id in your own database before redirecting — your return page should look up that id server-side (do not rely on the URL alone for fulfillment).

Partial payments

Two rails can leave a payment part-paid. Crypto: customers sometimes send less than the invoice, most often because their exchange deducts a withdrawal fee from the amount sent. When the shortfall is more than 1%, the payment is not completed and nothing is credited yet. Gift cards: a customer enters a code worth less than the order. The code is used up, so its value counts toward the order. The customer has 24 hours to add another code. You can click Accept before then. When the window ends, an order with at least $2 received completes at that amount on its own. Under $2 it expires. Deliver for amount on payment.completed only. A worked example, including how to turn a short payment into credits, is under Gift Card → What to deliver. Crypto underpayments follow the rest of this section:

  • We send payment.partial with status partially_paid, the invoice amount and amount_received in your currency. Crypto sends it once and adds asset and network for what arrived on-chain; gift cards send it each time a code is credited while the order is still short.
  • The checkout asks the customer for the rest. If they pay it in time (48 hours for crypto, 24 hours after the last gift card code), the payment completes for the full amount as usual. A gift card order that still has at least $2 received when that window ends completes at the received amount. Under $2 it expires with amount_received on payment.expired.
  • The payment shows as Partially paid on your Payments page, and the crypto_deposit object on GET /api/v1/payments/{id} has status underpaid with received_amount and remaining_amount.

Fulfil only on payment.completed, for amount. payment.partial and payment.expired are information only: nothing has been credited to you yet, and the customer may still pay the rest, in which case the payment completes for the full amount. Granting on the first notice would give the customer the product twice.

Accepting a partial payment

If you are happy to take the shortfall, click Accept on the payment in your dashboard (Payments → the banner, or any payment marked Partially paid). Account owners and admins can do this. The payment then completes at the amount the customer actually sent:

  • Your balance is credited for the received amount minus fees (the fee applies to the settled amount). The shortfall is never credited.
  • payment.completed fires with amount set to the received amount and amount_requested set to the original invoice amount. If your handler checks that amount matches the order total, allow for this case.
  • GET /api/v1/payments/{id} returns the same amount and amount_requested.
  • Accepting can't be undone. Anything the customer sends to the payment after you accept is not credited, so tell them it's settled. If they had already paid the rest when you click Accept, the payment completes for the full amount instead.

If you don't accept a crypto underpayment, it stays partially paid and nothing is credited. Contact support if you want those funds returned to the customer. A gift card code cannot be un-redeemed, so a part-paid gift card order completes at the received amount when its 24-hour window ends (under $2 it expires instead). Deliver for amount. See Gift Card → Part-paid orders.

Mastercard

Pass mastercard: true on POST /v1/payments to send the customer straight to the Mastercard checkout. The response checkout_url is on vexutopia.com/pay/mastercard/<id>. Use it for a dedicated “Pay with Mastercard” button next to your other payment options — the same way crypto: true gives crypto its own button.

Getting access

Mastercard is the one rail that needs approval, and it is granted per site. Ask for it under Settings → Payment Methods — we review the site and enable it. Until then this call returns 403 MASTERCARD_NOT_ENABLED. Nothing else about your account changes while a request is open.

Where it appears, and your two switches

Both live under Settings → Payment Methods once the site is approved.

  • Mastercard — the method itself. Off means off everywhere, including this call, which then returns 403 MASTERCARD_DISABLED. It is on by default once approved.
  • Show Mastercard on your main checkout — where it is offered, not whether. On (the default), customers see a Mastercard option on your multi-method /pay checkout alongside your other methods. Off, they do not, and Mastercard is reached only through mastercard: true — which opens a checkout carrying Mastercard alone. Use that if you would rather have a separate “Pay with Mastercard” button on your site than a card option among many.

The second switch never affects this endpoint: a dedicated Mastercard checkout keeps working whether or not the rail is shown on your main checkout.

  • Currencies — price in USD or EUR. The card is always charged in EUR; USD amounts are converted at our rate when the checkout opens.
  • Fees — 19% per transaction, deducted from the order amount when you settle. No payout fee and no refund fee; a chargeback costs EUR 100. See the fee summary on the Payment Providers page.
  • Card fee — the card network adds a EUR 0.62 fixed fee to every order, charged to your customer on top of the order total and shown on the checkout before they enter their card. It is not part of your fee: your customer pays it and it never passes through your balance, so it neither adds to nor reduces your payout — your fee is the 19% above. Worth knowing on small orders: EUR 0.62 on a EUR 3 order is about a fifth again on top.
  • Per-order range — EUR 2 minimum, with a ceiling of about EUR 500 that can shift slightly over time. Outside it you get 422 with AMOUNT_BELOW_MINIMUM or AMOUNT_ABOVE_MAXIMUM.
  • Countries — Mastercard is not available for customers in: Afghanistan, Albania, Barbados, Burkina Faso, Cambodia, Cayman Islands, Cuba, Haiti, Jamaica, Jordan, Mali, Malta, Morocco, Myanmar, Nicaragua, North Korea, Pakistan, Panama, Philippines, Senegal, Somalia, South Sudan, Syria, Uganda, Venezuela, Yemen. Pass the customer's country and a payment for one of these returns 422 COUNTRY_NOT_SUPPORTED straight away, and the Mastercard option is not shown on your main checkout. Without country we cannot check up front, and the card is declined at payment instead — offer these customers another method.
  • return_url — supports {payment_id}, {order_id}, {transaction_id} and {status}, like the other hosted checkouts.
  • Webhooks — same events as other rails, carrying the id this call returned; payment_method is mastercard.

Example request

cURL
curl -X POST https://vexutopia.com/api/v1/payments \
  -H "X-API-Key: vex_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "25.00",
    "currency": "USD",
    "mastercard": true,
    "customer_email": "[email protected]",
    "return_url": "https://yoursite.com/pay/return?id={payment_id}&status={status}",
    "webhook_url": "https://yoursite.com/api/webhooks/vexutopia"
  }'

Passing customer_email skips the email step on the checkout, so the customer goes straight to entering their card.

Gift cards

Gift card checkout only works when the invoice matches a fixed USD face. Each face covers 90% of the face up to the face. A $14 invoice uses the $15 card. We do not stretch a $5 or $10 card over an $8 product, unless you turn on Accept the next smaller gift card (Settings → Payment Methods → Gift Card): checkout then offers the next smaller face when it is at most $5 short, and redeeming it completes the order at that face. More detail: Gift Card.

FaceInvoice windowgift_card_amount
$5$4.50 – $5.005
$10$9.00 – $10.0010
$15$13.50 – $15.0015
$20$18.00 – $20.0020
$25$22.50 – $25.0025
$30$27.00 – $30.0030
$40$36.00 – $40.0040
$50$45.00 – $50.0050
$75$67.50 – $75.0075
$100$90.00 – $100.00100
$250$225.00 – $250.00250
$500$450.00 – $500.00500

Pass gift_card_amount on POST /v1/payments to choose the face shown. If you omit it and the invoice already sits in a window, checkout picks that face. If you pass a face that does not cover the invoice, the create call returns 422 GIFT_CARD_AMOUNT_MISMATCH. If the usual listing on the default store is sold out, checkout opens another in-stock listing on that store for the same face, then the other store if every listing there is gone. The tile stays hidden when neither store has stock.

Gift Card is on by default. Hide it from Settings. It is hosted checkout only. Other methods still appear. The session stays pending until the 16-character activation codes the customer pastes cover the invoice (a code worth less is credited and the customer is asked for the rest). Completing that fires payment.completed with payment_method: "gift_card". Credit is the invoice, not the card face. Fee is 14% + $1, T+1. You can request one Gift Card payout per day.

Cash App

US customers pay a Lightning invoice from Cash App or any Lightning wallet. You are credited the invoice in USD. This is not Cash App Pay and not an on-chain Bitcoin address. On by default; hide it from Settings. More detail: Cash App.

Completing fires payment.completed with payment_method: "cashapp" and processor: "lightning". Fee is 7%, T+0. $3 minimum. New York Cash App accounts may not be able to pay.

Indonesia & Philippines

Create the payment in IDR or PHP and send the customer to checkout_url. Hosted checkout shows QRIS in Indonesia and GCash / Maya in the Philippines when the region matches. Fee is 10% all-in, same-day settlement (T+0). Your site needs regional payments approval. These rails are not available with direct: true — see Direct Integration.

  • Indonesia — QRIS, IDR 10,000–10,000,000. Pass customer_phone as a local mobile in 628… form. An IDR create without it returns 422 CUSTOMER_PHONE_REQUIRED. Hosted USD checkout still collects the number when the customer picks QRIS.
  • Philippines — GCASH or MAYA, PHP 100–50,000. PHP without payment_method defaults to GCash. Pass customer_phone (local mobile in 639… form), customer_email and customer_name (first and last name). A PHP create without a phone returns 422 CUSTOMER_PHONE_REQUIRED, without an email 422 CUSTOMER_EMAIL_REQUIRED. Hosted USD checkout asks the customer for whichever of these you did not send.
  • Amounts outside those windows return 422 AMOUNT_OUT_OF_RANGE.
  • Webhooks use payment_method qris, gcash, or maya. Same child-transaction correlation as other regional rails.

Example — QRIS

cURL
curl -X POST https://vexutopia.com/api/v1/payments \
  -H "X-API-Key: vex_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "150000",
    "currency": "IDR",
    "payment_method": "QRIS",
    "customer_phone": "6281234567890",
    "return_url": "https://yoursite.com/order/success",
    "webhook_url": "https://yoursite.com/api/webhooks/vexutopia"
  }'

Example — GCash

cURL
curl -X POST https://vexutopia.com/api/v1/payments \
  -H "X-API-Key: vex_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "500",
    "currency": "PHP",
    "payment_method": "GCASH",
    "return_url": "https://yoursite.com/order/success",
    "webhook_url": "https://yoursite.com/api/webhooks/vexutopia"
  }'

Telegram Stars (optional)

Telegram Stars can be offered in two ways. Both require a LIVE API key and Telegram Stars enabled under Dashboard → Settings (off by default).

ModeHowBest for
Mixed checkoutUse POST /v1/payments as normal — Telegram Stars appears automatically at the top of the provider list if enabledMost merchants — one API call covers all payment methods
StandaloneUse POST /v1/payments/telegram-stars to create a Stars-only checkoutTelegram bots or flows where Stars is the only payment option

Mixed checkout still prices in USD and ceil-converts to stars when the customer picks Stars. The standalone endpoint accepts currency: "XTR" with a whole-star amount (e.g. 1500) when you need an exact pack, or currency: "USD" as before. The customer is charged the integer star count on the invoice — no KYC, no minimum, works worldwide. If they don't have enough Stars, Telegram prompts them to buy more inside the app.

POST/v1/payments/telegram-stars

Create a Telegram Stars invoice; returns checkout_url (same /pay page) and telegram_invoice_url

Request Body

ParameterTypeDescription
amountrequiredstringWhole stars when currency is "XTR" (e.g. "1500"), or a USD decimal when currency is "USD" (ceil-converted to stars)
currencyrequiredstring"XTR" for an exact star amount, or "USD"
return_urlrequiredstringURL to send the customer after successful payment
customer_emailstringOptional customer email
customer_namestringOptional customer name
customer_idstringOptional your internal customer ID
webhook_urlstringOptional URL for payment status webhooks
metadataobjectOptional custom key-value pairs (string values only)

Example Request

cURL
curl -X POST https://vexutopia.com/api/v1/payments/telegram-stars \
  -H "X-API-Key: vex_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "1500",
    "currency": "XTR",
    "return_url": "https://yoursite.com/order/success"
  }'

Response

JSON
{
  "id": "tx_…",
  "status": "pending",
  "checkout_url": "https://vexutopia.com/pay?session=tx_…",
  "telegram_invoice_url": "https://t.me/$…",
  "expires_at": "2026-04-09T19:32:10.977Z",
  "amount": "1500",
  "currency": "XTR",
  "stars": 1500,
  "created_at": "2026-04-09T18:32:10.710Z"
}

Webhook events

When the customer completes payment in Telegram, your webhook_url receives a payment.completed event:

JSON
{
  "event": "payment.completed",
  "id": "tx_…",
  "status": "completed",
  "amount": "100",
  "currency": "XTR",
  "timestamp": "2026-04-09T20:13:36.280Z"
}

amount is the number of stars charged. Cross-reference with stars from the create response.

If the customer requests a Telegram chargeback (refund), you will also receive a payment.refunded event:

JSON
{
  "event": "payment.refunded",
  "id": "tx_…",
  "status": "refunded",
  "amount": "100",
  "currency": "XTR",
  "timestamp": "2026-04-09T20:45:11.000Z"
}

Telegram initiates refunds unilaterally — you cannot prevent them. Revoke access to any digital goods when you receive this event. The payment status will also update to refunded if you poll GET /v1/payments/:id.

Chargeback fee — $50 per refund

Each Telegram Stars chargeback incurs a $50 platform fee. The fee is automatically deducted from your pending Stars payout balance at the time the chargeback is received — not at your next payout cycle. Your balance in Dashboard → Settings always reflects what you will actually receive. A running total of fees is shown for transparency. High chargeback rates may result in Telegram Stars being disabled for your account.

How it works

Redirect the customer to checkout_url. They open the Telegram invoice from the Vexutopia checkout page and complete payment in Telegram. If they don't have enough Stars, Telegram prompts them to buy more automatically. Webhooks fire on completion just like other payment methods.

🔗

Want to skip our hosted page and render your own “Pay with PIX” button on your site? See Direct Integration.

Retrieve a Payment

GET/v1/payments/:id

Retrieve a payment by its unique identifier

Path Parameters

ParameterTypeDescription
idrequiredstringThe payment ID (e.g., tx_b84050910b493a1e)

Example Request

cURL
curl https://vexutopia.com/api/v1/payments/tx_b84050910b493a1e \
  -H "X-API-Key: vex_test_your_api_key"

Response

JSON
{
  "id": "tx_b84050910b493a1e",
  "status": "completed",
  "amount": "20",
  "currency": "USD",
  "checkout_url": "https://checkout.vexutopia.com/pay?session=tx_b84050910b493a1e",
  "return_url": "https://yoursite.com/order/success",
  "provider": "card",
  "customer_email": "[email protected]",
  "customer_name": null,
  "failure_reason": null,
  "metadata": { "order_id": "1234" },
  "created_at": "2026-04-06T20:16:47.932Z",
  "updated_at": "2026-04-06T20:17:05.000Z",
  "expires_at": "2026-04-07T20:16:49.470Z",
  "completed_at": "2026-04-06T20:17:05.000Z"
}

Resend Webhook

POST/v1/payments/:id/resend-webhook

Re-fire the payment.completed webhook for a completed transaction

Use this to recover from missed or failed webhook deliveries. The endpoint re-sends the payment.completed event to all your registered webhook endpoints and the webhook_url set at payment creation. Your handler should be idempotent — duplicate deliveries for an already-fulfilled payment should be a no-op on your side.

Example Request

cURL
curl -X POST https://vexutopia.com/api/v1/payments/tx_b84050910b493a1e/resend-webhook \
  -H "X-API-Key: vex_live_your_api_key"
JSON
{
  "ok": true,
  "transaction_id": "tx_b84050910b493a1e",
  "event": "payment.completed"
}

Only works on completed transactions. Returns 400 NOT_COMPLETED if the transaction is still pending, processing, or failed.

Rate limited to 60 requests per minute per API key (sliding window).

Requires a LIVE API key.

Payment Statuses

StatusDescription
pendingSession created. Awaiting customer payment, or the customer has paid and we are awaiting final payment confirmation. Both pre-payment and post-payment in-flight transactions surface as "pending" — listen on the webhook (or poll) for the terminal state.
completedPayment confirmed and payout sent.
failedPayment failed or underpayment detected.
cancelledSession was superseded by a different rail (e.g. customer abandoned card and paid via WeChat — see Regional payments correlation), or explicitly cancelled. The original session ID still appears as payment_id on the child's webhook so you can reconcile.
expiredCheckout link expired without payment (default 1 hour TTL).
refundedPayment was refunded in full.
partially_refundedPayment was refunded for less than the full amount.

Idempotency

Send an Idempotency-Key header on POST /v1/payments to make retries safe. If your service double-fires the same request (network retry, double-click, two tabs), reusing the same key returns the original response instead of creating a second payment session.

  • Generate a unique key per logical payment intent (a UUID is fine). Format: A-Z a-z 0-9 _ -, 1–255 chars.
  • Keys are scoped per API key and remembered for 24 hours.
  • Same key + identical body within 24h → original response replayed, with header Idempotent-Replayed: true.
  • Same key + different body → 409 IDEMPOTENCY_KEY_REUSED. Use a fresh key for new requests.
  • Server errors (5xx) are not cached, so a retry with the same key will be processed normally.
  • The header is optional — requests without it behave exactly as before.

Example

cURL
curl -X POST https://vexutopia.com/api/v1/payments \
  -H "X-API-Key: vex_test_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 8b1f2c4e-9d3a-4f7b-8c1d-2e3f4a5b6c7d" \
  -d '{
    "amount": "20.00",
    "currency": "USD",
    "return_url": "https://yoursite.com/order/success"
  }'

Regional payments: correlating the webhook to your original payment

When a customer pays via a regional rail (PIX in Brazil, C2C in Argentina, SBP in Russia, QRIS in Indonesia, GCash or Maya in the Philippines), we settle the payment on a child transaction with its own ID. The original transaction we returned from POST /v1/payments is moved to CANCELLED with the reason “Superseded by [rail] regional payment”, and the payment.completed webhook is fired against the child.

To credit the customer in your system, correlate by payment_id (the original ID we returned to you) or by metadata.<your_field> if you passed one through.

Webhook payload (excerpt)

JSON
{
  "event": "payment.completed",
  "id": "tx_9fbd2dd90d27c04b",          // child tx (the regional settlement)
  "payment_id": "tx_68ede60fcaf92e24",   // original tx returned by POST /v1/payments
  "status": "completed",
  "amount": "98.47",
  "currency": "BRL",
  "processor": "regional",
  "payment_method": "pix",
  "settled_via": { "rail": "PIX", "amount": "98.47", "currency": "BRL" },
  "metadata": { "order_id": "1234" },
  "timestamp": "2026-05-08T01:33:43.656Z"
}

The same pattern applies if you also call GET /v1/payments/:id against the original ID — once the child has settled, the parent reflects status CANCELLED (superseded). Always trust the webhook's payment_id field as the authoritative back-reference.

Error Handling

All errors follow a consistent format:

JSON
{
  "error": "Human-readable error message",
  "code": "MACHINE_READABLE_CODE"
}
422Validation Error
JSON
{
  "error": "Validation failed",
  "code": "VALIDATION_ERROR",
  "details": {
    "fieldErrors": {
      "amount": ["Invalid input: expected string, received number"]
    }
  }
}
400Wallet Not Configured
JSON
{
  "error": "USDC wallet not configured. Set your payout wallet in Settings.",
  "code": "WALLET_NOT_CONFIGURED"
}

See the Errors reference for a complete list.