Documentation
Payment system for partners and AI bots, with added monetization features
Part 1: Introduction & Authentication
PAYchat Partner API
Build seamless USDT payments into your app with our simple REST API.
Welcome
PAYchat Partner API lets you:
- Accept payments — payment links, QR codes, and escrow
- Send money — instantly transfer USDT to any Telegram user
- Track everything — real-time webhooks and detailed analytics
Base URL: https://paychat.info/api/partner
To receive callbacks (webhooks): Partner Dashboard → Webhooks tab → enter your HTTPS endpoint → click Test.
Official SDK
npm install paychat-sdkimport PAYchatClient from 'paychat-sdk';
const client = new PAYchatClient('pk_your_api_key');
const payment = await client.createPayment({
amount: 10,
description: 'Limited Edition T-Shirt'
});
console.log(payment.payment.payment_url);SDK v2.0.1 — https://www.npmjs.com/package/paychat-sdk
Authentication
Your Credentials
| Key | Format | Purpose |
|---|---|---|
| API Key | pk_xxxx... | Authenticate API requests |
| Secret Key | sk_xxxx... | Verify webhook signatures |
The Secret Key is never sent in requests — it only verifies webhook signatures.
How to Authenticate
x-api-key: pk_your_api_key_hereThis is not a Bearer token.
Example Request
curl https://paychat.info/api/partner/balance \
-H "x-api-key: pk_abc123..."Access Conditions
| Condition | Response |
|---|---|
| Header missing or key unknown | 401 INVALID_API_KEY |
| Account disabled | 403 PARTNER_INACTIVE |
| Free plan, balance below minimum | 403 BALANCE_TOO_LOW |
| Free plan, over 1,000 calls this month | 429 MONTHLY_LIMIT_EXCEEDED |
Keep Your Keys Safe
- Never expose keys in client-side code
- Don't commit keys to version control
- Use environment variables
- Regenerate immediately if compromised
Part 2: Plans & Pricing
Plans
| Plan | Monthly calls | Rate multiplier |
|---|---|---|
| Free | 1,000 | ×1 |
| Founding Member | Unlimited | ×5 |
Pricing
Fees are charged per API call, not as a percentage of transaction volume, and are deducted from your balance. A volume discount applies based on your balance plus the combined balance of users you referred.
→ Full pricing, discount tiers, and worked examples
For your live rate and current discount tier, call GET /api/partner/fee-info. For plan details, GET /api/partner/plans.
Volume Discount (Based on Total Holdings)
Total Holdings = Your Balance + Referred Users' Balances
| Total Holdings (USDT) | Discount | Total Holdings (USDT) | Discount |
|---|---|---|---|
| 0 – 100 | 0% | 20,000 – 100,000 | 60% |
| 100 – 200 | 10% | 100,000 – 500,000 | 70% |
| 200 – 500 | 20% | 500,000 – 1,000,000 | 80% |
| 500 – 1,500 | 30% | 1,000,000 – 2,000,000 | 90% |
| 1,500 – 5,000 | 40% | 2,000,000 + | 100% |
| 5,000 – 20,000 | 50% |
Live figures: GET /fee-info and GET /plans.
How It Works
- Pay once → lifetime API access
- API fees deducted from your balance per call
- More holdings = higher discount = lower fees
- Refer users → their balances count toward your discount
Part 3: Rate Limits & Spend Limits
Rate Limits
Limits are per endpoint. All GET endpoints share a single counter.
| Endpoint | Free | Founding Member |
|---|---|---|
POST /payments | 100/min | 500/min |
POST /withdrawals | 50/min | 250/min |
All GET requests | 300/min | 1,500/min |
Response Headers
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Max requests per window |
X-RateLimit-Remaining | Requests left |
X-RateLimit-Reset | Seconds until reset |
When You Hit the Limit
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests. Please retry later."
}
}Tip: check the Retry-After header to know when to retry.
Spend Limits
Optional per-partner caps. A value of 0 means unlimited.
| Control | Error code | Status |
|---|---|---|
| Per-transaction cap | TX_LIMIT_EXCEEDED | 403 |
| Daily cap | DAILY_LIMIT_EXCEEDED | 403 |
The daily counter resets on the UTC date change. The amount is reserved at check time — if the withdrawal later fails, the reserved amount stays counted until the next reset.
Idempotency
Pass idempotency_key in the request body — not as a header.
Supported on POST /payments, POST /withdrawals, POST /escrows, POST /qr-payments.
| Behaviour | Detail |
|---|---|
| Retention | 24 hours |
| Replay | Returns the original stored response |
| Withdrawals | Key is claimed before funds move. Concurrent retry gets REQUEST_IN_PROGRESS. If the transfer fails, the claim is released so you can retry. |
Part 4: API Endpoints Overview
All Endpoints
Payments
| Method | Endpoint | Description |
|---|---|---|
POST | /payments | Create a payment link |
GET | /payments/:link_id | Check payment status |
QR Payments
| Method | Endpoint | Description |
|---|---|---|
POST | /qr-payments | Create a QR payment |
GET | /qr-payments | List QR payments |
GET | /qr-payments/:id | Check QR status |
DELETE | /qr-payments/:id | Deactivate a QR payment |
Escrow
| Method | Endpoint | Description |
|---|---|---|
POST | /escrows | Create an escrow |
GET | /escrows | List escrows |
GET | /escrows/:id | Check escrow status |
POST | /escrows/:id/release | Release funds to seller |
POST | /escrows/:id/cancel | Cancel and refund buyer |
Withdrawals & Deposits
| Method | Endpoint | Description |
|---|---|---|
POST | /withdrawals | Send USDT to a user |
POST | /deposits | Create a deposit request |
GET | /deposits | List deposits |
GET | /deposits/:id | Check deposit status |
DELETE | /deposits/:id | Cancel a pending deposit |
GET | /deposit-address | Get deposit address |
Gifts, Lucky Drops & Collects
Paid from your partner balance. Detailed reference to follow.
| Method | Endpoint | Description |
|---|---|---|
POST | /gifts | Create a gift for a user |
GET | /gifts/:id | Check gift status |
POST | /luckydrops | Create a lucky drop |
GET | /luckydrops/:id | Check lucky drop status |
POST | /collects | Create a collect request |
GET | /collects/:id | Check collect status |
M-Pesa
| Method | Endpoint | Description |
|---|---|---|
GET | /mpesa/transactions | List M-Pesa transactions |
GET | /mpesa/transactions/:id | Check an M-Pesa transaction |
Balance & Reporting
| Method | Endpoint | Description |
|---|---|---|
GET | /balance | Check your balance |
GET | /fee-info | Current fee rate and discount tier |
GET | /transactions | List transactions |
GET | /dashboard | Dashboard summary |
GET | /stats | Detailed statistics |
GET | /users/:telegram_id | Look up a user |
Webhooks
| Method | Endpoint | Description |
|---|---|---|
PUT | /webhook | Set webhook URL |
POST | /webhook-test | Send a test webhook |
GET | /webhooks | List webhook delivery logs |
POST | /webhooks/:id/retry | Retry a failed delivery |
Account
| Method | Endpoint | Description |
|---|---|---|
PUT | /settings | Update name / webhook URL |
POST | /regenerate-key | Rotate API and Secret keys |
GET | /transfer/:requestId | Check a transfer |
Partner Shop
| Method | Endpoint | Description |
|---|---|---|
GET | /shop/products | List escrow products open for sale |
GET | /shop/channels | List Telegram channels accepting ads |
POST | /shop/checkouts | Create a Mini App checkout link |
POST | /shop/carts | Sync a connected user's cart (shows in the Mini App Chat tab) |
GET | /shop/checkouts/:id | Read a checkout and its orders |
GET | /shop/orders | List your orders |
See Part 16 and Part 17.
Connect PAYchat
| Method | Endpoint | Description |
|---|---|---|
POST | /connect/requests | Ask a user to link their PAYchat account to your site |
GET | /connect/requests/:code | Poll the request; returns the user's ext_uid once approved |
See Part 16 › Connect PAYchat.
Testing (test keys only)
| Method | Endpoint | Description |
|---|---|---|
POST | /test/simulate-payment | Mark a test payment or escrow as paid and fire its webhooks |
Public Endpoints (No API Key Required)
| Method | Endpoint | Description |
|---|---|---|
GET | /plans | View available plans |
GET | /me?telegram_id= | Check partner status |
POST | /register | Register as a partner |
POST | /register-free | Register on the Free plan |
POST | /upgrade | Upgrade to Founding Member |
POST | /reset | Reset partner account |
POST | /transfer | Session-authenticated transfer |
POST | /sandbox/start | Issue a sandbox key claim link (3 per IP per day) |
GET | /sandbox/claim/:token | Claim the sandbox key from the link above |
Minimum Amounts
These differ per resource — a common source of INVALID_AMOUNT.
| Resource | Minimum |
|---|---|
| Payment link | 0.000001 USDT |
| Withdrawal | 0.000001 USDT |
| QR payment (fixed amount) | 0.01 USDT |
| Escrow | 1 USDT |
| Deposit | 1 USDT |
Part 5: Create Payment
Create a Payment
Endpoint: POST /api/partner/payments
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | ✅ | Amount in USDT (min: 0.000001) |
description | string | ❌ | What the payment is for |
metadata | object | ❌ | Custom data, stored and returned on lookup |
idempotency_key | string | ❌ | Prevent duplicate payments (24h) |
Example Request
curl -X POST https://paychat.info/api/partner/payments \
-H "x-api-key: pk_your_key" \
-H "Content-Type: application/json" \
-d '{
"amount": 29.99,
"description": "Limited Edition T-Shirt",
"metadata": {
"user_id": "user_123",
"item": "tshirt_black_L"
}
}'Example Response
{
"success": true,
"payment": {
"id": "x7k2m9",
"link_id": "x7k2m9",
"amount": 29.99,
"currency": "USDT",
"description": "Limited Edition T-Shirt",
"status": "pending",
"payment_url": "https://t.me/paychat_aibot?start=x7k2m9",
"expires_at": "2026-08-14T12:00:00.000Z",
"metadata": {
"user_id": "user_123",
"item": "tshirt_black_L"
},
"created_at": "2026-08-13T12:00:00.000Z"
}
}⚠️idis the 6-characterlink_id. Use this value forGET /payments/:link_id. Earlier SDK versions returned a separatepay_xxxidentifier that was not queryable — it has been removed.⚠️
expires_atis informational. No background job closes the link and payment is not blocked after that time. Enforce a cutoff in your own code if you need one.
What to Do Next
- Redirect your customer to
payment_url - They complete payment in Telegram
- You receive a
payment.completedwebhook - Funds are added to your balance instantly
Check Payment Status
Endpoint: GET /api/partner/payments/:link_id
curl https://paychat.info/api/partner/payments/x7k2m9 \
-H "x-api-key: pk_your_key"Example Response (Completed)
{
"success": true,
"payment": {
"id": "x7k2m9",
"link_id": "x7k2m9",
"amount": 29.99,
"currency": "USDT",
"description": "Limited Edition T-Shirt",
"metadata": { "user_id": "user_123" },
"status": "completed",
"fee": 0.0004,
"net_amount": 29.9896,
"payer": { "telegram_id": "987654321" },
"paid_at": "2026-08-13T12:30:00.000Z",
"created_at": "2026-08-13T12:00:00.000Z"
}
}fee and net_amount are 0 and full amount respectively while status is pending.
Payment Status Values
| Status | Meaning |
|---|---|
pending | Waiting for payment |
completed | Payment received |
Part 6: QR Payments
Create a QR Payment
Endpoint: POST /api/partner/qr-payments
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | conditional | Required when is_fixed_amount is true. Min 0.01 USDT |
description | string | ❌ | Payment description |
metadata | object | ❌ | Custom data, stored and returned |
is_fixed_amount | boolean | ❌ | Default true |
min_amount | number | ❌ | Used only when is_fixed_amount is false |
max_amount | number | ❌ | Used only when is_fixed_amount is false |
single_use | boolean | ❌ | Default true |
expires_hours | number | ❌ | Hours, not seconds. Stored only — status is not auto-updated. Omit for no expiry |
idempotency_key | string | ❌ | Prevent duplicates (24h) |
⚠️ expires_hours is measured in hours. If omitted, the QR code does not expire.Example Request
curl -X POST https://paychat.info/api/partner/qr-payments \
-H "x-api-key: pk_your_key" \
-H "Content-Type: application/json" \
-d '{
"amount": 15.00,
"description": "Coffee Shop Order",
"expires_hours": 1
}'Example Response
{
"success": true,
"qr_payment": {
"id": "qr_a1b2c3d4e5f6",
"amount": 15.00,
"description": "Coffee Shop Order",
"is_fixed_amount": true,
"min_amount": null,
"max_amount": null,
"single_use": true,
"status": "active",
"payment_url": "https://t.me/paychat_aibot?start=qr_a1b2c3d4e5f6",
"qr_image_url": "https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=...",
"metadata": null,
"expires_at": "2026-08-13T13:00:00.000Z",
"created_at": "2026-08-13T12:00:00.000Z"
}
}Variable Amount (Tip Jar)
curl -X POST https://paychat.info/api/partner/qr-payments \
-H "x-api-key: pk_your_key" \
-H "Content-Type: application/json" \
-d '{
"is_fixed_amount": false,
"min_amount": 1,
"max_amount": 100,
"single_use": false,
"description": "Tip Jar"
}'min_amount greater than max_amount returns INVALID_RANGE.
QR Status Values
| Status | Meaning | Set by |
|---|---|---|
active | Accepting payment | Creation, or rollback from processing |
processing | A payment is being executed | Payment flow (concurrency lock) |
paid | Payment received | Payment flow |
inactive | Deactivated | DELETE /qr-payments/:id |
⚠️ There is noexpiredstatus.expires_atis stored for your reference only — no background job transitions the record, and the payment flow does not check it. A QR code past its expiry still reportsactiveand can still be paid. Compareexpires_atyourself, or callDELETE /qr-payments/:idto close it.
processing is a short-lived lock. If the payment fails, the record returns to active.
DELETE only accepts QR codes currently in active status — otherwise QR_NOT_FOUND is returned.
{
"success": true,
"message": "QR payment deactivated",
"id": "qr_a1b2c3d4e5f6"
}Part 7: Escrow
Create an Escrow
Hold buyer funds until the seller delivers.
Endpoint: POST /api/partner/escrows
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
buyer_telegram_id | string | ✅ | Buyer's Telegram ID |
seller_telegram_id | string | ✅ | Seller's Telegram ID |
amount | number | ✅ | Min 1 USDT |
description | string | ❌ | What the escrow is for |
metadata | object | ❌ | Custom data, stored and returned |
expires_hours | number | ❌ | Default 72 |
idempotency_key | string | ❌ | Prevent duplicates (24h) |
Things to Know
| Behaviour | Detail |
|---|---|
| 1% fee | fee = amount × 0.01, deducted from the seller payout |
| Seller auto-registration | If seller_telegram_id has no PAYchat account, one is created automatically |
Example Request
curl -X POST https://paychat.info/api/partner/escrows \
-H "x-api-key: pk_your_key" \
-H "Content-Type: application/json" \
-d '{
"buyer_telegram_id": "123456789",
"seller_telegram_id": "987654321",
"amount": 100.00,
"description": "Freelance Project",
"expires_hours": 72
}'Example Response
{
"success": true,
"escrow": {
"id": "esc_a1b2c3d4e5f6",
"buyer_telegram_id": "123456789",
"seller_telegram_id": "987654321",
"amount": 100.00,
"fee": 1.00,
"net_amount": 99.00,
"description": "Freelance Project",
"metadata": null,
"status": "pending",
"payment_url": "https://t.me/paychat_aibot?start=esc_a1b2c3d4e5f6",
"expires_at": "2026-08-16T12:00:00.000Z",
"created_at": "2026-08-13T12:00:00.000Z"
}
}Response fields are flat —buyer_telegram_idandseller_telegram_id, not nestedbuyer/sellerobjects.
Escrow Lifecycle
| Status | Meaning |
|---|---|
pending | Created, waiting for buyer payment |
processing | Buyer payment being processed |
paid | Buyer funded the escrow |
releasing | Release in progress |
completed | Funds released to seller |
cancelling | Cancellation in progress |
cancelled | Cancelled, buyer refunded |
Release / Cancel
# Release funds to the seller
curl -X POST https://paychat.info/api/partner/escrows/esc_a1b2c3d4e5f6/release \
-H "x-api-key: pk_your_key"
# Cancel and refund the buyer
curl -X POST https://paychat.info/api/partner/escrows/esc_a1b2c3d4e5f6/cancel \
-H "x-api-key: pk_your_key" \
-H "Content-Type: application/json" \
-d '{"reason": "Buyer requested refund"}'An escrow already being processed returns ESCROW_PROCESSING.
Part 8: Send Withdrawal
Send a Withdrawal
Endpoint: POST /api/partner/withdrawals
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
telegram_id | string | ✅ | Recipient's Telegram ID |
amount | number | ✅ | Amount in USDT (min: 0.000001) |
memo | string | ❌ | Message shown to recipient |
idempotency_key | string | ❌ | Prevent duplicate withdrawals (24h) |
Example Request
curl -X POST https://paychat.info/api/partner/withdrawals \
-H "x-api-key: pk_your_key" \
-H "Content-Type: application/json" \
-d '{
"telegram_id": "123456789",
"amount": 10.00,
"memo": "Referral bonus - Thanks!",
"idempotency_key": "wd_order_5521"
}'Example Response
{
"success": true,
"withdrawal": {
"id": "wd_xyz789abc",
"telegram_id": "123456789",
"amount": 10.00,
"status": "completed",
"memo": "Referral bonus - Thanks!",
"created_at": "2026-08-13T12:00:00.000Z"
},
"referral_added": true
}referral_added is true only on the recipient's first transaction with you.
Good to Know
| Feature | How It Works |
|---|---|
| Instant delivery | Funds arrive immediately |
| Auto-registration | New users get a PAYchat account automatically |
| Notifications | Recipients get a Telegram message |
| Referral registration | First withdrawal registers the user as your referral |
| Self-transfer blocked | Returns CANNOT_WITHDRAW_TO_SELF |
Part 9: Deposits
Fund Your Partner Account
Endpoint: POST /api/partner/deposits
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | ✅ | Min 1 USDT |
network | string | ❌ | tron or ton. Default tron |
Any other network value returns INVALID_NETWORK. If you already have an open request, you get ACTIVE_REQUEST_EXISTS.
Example Request
curl -X POST https://paychat.info/api/partner/deposits \
-H "x-api-key: pk_your_key" \
-H "Content-Type: application/json" \
-d '{"amount": 50, "network": "tron"}'Deposit Status Values
| Status | Meaning |
|---|---|
pending | Waiting for funds |
completed | Credited to your balance |
expired | Request expired |
cancelled | Cancelled via DELETE |
Deposits do not emit webhooks. Poll GET /deposits/:id instead.
Get Deposit Address
Endpoint: GET /api/partner/deposit-address
Example Response
{
"success": true,
"deposit": {
"address": "TXxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"network": "TRON (TRC20)",
"currency": "USDT",
"memo": "partner_abc123",
"note": "Include the memo when sending to identify your deposit"
}
}This endpoint returns the TRON (TRC-20) address only. To deposit over TON, create a deposit request withPOST /depositsand"network": "ton".
Important: always include the memo when sending funds so we can credit your account.
Part 10: Balance, Transactions & Stats
Check Your Balance
Endpoint: GET /api/partner/balance
{
"success": true,
"balance": {
"available": 1250.50,
"currency": "USDT",
"monthly_volume": 48200.00,
"fee_rate": 0.01
}
}| Field | Description |
|---|---|
available | USDT you can withdraw or send |
monthly_volume | Total volume processed this month |
fee_rate | Your configured overage fee rate |
Total holdings and referral balances are not returned here. Use GET /fee-info for your discount tier and referral totals.
Fee Info
Endpoint: GET /api/partner/fee-info
Returns your current discount tier, effective per-call fee, total holdings, and how much more balance is needed to reach the next tier.
List Transactions
Endpoint: GET /api/partner/transactions
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
type | string | all | payment, withdraw, escrow, or qr |
status | string | all | Filter by status |
from | string | - | Start date (ISO 8601) |
to | string | - | End date (ISO 8601) |
limit | number | 20 | Results per page (max 100) |
offset | number | 0 | Pagination offset |
Example Response
{
"success": true,
"transactions": [
{
"id": 1,
"partner_id": "ptn_abc123",
"type": "payment",
"telegram_id": "123456789",
"amount": 29.99,
"fee": 0.0004,
"net_amount": 29.9896,
"status": "completed",
"created_at": "2026-08-13T12:00:00.000Z"
}
],
"pagination": {
"total": 156,
"limit": 10,
"offset": 0
}
}
Dashboard & Stats
| Endpoint | Returns |
|---|---|
GET /dashboard | Partner info, today/week totals, webhook delivery counts, recent transactions |
GET /stats?period=30d | Daily breakdown and totals. period accepts 7d, 30d, 90d |
GET /escrows,GET /qr-payments, andGET /depositsreturn a subset of fields and nopaginationobject. Use the detail endpoint for the full record.
Part 11: Webhooks
Webhooks
Available Events
| Event | Description | When It Fires |
|---|---|---|
payment.completed | Payment received | Customer completes payment link |
withdraw.completed | Withdrawal sent | Funds delivered to user |
qr.paid | QR payment received | Customer pays via QR code |
escrow.created | Escrow created | New escrow order created |
escrow.paid | Escrow funded | Buyer completes escrow payment |
escrow.released | Escrow released | Funds released to seller |
escrow.cancelled | Escrow cancelled | Cancelled (refunded if paid) |
test | Test event | You send a test webhook |
Partner Shop Events
| Event | Description | When It Fires |
|---|---|---|
shop.order.paid | Shop deal funded | Buyer pays for a shop item or ad run |
shop.order.ad_posted | Ad posted | PAYchat bot posts a channel-ad run |
shop.order.ad_removed | Ad removed | The channel owner took the post down during the 24 h guarantee |
shop.order.disputed | Shop deal disputed | Buyer opens a dispute |
shop.order.cancelled | Shop deal cancelled | Deal cancelled and buyer refunded |
shop.order.completed | Shop deal completed | Deal completes; carries your commission |
Payloads and rules are in Part 16 › Order Status Webhooks. Shop events carry click_id instead of metadata.
There are noqr.created,qr.expired, or deposit events. Poll the relevantGETendpoint instead.
Setting Up Webhooks
1. Set your webhook URL
curl -X PUT https://paychat.info/api/partner/webhook \
-H "x-api-key: pk_your_key" \
-H "Content-Type: application/json" \
-d '{"webhook_url": "https://your-server.com/webhooks/paychat"}'2. Test it
curl -X POST https://paychat.info/api/partner/webhook-test \
-H "x-api-key: pk_your_key"
Webhook URL Requirements
The URL must be a public HTTPS endpoint. The following are rejected with INVALID_WEBHOOK_URL:
- Any
http://URL localhostor any.localhostname- Private ranges:
10.x,172.16–31.x,192.168.x - Loopback:
127.x,::1 - Link-local:
169.254.x(including cloud metadata endpoints) - CGNAT:
100.64–127.x - Multicast and reserved ranges
- Bare hostnames with no dot, and integer or hex-encoded IP forms
Webhook Headers
| Header | Description |
|---|---|
Content-Type | application/json |
X-PAYchat-Event | Event type |
X-PAYchat-Signature | HMAC signature for verification |
X-PAYchat-Delivery-ID | Unique delivery ID |
Webhook Payload: payment.completed
{
"event": "payment.completed",
"data": {
"payment_id": "x7k2m9",
"status": "completed",
"transaction_id": "x7k2m9",
"amount": 29.99,
"fee": 0.0004,
"net_amount": 29.9896,
"metadata": {
"user_id": "user_123",
"item": "tshirt_black_L"
},
"payer": { "telegram_id": "987654321" }
},
"created_at": "2026-08-13T12:00:00.000Z"
}Whatever you attached asmetadataon creation is echoed back here — no follow-upGETneeded. This applies topayment.completed,qr.paid,escrow.created,escrow.paid,escrow.released, andescrow.cancelled.withdraw.completedcarriesmemoinstead.
Webhook Payload: withdraw.completed
{
"event": "withdraw.completed",
"data": {
"withdrawal_id": "wd_xyz789",
"telegram_id": "123456789",
"amount": 10.00,
"memo": "Referral bonus"
},
"created_at": "2026-08-13T12:00:00.000Z"
}Part 12: Webhook Security & Retries
Verifying Webhooks
How Signatures Work
- We build the message
{timestamp}.{JSON.stringify(payload)} - We sign it with your Secret Key using HMAC-SHA256
- We send the result in
X-PAYchat-Signature
Signature Format
X-PAYchat-Signature: t=1704628800,v1=abc123def456...| Part | Description |
|---|---|
t | Unix timestamp (seconds) |
v1 | HMAC-SHA256 signature (hex) |
Reject requests whose timestamp is older than 5 minutes.
Verification Code (Node.js)
const crypto = require('crypto');
function verifyWebhook(payload, signatureHeader, secretKey) {
const parts = signatureHeader.split(',');
const timestamp = parts.find(p => p.startsWith('t=')).split('=')[1];
const signature = parts.find(p => p.startsWith('v1=')).split('=')[1];
// Reject stale timestamps
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) return false;
const message = `${timestamp}.${JSON.stringify(payload)}`;
const expected = crypto
.createHmac('sha256', secretKey)
.update(message)
.digest('hex');
return signature === expected;
}
app.post('/webhooks/paychat', (req, res) => {
const isValid = verifyWebhook(
req.body,
req.headers['x-paychat-signature'],
process.env.PAYCHAT_SECRET_KEY
);
if (!isValid) return res.status(401).send('Invalid signature');
res.status(200).send('OK');
});Or use the SDK:
import { PAYchatClient } from 'paychat-sdk';
// Node.js
PAYchatClient.verifyWebhook(payload, signature, 'sk_your_secret_key');
// Browser / Workers
await PAYchatClient.verifyWebhookAsync(payload, signature, 'sk_your_secret_key');
Webhook Retries
If your server doesn't respond with a 2xx status, we retry automatically.
Retry Schedule
| Attempt | Wait Time |
|---|---|
| 1st | Immediate |
| 2nd | 1 minute |
| 3rd | 5 minutes |
| 4th | 30 minutes |
| 5th | 2 hours |
| Final | Marked as failed |
Retries run on a background scheduler. A delivery is abandoned after 5 total attempts and marked failed.
Failed deliveries can be replayed manually with POST /webhooks/:id/retry.
Requirements for Success
| Requirement | Details |
|---|---|
| Status code | Any 2xx (200–299) |
| Timeout | Respond within 10 seconds |
| Response body | Not required |
Part 13: Error Handling
Error Handling
Error Response Format
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human readable description"
}
}Some errors add context fields — for example DAILY_LIMIT_EXCEEDED also returns daily_limit, daily_used, and amount.
HTTP Status Codes
| Code | Meaning |
|---|---|
200 | Success |
400 | Bad request (check your input) |
401 | Authentication failed |
403 | Forbidden (inactive, or limit exceeded) |
404 | Resource not found |
409 | Conflict (item no longer available) |
429 | Rate or monthly limit exceeded |
500 | Server error (retry later) |
503 | Partner API temporarily paused. Retry later |
Authentication & Access
| Code | Meaning |
|---|---|
INVALID_API_KEY | Missing or invalid API key |
PARTNER_INACTIVE | Partner account is inactive |
BALANCE_TOO_LOW | Balance below the minimum for API access |
SANDBOX_EXPIRED | Sandbox key has expired |
UNAUTHORIZED | Not authorised for this resource |
FORBIDDEN | Action not permitted |
NOT_PARTNER | Caller is not a partner |
ALREADY_PARTNER | Already registered |
ALREADY_UPGRADED | Plan already upgraded |
ADMIN_NOT_FOUND | Admin account not found |
Limits
| Code | Meaning |
|---|---|
RATE_LIMIT_EXCEEDED | Too many requests |
MONTHLY_LIMIT_EXCEEDED | Free plan monthly call limit reached |
TX_LIMIT_EXCEEDED | Per-transaction cap exceeded |
DAILY_LIMIT_EXCEEDED | Daily cap exceeded |
TEMPORARILY_BLOCKED | Rate limit exceeded: API access blocked for 10 minutes. Three of these within 24 hours deactivate the partner account |
Validation
| Code | Meaning |
|---|---|
INVALID_AMOUNT | Amount below minimum or missing |
INVALID_PARAMS | Required parameters missing |
INVALID_RANGE | min_amount greater than max_amount |
INVALID_NETWORK | Network must be tron or ton |
INVALID_URL | Malformed URL |
INVALID_WEBHOOK_URL | Webhook URL must be a public HTTPS endpoint |
SAME_USER | Buyer and seller are identical |
CANNOT_WITHDRAW_TO_SELF | Cannot withdraw to yourself |
NO_UPDATES | No fields to update |
INVALID_ITEMS | Shop checkout items missing or outside 1–50 |
INVALID_CLICK_ID | Shop checkout click_id format is invalid |
INVALID_ITEM_TYPE | Shop item type must be product or channel_ad |
INVALID_SLOT | Channel ad slot_at is not asap or an ISO time between 24 hours and 60 days from now |
INVALID_REPEAT | repeat_count / repeat_freq not in the allowed set |
INVALID_IMAGE | Shop ad image_url rejected (URL, host, download, format or size). message gives the reason |
INVALID_AD_TEXT | Shop ad ad_text over 1,024 characters while image_url is set |
AD_LINK | Shop ad ad_text contains a link or email — use button_url |
INVALID_BUTTON_URL | Shop ad button_url is not an http(s) URL, a domain or an @username |
INVALID_EXT_UID | Cart sync ext_uid missing or malformed |
BAD_REDIRECT | Connect redirect_uri must be a public HTTPS URL |
Resource State
| Code | Meaning |
|---|---|
PAYMENT_NOT_FOUND | Payment not found |
QR_NOT_FOUND | QR payment not found |
ESCROW_NOT_FOUND | Escrow not found |
DEPOSIT_NOT_FOUND | Deposit not found |
WEBHOOK_NOT_FOUND | Webhook log not found |
USER_NOT_FOUND | User not found |
BUYER_NOT_FOUND | Buyer not found |
SELLER_NOT_FOUND | Seller not found |
NO_WEBHOOK_URL | Webhook URL not configured |
ACTIVE_REQUEST_EXISTS | An active deposit request already exists |
ESCROW_PROCESSING | Escrow is currently being processed |
PRODUCT_UNAVAILABLE | Shop product is no longer open for sale |
CHANNEL_UNAVAILABLE | Channel is not accepting ads |
CHECKOUT_NOT_FOUND | Shop checkout not found |
NOT_FOUND | Connect request code unknown |
USER_NOT_CONNECTED | This ext_uid has not connected PAYchat to your account |
Execution
| Code | Meaning |
|---|---|
INSUFFICIENT_BALANCE | Not enough balance |
REQUEST_IN_PROGRESS | Duplicate idempotency key still processing |
RELEASE_FAILED | Escrow release failed |
CANCEL_FAILED | Escrow cancellation failed |
INTERNAL_ERROR | Unexpected server error |
Client-Side (SDK only)
| Code | Meaning |
|---|---|
TIMEOUT | Request exceeded the SDK timeout. Never returned by the server |
Part 14: Code Examples
Using the Official SDK (Recommended)
npm install paychat-sdkimport PAYchatClient from 'paychat-sdk';
const client = new PAYchatClient('pk_your_api_key');
// Payment link
const payment = await client.createPayment({
amount: 29.99,
description: 'Limited Edition Hoodie',
metadata: { user_id: '123' }
});
// QR payment — expiresHours is in HOURS
const qr = await client.createQRPayment({
amount: 15.00,
description: 'Coffee Shop Order',
expiresHours: 1
});
console.log(qr.qr_payment.qr_image_url);
// Escrow — minimum 1 USDT, 1% fee
const escrow = await client.createEscrow({
amount: 100,
buyerTelegramId: '123456789',
sellerTelegramId: '987654321',
description: 'Freelance Project'
});
await client.releaseEscrow(escrow.escrow.id);
// Withdrawal
const withdrawal = await client.createWithdrawal({
telegramId: '123456789',
amount: 5.00,
memo: 'Referral bonus'
});
// Balance
const balance = await client.getBalance();
// Webhook verification
const isValid = PAYchatClient.verifyWebhook(payload, signature, 'sk_your_secret_key');
Raw HTTP (any language)
Node.js Setup
const axios = require('axios');
const paychat = axios.create({
baseURL: 'https://paychat.info/api/partner',
headers: {
'x-api-key': process.env.PAYCHAT_API_KEY,
'Content-Type': 'application/json'
}
});Create a Payment
async function createPayment(amount, description, metadata = {}) {
try {
const { data } = await paychat.post('/payments', {
amount,
description,
metadata,
idempotency_key: `pay_${Date.now()}_${Math.random()}`
});
console.log('Payment URL:', data.payment.payment_url);
console.log('Lookup ID:', data.payment.id); // 6-char link_id
return data.payment;
} catch (error) {
console.error('Error:', error.response?.data?.error?.message);
throw error;
}
}Send Money to a User
async function sendReward(telegramId, amount, memo) {
try {
const { data } = await paychat.post('/withdrawals', {
telegram_id: telegramId,
amount,
memo,
idempotency_key: `wd_${telegramId}_${Date.now()}`
});
return data.withdrawal;
} catch (error) {
const code = error.response?.data?.error?.code;
if (code === 'INSUFFICIENT_BALANCE') {
console.error('Not enough balance! Please deposit more funds.');
}
if (code === 'DAILY_LIMIT_EXCEEDED') {
console.error('Daily spend cap reached.');
}
throw error;
}
}Handle Webhooks (Express)
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
app.post('/webhooks/paychat', (req, res) => {
const signature = req.headers['x-paychat-signature'];
if (!verifySignature(req.body, signature)) {
return res.status(401).send('Invalid signature');
}
switch (req.headers['x-paychat-event']) {
case 'payment.completed': handlePayment(req.body.data); break;
case 'withdraw.completed': handleWithdrawal(req.body.data); break;
case 'qr.paid': handleQR(req.body.data); break;
case 'escrow.paid': handleEscrowPaid(req.body.data); break;
case 'escrow.released': handleEscrowReleased(req.body.data); break;
}
res.status(200).send('OK');
});
function verifySignature(payload, header) {
const parts = header.split(',');
const timestamp = parts.find(p => p.startsWith('t=')).split('=')[1];
const signature = parts.find(p => p.startsWith('v1=')).split('=')[1];
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) return false;
const message = `${timestamp}.${JSON.stringify(payload)}`;
const expected = crypto
.createHmac('sha256', process.env.PAYCHAT_SECRET_KEY)
.update(message)
.digest('hex');
return signature === expected;
}
app.listen(3000);Part 15: FAQ
Frequently Asked Questions
General
Q: What if my balance is too low?
A: Auto-charge triggers if enabled. Otherwise the API returns BALANCE_TOO_LOW (Free plan) or INSUFFICIENT_BALANCE.
Q: How do referred users affect my discount?
A: The first withdrawal to a user registers them as your referral. Their balance counts toward your total holdings, increasing your discount rate.
Q: What if my API key is exposed?
A: Regenerate it immediately:
curl -X POST https://paychat.info/api/partner/regenerate-key \
-H "x-api-key: pk_your_current_key"Your old key becomes invalid instantly. The Secret Key is rotated at the same time — update your webhook verification code too.
Payments
Q: Which ID do I use to check a payment?
A: The payment.id from the create response — a 6-character link_id. It is also returned as payment.link_id.
Q: Is metadata sent in webhooks?
A: Yes. Whatever you attached on creation is echoed in payment.completed, qr.paid, and every escrow.* event (see Part 11). withdraw.completed carries memo instead, and Partner Shop events carry click_id instead of metadata.
Q: What currencies are supported?
A: USDT. Deposits accept the TRON (TRC-20) and TON networks.
Q: When do I receive funds?
A: Instantly — as soon as the customer completes payment.
QR Payments
Q: Why doesn't my QR code expire?
A: expires_hours is measured in hours, and if omitted the QR never expires. Even when set, expires_at is informational — the status is not auto-updated and the payment flow does not block on it. Enforce expiry in your own code, or call DELETE /qr-payments/:id.
Q: What's the minimum QR amount?
A: 0.01 USDT for fixed-amount QR codes — higher than the 0.000001 minimum for payment links.
Escrow
Q: What does escrow cost?
A: 1% of the amount, deducted from the seller payout. A 100 USDT escrow pays the seller 99 USDT.
Q: What if the seller has no PAYchat account?
A: An account is created automatically when the escrow is made.
Withdrawals
Q: What if the recipient doesn't have PAYchat?
A: We create an account automatically. They receive a Telegram message with instructions.
Q: Is there a minimum withdrawal?
A: 0.000001 USDT.
Webhooks
Q: What if my webhook endpoint is down?
A: We retry up to 5 times over roughly 2.5 hours. You can also replay manually with POST /webhooks/:id/retry.
Q: Why was my webhook URL rejected?
A: It must be a public HTTPS endpoint. See the Webhook URL Requirements table — private, loopback, and link-local addresses are blocked.
Security
Q: What's idempotency_key for?
A: It prevents duplicate transactions if the same request is sent twice. Send it in the request body, not as a header. Keys are retained for 24 hours.
Q: How do I verify webhooks are really from PAYchat?
A: Check X-PAYchat-Signature with your Secret Key, and reject timestamps older than 5 minutes.
Need Help?
| Channel | Contact |
|---|---|
| Telegram | @paychat_mafia |
| [email protected] |
PAYchat Partner API — Documentation
Payment system for partners and AI agents. Base URL: https://paychat.info/api/partner
Quick start (no signup, works for AI agents)
POST https://paychat.info/api/partner/sandbox/start— no auth. Returns JSON with aclaim_url. Limit: 3 issues per IP per day.GETtheclaim_url— returns a sandboxapi_key(prefixpk_test_, valid 30 days, 1,000 calls/month) plus ready-to-run curl examples.- Call any endpoint with header
X-API-Key: <api_key>.
Sandbox keys run in test mode: no real funds move, and payments are completed via the simulator below. Live keys (pk_) require a PAYchat account — register as a partner in the Telegram Mini App.
Authentication
Every request needs the header X-API-Key. Keys starting with pk_test_ are test/sandbox; keys starting with pk_ are live. A missing or wrong key returns 401 INVALID_API_KEY with a hint field pointing back to the sandbox start endpoint. Expired sandbox keys return SANDBOX_EXPIRED.
Idempotency
Write endpoints accept an optional idempotency_key in the body. Repeating a request with the same key within 24 hours returns the original response instead of creating a duplicate.
Payments
POST /payments — create a payment link.
Body: amount (USDT, required), description, metadata, idempotency_key.
The returned id is a payment link id; the link expires in 24 hours. Test-mode payments are marked api_mode: "test".
GET /payments/:id — fetch one payment.
Escrow
POST /escrows — create an escrow between two Telegram users.
Body: buyer_telegram_id (required), seller_telegram_id (required), amount (required, minimum 1 USDT), description, metadata, idempotency_key, expires_hours (default 72).
Errors: INVALID_PARAMS (missing required fields), INVALID_AMOUNT (below 1 USDT).
GET /escrows — list escrows. GET /escrows/:id — fetch one. POST /escrows/:id/release — release funds to the seller. POST /escrows/:id/cancel — cancel and refund.
Withdrawals
POST /withdrawals — pay out to a PAYchat user.
Body: telegram_id, amount, memo, idempotency_key.
QR payments
POST /qr-payments — create a QR payment.
Body: amount, description, metadata, idempotency_key, is_fixed_amount (default true), min_amount, max_amount (for variable-amount QR), single_use (default true), expires_hours.
GET /qr-payments, GET /qr-payments/:id, DELETE /qr-payments/:id.
Test simulator (test mode only)
POST /test/simulate-payment — mark a test payment or escrow as paid, firing the same webhooks as a real payment.
Body: type ("payment" or "escrow"), id (the object id).
Webhooks
Set your webhook URL via PUT /webhook (HTTPS only). Delivery log: GET /webhooks; retry: POST /webhooks/:id/retry; send a test event: POST /webhook-test.
Events: payment.completed, escrow.created, escrow.paid, escrow.released, escrow.cancelled.
Account & info
GET /balance — partner balance. GET /fee-info — per-call costs and volume discounts. GET /dashboard, GET /stats, GET /transactions — activity overviews. GET /plans — plans and pricing (no auth needed).
Not for autonomous agents
POST /transfer moves funds between users and requires the end user's PIN session inside the PAYchat Mini App. AI agents cannot call it standalone; use payments or escrow instead.
Also available
Read-only user-side OpenAPI (MCP tools): https://paychat.info/mcp/openapi.json. MCP API keys are issued by users inside the PAYchat Mini App.
Questions → https://t.me/paychat_group
Start → https://t.me/paychat_aibot?start=partner
Part 16: Partner Shop
Partner Shop
Build your own storefront from PAYchat escrow products and Telegram channel ads. You design the catalog and the cart. Payment always happens inside the PAYchat Mini App. Every deal from your shop is tracked with orders and webhooks, and you earn a commission on deals from buyers who joined PAYchat through your shop (see Attribution & Commission).
How It Works
- Load the catalog with
GET /shop/productsandGET /shop/channels - Show the items in your own shop and cart
- Create a checkout with
POST /shop/checkouts(or keep a connected user's cart in sync withPOST /shop/carts) - Send the buyer to the checkout
url— it opens the PAYchat Mini App - The buyer pays inside the Mini App
- Follow each deal with the order endpoints and webhooks. Each completed deal sends you
shop.order.completedwith yourcommission
Shop Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /shop/products | List escrow products open for sale |
GET | /shop/channels | List Telegram channels accepting ads |
POST | /shop/checkouts | Create a Mini App checkout link |
POST | /shop/carts | Sync the cart of a user who connected PAYchat to your site |
GET | /shop/checkouts/:id | Read a checkout and its orders |
GET | /shop/orders | List your orders |
All use x-api-key and the same access conditions as other endpoints (see Part 1). Errors use the standard format (see Part 13).
List Products
Endpoint: GET /api/partner/shop/products
A product is listed only while it is open for sale — the same rule as the PAYchat Mini App feed: publicly posted for sale, waiting for a buyer, not expired, and in stock. Adult products are never listed.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | 50 | 1–100 |
offset | number | 0 | Pagination offset |
category | string | all | digital, gaming, service, or other |
country | string | all | 2-letter uppercase country code, e.g. KE |
Products come in the same order as the PAYchat Mini App feed (sales and saves rank higher). An unrecognized category or country value is ignored and no filter is applied.
Example Request
curl "https://paychat.info/api/partner/shop/products?category=digital&limit=20" \
-H "x-api-key: pk_your_key"Product Fields
| Field | Description |
|---|---|
id | Product ID — use this as id in a checkout item |
post_id | Feed post ID |
canonical_url | Matching public page on paychat.info (https://paychat.info/escrow/{id}). Optional: use it as <link rel="canonical"> if you want search engines to treat the PAYchat page as the original. Ignore it if you build your own SEO |
title | Product title |
description | Product description (empty string if none) |
category | Category, or null |
price | Price in USDT |
currency | Always USDT |
images | Array of absolute image URLs (empty array if none) |
stock | Remaining quantity. null means unlimited |
inspection_days | Inspection period in days, or null |
share_fee_rate | Share fee rate set by the seller — the source of your commission |
country | Country code, or null |
views | View count |
paid_amount | USDT already paid on this product |
saved_count | How many users saved this product |
seller | Seller profile (see Person Object) |
created_at | Listing time |
The response also returns limit and offset. There is no total count.
Person Object
Used for seller in products and owner in channels. Sellers and channel owners are always masked, the same way the PAYchat Mini App shows them.
| Field | Value |
|---|---|
name | Masked name: @ab****** (first 2 characters of the username or name), or #1234 Trader (last 4 digits of the Telegram ID) when neither exists |
telegram_id | Always null |
username | Always null |
photo_url | Always null — use a placeholder image |
masked | Always true |
Show name exactly as returned and do not try to reveal the seller.
List Channels
Endpoint: GET /api/partner/shop/channels
A channel is listed while it is accepting ads and has a price. Adult, gambling, hacking and similar channels are never listed.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | 50 | 1–100 |
offset | number | 0 | Pagination offset |
sort | string | rec | rec (Mini App ranking), votes, cheap, or views |
category | string | all | crypto, tech, news, biz, ent, edu, life, or other |
country | string | all | Country code. Omit for all channels |
lang | string | all | Channel language code |
q | string | — | Search text, up to 100 characters |
Channels come in the same order as the PAYchat Mini App channel list for the same filters. A channel the owner has paused is not listed until they resume.
Top-level Fields
| Field | Description |
|---|---|
total | Number of channels matching your filters |
category | The category filter applied, or null |
categories | Channel count per category, e.g. { "crypto": 120, "news": 45 } |
slot_gap_hours | Legacy field, always 0. Ignore it |
Channel Fields
| Field | Description |
|---|---|
id | Channel ID — use this as channel_id in a checkout item |
canonical_url | Matching public page on paychat.info (https://paychat.info/ads/{id}-{slug}). The slug follows the channel title and can change, so read it from the API each time. Optional, same as products |
title | Channel title (falls back to the username) |
username | Telegram username, or null |
url | https://t.me/{username}, or null |
members | Subscriber count |
avg_views | Average views per post |
er_percent | avg_views ÷ members × 100, one decimal. 0 if either value is missing |
price | Price per run in USDT |
prices | Legacy per-format price map. Every ad is now a 24 h run, charged at the first price the channel lists (24 if offered), otherwise price |
currency | Always USDT |
share_fee_rate | Share fee rate for this channel |
country | Country code, or null |
category | One of the 8 category keys, or null |
busy_slots | Legacy field, always an empty array. Ignore it — a channel accepts any number of ads at any time |
owner | Channel owner profile (see Person Object). null for channels PAYchat is still bringing on board |
created_at | Registration time |
members and avg_views refresh about every 6 hours. Cache accordingly.
Create a Checkout
Endpoint: POST /api/partner/shop/checkouts
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
items | array | ✅ | 1–50 items |
click_id | string | ❌ | Your tracking ID, e.g. an ad click ID. 1–128 chars: letters, digits, _ . -. Returned in every order and webhook |
Item: product
| Field | Required | Description |
|---|---|---|
type | ✅ | "product" |
id | ✅ | Product id from GET /shop/products |
Item: channel_ad
| Field | Required | Description |
|---|---|---|
type | ✅ | "channel_ad" |
channel_id | ✅ | Channel id from GET /shop/channels |
slot_at | ❌ | "asap" (default — posted right after the owner accepts; the owner has 24 h to answer) or an ISO 8601 time between 24 hours and 60 days from now. Time of the first run |
ad_text | ❌ | Ad text. No links or emails — @usernames are fine; put your link in button_url. Max 1,024 characters when image_url is set. The buyer can edit it in the Mini App before paying (5–4,096 characters). Shown to the channel owner before they accept — they can decline it |
button_url | ❌ | One link button under the post. https://… URL, a bare domain (example.com) or @username, max 300 characters |
button_text | ❌ | Button label, up to 30 characters. Default Learn more |
image_url | ❌ | One photo for the ad. Public HTTPS URL to a JPEG, PNG or WebP file, max 5 MB. Downloaded once when the checkout is created — see Ad Photo |
repeat_count | ❌ | Number of runs: 1, 3, 7 or 14. Default 1 |
repeat_freq | ❌ | Days between runs: 1, 2, 3 or 7. Default 1 |
Ad Text Formatting
| Syntax | Result | Limit |
|---|---|---|
**text** | Bold | 1–300 characters, one line |
__text__ | Italic | 1–300 characters, one line |
Everything else is posted as plain text. HTML tags are not interpreted. If Telegram rejects the formatted text, the ad is posted as plain text with the markers visible. Links are not allowed in the text — use button_url. Videos are not supported.
Ad Photo
| Rule | Detail |
|---|---|
| Protocol | https only, URL up to 1,000 characters |
| Host | Must resolve to a public address. Private, loopback and internal hosts are rejected |
| Redirects | Not followed — give the final image URL |
| Timeout | 10 seconds |
| Format | JPEG, PNG or WebP, checked by file content, not by extension |
| Size | 5 MB or smaller |
| When | Downloaded once at checkout creation. Changing the file at the URL later has no effect |
| Caption | With a photo, ad_text is posted as the photo caption, so it is limited to 1,024 characters |
Rules
| Rule | Detail |
|---|---|
| All or nothing | One invalid item rejects the whole checkout |
| No slot blocking | Any number of ads can be booked in the same channel at any time |
| Price | Channel price × repeat_count, taken at payment time. The checkout does not lock the price |
| Run times | Run n is posted at the first run time + (n − 1) × repeat_freq days. Each run is a separate post and stays up — PAYchat never removes it |
| Guarantee | 24 h from posting. If the owner removes the post inside that window, the advertiser is refunded pro-rata (see Part 17) |
| One payment | The advertiser pays the whole run set once in the Mini App |
Example Request
curl -X POST https://paychat.info/api/partner/shop/checkouts \
-H "x-api-key: pk_your_key" \
-H "Content-Type: application/json" \
-d '{
"items": [
{ "type": "product", "id": "PRODUCT_ID" },
{ "type": "channel_ad", "channel_id": 12, "slot_at": "asap", "ad_text": "Try PAYchat", "button_url": "paychat.info", "button_text": "Open", "repeat_count": 7, "repeat_freq": 1 }
],
"click_id": "gclid_abc123"
}'Example Response
{
"success": true,
"checkout": {
"id": "shop_1a2b3c4d5e6f7a8b9c0d",
"startapp": "shop_1a2b3c4d5e6f7a8b9c0d",
"url": "https://t.me/paychat_aibot?startapp=shop_1a2b3c4d5e6f7a8b9c0d
"app_url": null,
"items": [
{ "type": "product", "id": "PRODUCT_ID" },
{ "type": "channel_ad", "channel_id": 12, "slot_at": "2026-11-01T09:05:00.000Z", "asap": 1, "ad_text": "Try PAYchat", "repeat_count": 7, "repeat_freq": 1, "ad_image": null, "format": "24", "btn_url": "https://paychat.info", "btn_text": "Open" }
],
"click_id": "gclid_abc123",
"mode": "live",
"expires_at": "2026-11-08T09:00:00.000Z"
},
"note": "Conversion webhook event shop.order.completed is sent to your webhook_url when each deal completes."
}| Field | Description |
|---|---|
id | Checkout ID (shop_ + 20 hex characters) |
startapp | Same value — the Mini App start parameter |
url | Send the buyer here. It opens the PAYchat Mini App on the shop cart |
app_url | Only when the checkout has exactly one channel_ad item: opens that channel's ad screen in the Mini App, where the advertiser writes and pays the ad. Otherwise null |
items | Validated items. For asap, slot_at is set to 5 minutes from now and asap is 1. repeat_count / repeat_freq are filled with defaults; button_url is returned normalized as btn_url |
click_id | The click_id you sent, or null |
mode | live or test, depending on the key used |
expires_at | 7 days after creation |
⚠️ A checkout does not reserve stock or lock a price. Availability is checked only when the checkout is created, so a product can still sell out before your buyer pays.
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | INVALID_ITEMS | items missing, not an array, or outside 1–50 |
| 400 | INVALID_CLICK_ID | click_id format is invalid |
| 400 | INVALID_ITEM_TYPE | type must be product or channel_ad |
| 400 | INVALID_SLOT | slot_at is not asap or an ISO time between 24 hours and 60 days from now |
| 400 | INVALID_REPEAT | repeat_count not in 1/3/7/14, or repeat_freq not in 1/2/3/7 |
| 400 | INVALID_IMAGE | image_url is not an allowed HTTPS URL, could not be downloaded, is not JPEG/PNG/WebP, or is over 5 MB. message gives the reason |
| 400 | INVALID_AD_TEXT | ad_text is over 1,024 characters while image_url is set |
| 400 | AD_LINK | ad_text contains a link or email. Move the link to button_url |
| 400 | INVALID_BUTTON_URL | button_url is not an http(s) URL, a domain or an @username, or is over 300 characters |
| 409 | PRODUCT_UNAVAILABLE | Product is no longer open for sale, or is not allowed in the shop |
| 409 | CHANNEL_UNAVAILABLE | Channel is not accepting ads (paused, no longer registered, or not allowed in the shop) |
| 500 | INTERNAL_ERROR | Unexpected server error (all shop endpoints) |
Sync a Connected User's Cart
Endpoint: POST /api/partner/shop/carts
For users who linked PAYchat to your site (see Connect PAYchat). Send the user's cart whenever it changes; the same cart then appears in the user's PAYchat Chat tab and opens prefilled, so they can finish paying in the Mini App even if they leave your site.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
ext_uid | string | ✅ | The user's ext_uid from Connect PAYchat |
items | array | ✅ | Same items and rules as POST /shop/checkouts. An empty array clears the cart |
click_id | string | ❌ | Same as checkouts |
| Behaviour | Detail |
|---|---|
| Replace | Each call replaces the previous synced cart for that user |
| Clear | items: [] returns { "success": true, "cleared": true } |
| Response | Same checkout object as POST /shop/checkouts; checkout.url opens the cart directly |
| Attribution | With a live key, the items count as visited by that user right away (see Attribution & Commission) |
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | INVALID_EXT_UID | ext_uid missing or not in the u_ + 24 hex format |
| 404 | USER_NOT_CONNECTED | This ext_uid has not connected PAYchat to your account |
| — | Checkout errors | Any error from Create a Checkout |
Conversion Webhook: shop.order.completed
Sent to the URL set with PUT /webhook, signed and retried the same way as other webhooks (see Part 12). If no webhook URL is set, nothing is sent.
| Behaviour | Detail |
|---|---|
| One event per deal | A checkout with several items — or a channel ad with several runs — sends several events |
| Zero-commission deals | Still reported, with commission: 0 |
Example Payload
{
"event": "shop.order.completed",
"data": {
"checkout_id": "shop_1a2b3c4d5e6f7a8b9c0d",
"click_id": "gclid_abc123",
"escrow_id": "DEAL_ID",
"group_id": "DEAL_ID",
"item": "esc:PRODUCT_ID",
"amount": 25,
"commission": 0.125,
"currency": "USDT",
"completed_at": "2026-09-14T08:30:00.000Z"
},
"created_at": "2026-09-14T08:30:00.000Z"
}Payload data Fields
| Field | Description |
|---|---|
checkout_id | Checkout ID (shop_...) |
click_id | The click_id you sent, or null |
escrow_id | ID of the completed PAYchat deal |
group_id | For a channel ad, the first run of the same purchase (see Part 17). Otherwise equals escrow_id |
item | esc:{product id} for a product, ad:{channel_id} for a channel ad |
amount | Amount of the completed deal in USDT |
commission | Your commission in USDT (can be 0) |
currency | Always USDT |
completed_at | Completion time |
Attribution & Commission
| Rule | Detail |
|---|---|
| Tracking | A deal is yours when the buyer opened your checkout (or synced cart) containing that item within 30 days before paying. The latest such visit wins. Tracked deals get orders and all shop.order.* webhooks |
| Commission | Paid only if the buyer first joined PAYchat through your shop — your checkout link or your app_url. Existing PAYchat users who buy through your shop are tracked, with commission: 0 |
| Self-dealing | If you are the seller or channel owner, the order is still tracked, with commission: 0. A purchase you make yourself is not tracked, because opening your own checkout records no visit |
| When | Paid when the deal completes |
| Where | Credited to the PAYchat balance of the Telegram account linked to your partner account. A Telegram message confirms it |
| Amount | Your share of the deal's share fee, which comes from share_fee_rate. The exact amount is sent as commission |
Testing with a Test Key
| Behaviour | Detail |
|---|---|
| Catalog | Test keys (pk_test_...) return the same live catalog |
| Checkout | Created with mode: "test" |
| Attribution | Not recorded: no commission, no orders and no webhooks |
| Payments | ⚠️ Items in a test checkout are real listings. Paying for one inside the Mini App is a real payment and creates a real deal |
Use a test checkout only to check your integration and the Mini App screens. Do not pay for an item unless you really want to buy it. To test your webhook endpoint, use POST /webhook-test (see Part 12).
What the Buyer Sees in the Mini App
- The checkout
urlopens the PAYchat Mini App on a Shop cart screen. It lists your items, the buyer's balance and the total. - A new user first sets a PIN and date of birth. The cart appears when they finish.
- Products are paid one by one with the buyer's PIN. When the cart has two or more channel ads, a Pay all button pays them together with one PIN. Paid items show Paid. Items that are no longer for sale show Not available right now.
- If the balance is too low, the item shows the missing amount and a Go to Deposit button. After topping up, the buyer returns to the Shop cart. The cart also stays in the Chat tab while it has unpaid items, until the checkout expires.
- For a channel ad, the buyer can edit the ad text (5–4,096 characters, or up to 1,024 with a photo) before paying. The photo and button from your checkout are posted with the ad. A repeat ad shows the total for all runs and is paid once. A phone number must be verified the first time.
- After payment, the deal continues in the PAYchat deal room like any other PAYchat escrow deal: delivery, confirmation and disputes. For a channel ad, the channel owner reviews the ad and accepts; funds stay locked until the post is verified.
A checkout link can be opened again at any time before expires_at. It can also be shared: every Telegram user who opens it is tracked for you separately. Opening your own checkout records nothing.
Tracking Google Ads Conversions
The purchase happens inside Telegram, so a tag on your website cannot see it. Report conversions from your server:
- When a visitor arrives from an ad, store the
gclidon your server and link it to a short ID of your own. - Send that short ID as
click_idwhen you create the checkout. - When
shop.order.paidorshop.order.completedarrives, find thegclidbyclick_idand upload an offline conversion to Google Ads, usingamountas the conversion value.
shop.order.completed is sent when the deal completes, which can be days after the click because of the inspection period or the ad schedule. Choose a conversion window long enough to cover it, or count conversions on shop.order.paid. One checkout can produce several events, one per item or ad run.
Order Status Webhooks
Besides shop.order.completed, PAYchat sends a status event for every deal that comes from your shop. Webhooks are signed and retried the same way as other webhooks (see Part 12).
| Event | When |
|---|---|
shop.order.paid | The buyer paid. Funds are held in escrow |
shop.order.ad_posted | Channel ad only — the PAYchat bot posted this run in the channel |
shop.order.ad_removed | Channel ad only — the owner removed the post during the 24 h guarantee |
shop.order.disputed | The buyer opened a dispute |
shop.order.cancelled | The deal was cancelled and the buyer was refunded |
shop.order.completed | The deal completed. Includes your commission (see above) |
| Behaviour | Detail |
|---|---|
| Timing | All events except completed come from a status check that runs every minute, so they arrive up to about one minute after the change |
| Order | shop.order.paid is always sent before any other event for the same deal |
| Refunds | There is no separate "refunded" event. Every refund path ends in shop.order.cancelled |
| Dispute resolved | A dispute ends with shop.order.completed or shop.order.cancelled. If the deal simply continues, no event is sent; use the status endpoints |
| Tracking | Same visit rules as Attribution & Commission. Test checkouts never produce events |
Common data Fields
Every status event carries these fields.
| Field | Description |
|---|---|
checkout_id | Checkout ID (shop_...) |
click_id | The click_id you sent, or null |
escrow_id | PAYchat deal ID — one channel-ad run, or one product deal |
group_id | First run of the same channel-ad purchase; equals escrow_id otherwise |
run_no | Run number within the group. null for products |
run_count | Total runs in the group. null for products |
item | esc:{product id} or ad:{channel_id} |
amount | Deal amount in USDT (per run for channel ads) |
paid / disputed / cancelled
Add status, currency and at (time the change was detected).
{
"event": "shop.order.cancelled",
"data": {
"checkout_id": "shop_1a2b3c4d5e6f7a8b9c0d",
"click_id": "gclid_abc123",
"escrow_id": "esc_a1b2c3d4e5f6",
"group_id": "esc_a1b2c3d4e5f6",
"run_no": 1,
"run_count": 7,
"item": "ad:12",
"amount": 10,
"status": "cancelled",
"currency": "USDT",
"at": "2026-09-14T08:30:00.000Z"
},
"created_at": "2026-09-14T08:30:00.000Z"
}ad_posted / ad_removed
| Event | Extra fields |
|---|---|
shop.order.ad_posted | posted_at, views |
shop.order.ad_removed | posted_at, removed_at, views, early |
The post is monitored only until the run pays out (24 h after posting). A removal inside that window sends shop.order.ad_removed with early: true; the owner is paid pro-rata for the time kept and the run still ends with shop.order.completed. After payout the post is no longer watched, so a later removal sends nothing.
{
"event": "shop.order.ad_removed",
"data": {
"checkout_id": "shop_1a2b3c4d5e6f7a8b9c0d",
"click_id": "gclid_abc123",
"escrow_id": "esc_a1b2c3d4e5f6",
"group_id": "esc_a1b2c3d4e5f6",
"run_no": 1,
"run_count": 7,
"item": "ad:12",
"amount": 10,
"views": 1240,
"posted_at": "2026-09-20T09:00:40.000Z",
"removed_at": "2026-09-20T14:12:05.000Z",
"early": true
},
"created_at": "2026-09-20T14:13:00.000Z"
}
Check Order Status
Get a Checkout
Endpoint: GET /api/partner/shop/checkouts/:id
Returns a checkout you created and every order placed through it. Only your own checkouts can be read.
| Field | Description |
|---|---|
id | Checkout ID |
mode | live or test |
click_id | Your click_id, or null |
items | Items as validated at creation |
opened_count | How many times the checkout was opened in the Mini App (every open counts, including your own) |
expires_at | Expiry time |
expired | true after expires_at |
created_at | Creation time |
orders | Array of Order objects, newest first |
List Orders
Endpoint: GET /api/partner/shop/orders
| Parameter | Type | Default | Description |
|---|---|---|---|
status | string | all | paid, disputed, completed or cancelled |
limit | number | 50 | 1–100 |
offset | number | 0 | Pagination offset |
Newest orders come first. The response also returns limit and offset.
Order Object
| Field | Description |
|---|---|
escrow_id | PAYchat deal ID — one channel-ad run, or one product deal |
checkout_id | The checkout the buyer opened last before paying |
click_id | Your click_id, or null |
group_id | First run of the same channel-ad purchase; equals escrow_id otherwise |
run_no | Run number within the group. null for products |
run_count | Total runs in the group. null for products |
item | esc:{product id} or ad:{channel_id} |
amount | Deal amount in USDT (per run for channel ads) |
currency | Always USDT |
status | paid, disputed, completed or cancelled |
paid_at | Payment time |
updated_at | Last status change |
ad | Channel ads only, otherwise null. See the table below |
| Field | Description |
|---|---|
accepted_at | When the owner accepted, or null |
slot_at | Scheduled post time for this run |
asap | true when the run is posted right after the owner accepts |
keep_hours | Guaranteed time in hours (24). The post stays up after it |
posted_at | When the bot posted, or null |
removed_at | When the owner removed the post during the guarantee, or null |
last_alive_at | Last time the post was confirmed still up |
views | View count of the post, refreshed about every 5 minutes while the run is monitored |
live | true while the post is up and the run is still being monitored |
{
"escrow_id": "esc_a1b2c3d4e5f6",
"checkout_id": "shop_1a2b3c4d5e6f7a8b9c0d",
"click_id": "gclid_abc123",
"group_id": "esc_a1b2c3d4e5f6",
"run_no": 1,
"run_count": 7,
"item": "ad:12",
"amount": 10,
"currency": "USDT",
"status": "paid",
"paid_at": "2026-09-14T08:30:00.000Z",
"updated_at": "2026-09-20T09:01:00.000Z",
"ad": {
"accepted_at": "2026-09-14T09:00:00.000Z",
"slot_at": "2026-09-20T09:00:00.000Z",
"asap": false,
"keep_hours": 24,
"posted_at": "2026-09-20T09:00:40.000Z",
"removed_at": null,
"last_alive_at": "2026-09-20T12:30:00.000Z",
"views": 1240,
"live": true
}
}An order appears up to about one minute after payment. A repeat_count: 7 purchase produces seven orders sharing one group_id. Buyer identity is never returned.
Errors
| Status | Code | Meaning |
|---|---|---|
| 404 | CHECKOUT_NOT_FOUND | No checkout with that ID belongs to your account |
| 500 | INTERNAL_ERROR | Unexpected server error |
Connect PAYchat
Let a user link their PAYchat account to your site without ever seeing an API key. Your site gets a stable, pseudonymous ext_uid for that user — never their Telegram ID. Use it with POST /shop/carts.
Flow
POST /connect/requests→ you get acodeand aconnect_url- Show the user a Connect PAYchat button that opens
connect_url - The first time, the PAYchat Mini App shows your site name and an Approve / Reject screen. A user who already approved your site is approved again automatically, without the screen
- Poll
GET /connect/requests/:codeuntilstatusleavespending. Onapproved, storeext_uid; onrejectedorexpired, let the user start over
Create a Request
Endpoint: POST /api/partner/connect/requests
| Field | Type | Required | Description |
|---|---|---|---|
redirect_uri | string | ❌ | Public HTTPS URL. After approval the Mini App offers the user a button back to it |
{
"success": true,
"code": "pcon_1a2b3c4d5e6f7a8b9c0d",
"connect_url": "https://t.me/paychat_aibot?start=pcon_1a2b3c4d5e6f7a8b9c0d",
"expires_in": 900,
"next": "Poll GET /api/partner/connect/requests/pcon_1a2b3c4d5e6f7a8b9c0d until status is approved."
}A request expires 15 minutes after creation.
Poll a Request
Endpoint: GET /api/partner/connect/requests/:code
| Field | Description |
|---|---|
status | pending, approved, rejected, or expired |
ext_uid | u_ + 24 hex characters. Present only when approved |
Responses are sent with Cache-Control: no-store. Polling counts against the shared GET rate limit (see Part 3).
About ext_uid
| Rule | Detail |
|---|---|
| Stable | The same user always gets the same ext_uid on your account |
| Private | It is derived from your Secret Key, so no other partner can map it to a Telegram ID |
| Key rotation | ⚠️ POST /regenerate-key rotates the Secret Key, so every ext_uid changes afterwards. Re-connect your users after rotating |
| Test keys | Requests made with pk_test_ are marked test mode |
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | BAD_REDIRECT | redirect_uri is not a public HTTPS URL |
| 404 | NOT_FOUND | Unknown code, or it belongs to another partner |
Part 17: Channel Ads — How a Deal Runs
Who Does What
Your site lists channels and creates checkouts. Everything that touches money or the Telegram channel runs inside PAYchat.
| Step | Done by | Where |
|---|---|---|
| List channels, build the cart | Your site | GET /shop/channels |
| Create a checkout (ad text, optional photo) | Your site | POST /shop/checkouts |
| Top up, verify phone, pay | Advertiser | PAYchat Mini App |
| Review the ad text and photo, accept or decline | Channel owner | Telegram (PAYchat bot message) |
| Post the ad and confirm it stays up | PAYchat bot | Telegram channel |
| Refunds, pro-rata settlement, owner payout, disputes | PAYchat | Automatic |
| Follow progress | Your site | Polling or webhooks — read only |
Your site never posts to a channel and never moves funds. There is no endpoint for reporting "posted", "retention complete" or "removed early" — PAYchat detects all of these itself and pushes shop.order.ad_posted / shop.order.ad_removed to you.
Lifecycle of One Run
Each run is its own deal (escrow_id). Runs of one purchase share a group_id.
paid ─ owner accepts ─────────────► ad.accepted_at
─ bot posts ─────────────────► ad.posted_at → shop.order.ad_posted
─ 24 h up ───────────────────► owner paid → shop.order.completed
(post stays in the channel)
─ owner removes within 24 h ─► ad.removed_at → shop.order.ad_removed (early:true)
then pro-rata payout → shop.order.completed
─ owner silent 24h / declines / pauses channel
/ bot lost rights / post not made within 30 min of slot
/ advertiser cancels before acceptance ────────────► cancelled, refund
→ shop.order.cancelled
─ advertiser disputes ───────► disputed → completed | cancelledTop-level status is always paid, disputed, completed or cancelled. The finer ad lifecycle is in ad.* on the Order object.
Refund Rules
| Situation | Result |
|---|---|
| Owner does not answer within 24 h | Full refund. Every run not yet posted is cancelled |
| Owner declines | Full refund. Every run not yet posted is cancelled |
| Owner pauses the channel | Requests the owner has not accepted yet are refunded at once. Accepted runs continue |
| Advertiser cancels | Allowed 6 h after payment and only before the owner accepts. Full refund; every run not yet posted is cancelled |
| Bot loses posting rights before a run | That run refunded; channel hidden from the catalog |
Post not made within 30 min of slot_at (after acceptance) | That run and every later unposted run refunded, in one pass |
| Post removed within 24 h | Owner paid pro-rata for the time kept (under 1 h = nothing); advertiser gets the difference |
| Post removed after 24 h | No effect — the run has already paid out |
| Low views | Not a refund reason. Views depend on the channel |
All of this is automatic. The partner API has no route that releases or refunds buyer funds. For a repeat purchase, expect one shop.order.cancelled per cancelled run.
Good to Know
| Topic | Detail |
|---|---|
| Post stays up | PAYchat never removes an ad. The advertiser buys a 24 h guarantee; after that the post remains at the owner's discretion |
| Paused channels | An owner can pause a channel. It leaves GET /shop/channels and a checkout on it returns CHANNEL_UNAVAILABLE until the owner resumes |
| Owner review | The owner sees the ad text and photo in the acceptance message and can decline. Ads are text with an optional single photo; videos only as links |
| Formatting | **bold** and __italic__ are rendered in the channel post. Links are not allowed in the ad text — use button_url for one link button. If Telegram rejects the formatting, the ad is posted as plain text (see Part 16 › Ad Text Formatting) |
| Own channels | A channel you own is tracked like any other: orders and webhooks are sent, with commission: 0 |
| Verification | Phone verification and top-up happen inside the Mini App at payment time. Nothing to build on your side — tell the advertiser about the redirect beforehand |
| Deal room | To show the advertiser a run inside PAYchat, deep-link to the Mini App with the run's escrow_id: https://t.me/paychat_aibot?startapp={escrow_id}. Using ?start={escrow_id} instead sends a bot message with a View Escrow button that opens the same screen |
| Paused API | 503 on any shop endpoint means the partner API is temporarily paused. Retry later |
| Test keys | pk_test_ checkouts create no orders and no webhooks (see Part 16 › Testing) |
Recent Changes (Sep 11 – Nov 1, 2026)
Partner-facing changes only. Dates are when the change was finalized. Newer rows replace older ones where they differ.
| Date | Area | Change |
|---|---|---|
| Nov 1 | Catalog | New optional field canonical_url on products and channels (matching paychat.info page). Products also return paid_amount and saved_count, and come in the Mini App feed order |
| Oct 30 | Checkouts | items limit raised from 20 to 50 |
| Oct 30 | Carts | New POST /shop/carts: keep a connected user's cart in sync; it shows in their PAYchat Chat tab |
| Oct 29 | Catalog | Adult products and adult, gambling, hacking and similar channels are no longer listed; checkouts on them return PRODUCT_UNAVAILABLE / CHANNEL_UNAVAILABLE |
| Oct 24 | Channel ads | New button_url / button_text: one link button under the post |
| Oct 17 | Rate limits | Exceeding a rate limit blocks API access for 10 minutes (TEMPORARILY_BLOCKED); three times within 24 hours deactivates the partner account |
| Oct 16 | Channel ads | Links and emails are no longer allowed in ad_text (AD_LINK). Use button_url. Replaces the Sep 16 link formatting |
| Oct 14 | Catalog | Sellers and channel owners are always masked: telegram_id, username and photo_url are null, masked is true |
| Oct 11 | Checkouts | New app_url for single-channel checkouts: opens the channel's ad screen in the Mini App |
| Oct 7 | Channel ads | Every ad is a 24 h run; prices is legacy |
| Oct 2 | GET /shop/channels | Same order as the Mini App channel list, with new sort, country, lang and q parameters |
| Sep 24 | GET /shop/channels | category filter and field, plus categories and total in the response |
| Sep 19 | Commission | Commission is paid only when the buyer first joined PAYchat through your shop. Other shop deals are still tracked, with commission: 0 |
| Sep 19 | Mini App | Two or more channel ads in a cart can be paid together with one PIN |
| Sep 18 | Channel ads | slot_at accepts asap (default) and otherwise must be 24 hours to 60 days ahead. Orders gain ad.asap |
| Sep 16 | Channel ads | channel_ad items accept image_url (one JPEG/PNG/WebP photo, max 5 MB, downloaded at checkout). With a photo, ad_text max is 1,024. New errors INVALID_IMAGE, INVALID_AD_TEXT |
| Sep 16 | Channel ads | ad_text formatting: **bold**, __italic__, and links written as [label] followed directly by (https://…). If Telegram rejects the formatting, the ad is posted as plain text |
| Sep 16 | Orders & webhooks | Deals on your own products or channels are now tracked: orders and all shop.order.* webhooks are sent, with commission: 0 |
| Sep 15 | GET /shop/channels | slot_gap_hours now returns 0 and busy_slots is always []. Both keys are kept for compatibility — ignore them. No slot spacing or blocking of any kind |
| Sep 15 | Channel ads | ad_text was being cut to 300 characters at posting time. Fixed — the full text (up to 2,000) is now posted to the channel and shown to the owner in the acceptance message |
| Sep 15 | Docs | Refresh cadence corrected: members / avg_views refresh about every 6 hours; ad.views about every 5 minutes while a run is monitored |
| Sep 14 | Channel ads | Posts are never removed. PAYchat no longer deletes an ad after 24 h. keep_hours (24) now means the guaranteed window: early removal inside it is settled pro-rata, payout lands 24 h after posting, and the post stays up afterwards |
| Sep 14 | Channel ads | Paused channels: an owner can pause a channel. It leaves the catalog and a checkout on it returns CHANNEL_UNAVAILABLE. Pausing refunds any request the owner has not accepted yet; accepted runs continue |
| Sep 14 | Channel ads | Refund paths added: advertiser cancel (6 h after payment, before acceptance) also cancels every unposted run; a post not made within 30 min of slot_at after acceptance refunds that run and all later unposted runs in one pass; bot losing rights refunds the affected run and hides the channel |
| Sep 14 | Channel ads | Repeat purchases from the shop: repeat_count (1/3/7/14) and repeat_freq (1/2/3/7 days) on channel_ad items. One payment in the Mini App; each run is its own order and shares a group_id |
| Sep 14 | Webhooks | New events shop.order.ad_posted and shop.order.ad_removed (with early flag). All shop events now carry group_id; paid / cancelled / disputed also carry run_no / run_count |
| Sep 14 | Orders | Order object gained the ad block (accepted_at, slot_at, keep_hours, posted_at, removed_at, last_alive_at, views, live) |
| Sep 14 | Connect PAYchat | New: POST /connect/requests and GET /connect/requests/:code. Users link their PAYchat account to your site with one tap; you receive a stable pseudonymous ext_uid (never a Telegram ID). Rotating your key changes every ext_uid |
| Sep 13 | Mini App | After topping up from the cart, the buyer now returns to the Shop cart, not the main screen |
| Sep 12 | Orders & webhooks | New: GET /shop/checkouts/:id, GET /shop/orders, and status webhooks shop.order.paid / disputed / cancelled (1-minute status sync) |
| Sep 12 | Mini App | Phone verification is requested at payment time for a channel ad, then the purchase retries automatically. Nothing to build on your side |
| Sep 11 | Partner Shop | Launched: GET /shop/products, GET /shop/channels, POST /shop/checkouts, shop.order.completed conversion webhook, 30-day last-click attribution, commission from the seller's share_fee_rate |