PAYchat

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

bash
npm install paychat-sdk
javascript
import 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

KeyFormatPurpose
API Keypk_xxxx...Authenticate API requests
Secret Keysk_xxxx...Verify webhook signatures

The Secret Key is never sent in requests — it only verifies webhook signatures.

How to Authenticate

code
x-api-key: pk_your_api_key_here

This is not a Bearer token.

Example Request

bash
curl https://paychat.info/api/partner/balance \
  -H "x-api-key: pk_abc123..."

Access Conditions

ConditionResponse
Header missing or key unknown401 INVALID_API_KEY
Account disabled403 PARTNER_INACTIVE
Free plan, balance below minimum403 BALANCE_TOO_LOW
Free plan, over 1,000 calls this month429 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

PlanMonthly callsRate multiplier
Free1,000×1
Founding MemberUnlimited×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)DiscountTotal Holdings (USDT)Discount
0 – 1000%20,000 – 100,00060%
100 – 20010%100,000 – 500,00070%
200 – 50020%500,000 – 1,000,00080%
500 – 1,50030%1,000,000 – 2,000,00090%
1,500 – 5,00040%2,000,000 +100%
5,000 – 20,00050%

Live figures: GET /fee-info and GET /plans.

🔄How It Works

  1. Pay once → lifetime API access
  2. API fees deducted from your balance per call
  3. More holdings = higher discount = lower fees
  4. 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.

EndpointFreeFounding Member
POST /payments100/min500/min
POST /withdrawals50/min250/min
All GET requests300/min1,500/min

Response Headers

HeaderMeaning
X-RateLimit-LimitMax requests per window
X-RateLimit-RemainingRequests left
X-RateLimit-ResetSeconds until reset

When You Hit the Limit

json
{
  "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.

ControlError codeStatus
Per-transaction capTX_LIMIT_EXCEEDED403
Daily capDAILY_LIMIT_EXCEEDED403

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.

BehaviourDetail
Retention24 hours
ReplayReturns the original stored response
WithdrawalsKey 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

MethodEndpointDescription
POST/paymentsCreate a payment link
GET/payments/:link_idCheck payment status

QR Payments

MethodEndpointDescription
POST/qr-paymentsCreate a QR payment
GET/qr-paymentsList QR payments
GET/qr-payments/:idCheck QR status
DELETE/qr-payments/:idDeactivate a QR payment

Escrow

MethodEndpointDescription
POST/escrowsCreate an escrow
GET/escrowsList escrows
GET/escrows/:idCheck escrow status
POST/escrows/:id/releaseRelease funds to seller
POST/escrows/:id/cancelCancel and refund buyer

Withdrawals & Deposits

MethodEndpointDescription
POST/withdrawalsSend USDT to a user
POST/depositsCreate a deposit request
GET/depositsList deposits
GET/deposits/:idCheck deposit status
DELETE/deposits/:idCancel a pending deposit
GET/deposit-addressGet deposit address

Gifts, Lucky Drops & Collects

Paid from your partner balance. Detailed reference to follow.

MethodEndpointDescription
POST/giftsCreate a gift for a user
GET/gifts/:idCheck gift status
POST/luckydropsCreate a lucky drop
GET/luckydrops/:idCheck lucky drop status
POST/collectsCreate a collect request
GET/collects/:idCheck collect status

M-Pesa

MethodEndpointDescription
GET/mpesa/transactionsList M-Pesa transactions
GET/mpesa/transactions/:idCheck an M-Pesa transaction

Balance & Reporting

MethodEndpointDescription
GET/balanceCheck your balance
GET/fee-infoCurrent fee rate and discount tier
GET/transactionsList transactions
GET/dashboardDashboard summary
GET/statsDetailed statistics
GET/users/:telegram_idLook up a user

Webhooks

MethodEndpointDescription
PUT/webhookSet webhook URL
POST/webhook-testSend a test webhook
GET/webhooksList webhook delivery logs
POST/webhooks/:id/retryRetry a failed delivery

Account

MethodEndpointDescription
PUT/settingsUpdate name / webhook URL
POST/regenerate-keyRotate API and Secret keys
GET/transfer/:requestIdCheck a transfer

Partner Shop

MethodEndpointDescription
GET/shop/productsList escrow products open for sale
GET/shop/channelsList Telegram channels accepting ads
POST/shop/checkoutsCreate a Mini App checkout link
POST/shop/cartsSync a connected user's cart (shows in the Mini App Chat tab)
GET/shop/checkouts/:idRead a checkout and its orders
GET/shop/ordersList your orders

See Part 16 and Part 17.

Connect PAYchat

MethodEndpointDescription
POST/connect/requestsAsk a user to link their PAYchat account to your site
GET/connect/requests/:codePoll the request; returns the user's ext_uid once approved

See Part 16 › Connect PAYchat.

Testing (test keys only)

MethodEndpointDescription
POST/test/simulate-paymentMark a test payment or escrow as paid and fire its webhooks

Public Endpoints (No API Key Required)

MethodEndpointDescription
GET/plansView available plans
GET/me?telegram_id=Check partner status
POST/registerRegister as a partner
POST/register-freeRegister on the Free plan
POST/upgradeUpgrade to Founding Member
POST/resetReset partner account
POST/transferSession-authenticated transfer
POST/sandbox/startIssue a sandbox key claim link (3 per IP per day)
GET/sandbox/claim/:tokenClaim the sandbox key from the link above

📏Minimum Amounts

These differ per resource — a common source of INVALID_AMOUNT.

ResourceMinimum
Payment link0.000001 USDT
Withdrawal0.000001 USDT
QR payment (fixed amount)0.01 USDT
Escrow1 USDT
Deposit1 USDT

Part 5: Create Payment


💵Create a Payment

Endpoint: POST /api/partner/payments

Request Body

FieldTypeRequiredDescription
amountnumber✅Amount in USDT (min: 0.000001)
descriptionstring❌What the payment is for
metadataobject❌Custom data, stored and returned on lookup
idempotency_keystring❌Prevent duplicate payments (24h)

Example Request

bash
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

json
{
  "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"
  }
}
⚠️ id is the 6-character link_id. Use this value for GET /payments/:link_id. Earlier SDK versions returned a separate pay_xxx identifier that was not queryable — it has been removed.

⚠️ expires_at is 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

  1. Redirect your customer to payment_url
  2. They complete payment in Telegram
  3. You receive a payment.completed webhook
  4. Funds are added to your balance instantly

🔍Check Payment Status

Endpoint: GET /api/partner/payments/:link_id

bash
curl https://paychat.info/api/partner/payments/x7k2m9 \
  -H "x-api-key: pk_your_key"

Example Response (Completed)

json
{
  "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

StatusMeaning
pendingWaiting for payment
completedPayment received

Part 6: QR Payments


📱Create a QR Payment

Endpoint: POST /api/partner/qr-payments

Request Body

FieldTypeRequiredDescription
amountnumberconditionalRequired when is_fixed_amount is true. Min 0.01 USDT
descriptionstring❌Payment description
metadataobject❌Custom data, stored and returned
is_fixed_amountboolean❌Default true
min_amountnumber❌Used only when is_fixed_amount is false
max_amountnumber❌Used only when is_fixed_amount is false
single_useboolean❌Default true
expires_hoursnumber❌Hours, not seconds. Stored only — status is not auto-updated. Omit for no expiry
idempotency_keystring❌Prevent duplicates (24h)
⚠️ expires_hours is measured in hours. If omitted, the QR code does not expire.

Example Request

bash
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

json
{
  "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)

bash
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

StatusMeaningSet by
activeAccepting paymentCreation, or rollback from processing
processingA payment is being executedPayment flow (concurrency lock)
paidPayment receivedPayment flow
inactiveDeactivatedDELETE /qr-payments/:id
⚠️ There is no expired status. expires_at is 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 reports active and can still be paid. Compare expires_at yourself, or call DELETE /qr-payments/:id to 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.

json
{
  "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

FieldTypeRequiredDescription
buyer_telegram_idstring✅Buyer's Telegram ID
seller_telegram_idstring✅Seller's Telegram ID
amountnumber✅Min 1 USDT
descriptionstring❌What the escrow is for
metadataobject❌Custom data, stored and returned
expires_hoursnumber❌Default 72
idempotency_keystring❌Prevent duplicates (24h)

💡Things to Know

BehaviourDetail
1% feefee = amount × 0.01, deducted from the seller payout
Seller auto-registrationIf seller_telegram_id has no PAYchat account, one is created automatically

Example Request

bash
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

json
{
  "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_id and seller_telegram_id, not nested buyer / seller objects.

Escrow Lifecycle

StatusMeaning
pendingCreated, waiting for buyer payment
processingBuyer payment being processed
paidBuyer funded the escrow
releasingRelease in progress
completedFunds released to seller
cancellingCancellation in progress
cancelledCancelled, buyer refunded

Release / Cancel

bash
# 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

FieldTypeRequiredDescription
telegram_idstring✅Recipient's Telegram ID
amountnumber✅Amount in USDT (min: 0.000001)
memostring❌Message shown to recipient
idempotency_keystring❌Prevent duplicate withdrawals (24h)

Example Request

bash
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

json
{
  "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

FeatureHow It Works
Instant deliveryFunds arrive immediately
Auto-registrationNew users get a PAYchat account automatically
NotificationsRecipients get a Telegram message
Referral registrationFirst withdrawal registers the user as your referral
Self-transfer blockedReturns CANNOT_WITHDRAW_TO_SELF

Part 9: Deposits


🏦Fund Your Partner Account

Endpoint: POST /api/partner/deposits

Request Body

FieldTypeRequiredDescription
amountnumber✅Min 1 USDT
networkstring❌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

bash
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

StatusMeaning
pendingWaiting for funds
completedCredited to your balance
expiredRequest expired
cancelledCancelled via DELETE
Deposits do not emit webhooks. Poll GET /deposits/:id instead.

🔑Get Deposit Address

Endpoint: GET /api/partner/deposit-address

Example Response

json
{
  "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 with POST /deposits and "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

json
{
  "success": true,
  "balance": {
    "available": 1250.50,
    "currency": "USDT",
    "monthly_volume": 48200.00,
    "fee_rate": 0.01
  }
}
FieldDescription
availableUSDT you can withdraw or send
monthly_volumeTotal volume processed this month
fee_rateYour 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

ParameterTypeDefaultDescription
typestringallpayment, withdraw, escrow, or qr
statusstringallFilter by status
fromstring-Start date (ISO 8601)
tostring-End date (ISO 8601)
limitnumber20Results per page (max 100)
offsetnumber0Pagination offset

Example Response

json
{
  "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

EndpointReturns
GET /dashboardPartner info, today/week totals, webhook delivery counts, recent transactions
GET /stats?period=30dDaily breakdown and totals. period accepts 7d, 30d, 90d
GET /escrows, GET /qr-payments, and GET /deposits return a subset of fields and no pagination object. Use the detail endpoint for the full record.

Part 11: Webhooks


🔔Webhooks

Available Events

EventDescriptionWhen It Fires
payment.completedPayment receivedCustomer completes payment link
withdraw.completedWithdrawal sentFunds delivered to user
qr.paidQR payment receivedCustomer pays via QR code
escrow.createdEscrow createdNew escrow order created
escrow.paidEscrow fundedBuyer completes escrow payment
escrow.releasedEscrow releasedFunds released to seller
escrow.cancelledEscrow cancelledCancelled (refunded if paid)
testTest eventYou send a test webhook

Partner Shop Events

EventDescriptionWhen It Fires
shop.order.paidShop deal fundedBuyer pays for a shop item or ad run
shop.order.ad_postedAd postedPAYchat bot posts a channel-ad run
shop.order.ad_removedAd removedThe channel owner took the post down during the 24 h guarantee
shop.order.disputedShop deal disputedBuyer opens a dispute
shop.order.cancelledShop deal cancelledDeal cancelled and buyer refunded
shop.order.completedShop deal completedDeal completes; carries your commission

Payloads and rules are in Part 16 › Order Status Webhooks. Shop events carry click_id instead of metadata.

There are no qr.created, qr.expired, or deposit events. Poll the relevant GET endpoint instead.

Setting Up Webhooks

1. Set your webhook URL

bash
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

bash
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
  • localhost or any .local hostname
  • 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

HeaderDescription
Content-Typeapplication/json
X-PAYchat-EventEvent type
X-PAYchat-SignatureHMAC signature for verification
X-PAYchat-Delivery-IDUnique delivery ID

Webhook Payload: payment.completed

json
{
  "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 as metadata on creation is echoed back here — no follow-up GET needed. This applies to payment.completed, qr.paid, escrow.created, escrow.paid, escrow.released, and escrow.cancelled. withdraw.completed carries memo instead.

Webhook Payload: withdraw.completed

json
{
  "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

  1. We build the message {timestamp}.{JSON.stringify(payload)}
  2. We sign it with your Secret Key using HMAC-SHA256
  3. We send the result in X-PAYchat-Signature

Signature Format

code
X-PAYchat-Signature: t=1704628800,v1=abc123def456...
PartDescription
tUnix timestamp (seconds)
v1HMAC-SHA256 signature (hex)

Reject requests whose timestamp is older than 5 minutes.

Verification Code (Node.js)

javascript
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:

javascript
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

AttemptWait Time
1stImmediate
2nd1 minute
3rd5 minutes
4th30 minutes
5th2 hours
FinalMarked 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

RequirementDetails
Status codeAny 2xx (200–299)
TimeoutRespond within 10 seconds
Response bodyNot required

Part 13: Error Handling


❌Error Handling

Error Response Format

json
{
  "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

CodeMeaning
200Success
400Bad request (check your input)
401Authentication failed
403Forbidden (inactive, or limit exceeded)
404Resource not found
409Conflict (item no longer available)
429Rate or monthly limit exceeded
500Server error (retry later)
503Partner API temporarily paused. Retry later

Authentication & Access

CodeMeaning
INVALID_API_KEYMissing or invalid API key
PARTNER_INACTIVEPartner account is inactive
BALANCE_TOO_LOWBalance below the minimum for API access
SANDBOX_EXPIREDSandbox key has expired
UNAUTHORIZEDNot authorised for this resource
FORBIDDENAction not permitted
NOT_PARTNERCaller is not a partner
ALREADY_PARTNERAlready registered
ALREADY_UPGRADEDPlan already upgraded
ADMIN_NOT_FOUNDAdmin account not found

Limits

CodeMeaning
RATE_LIMIT_EXCEEDEDToo many requests
MONTHLY_LIMIT_EXCEEDEDFree plan monthly call limit reached
TX_LIMIT_EXCEEDEDPer-transaction cap exceeded
DAILY_LIMIT_EXCEEDEDDaily cap exceeded
TEMPORARILY_BLOCKEDRate limit exceeded: API access blocked for 10 minutes. Three of these within 24 hours deactivate the partner account

Validation

CodeMeaning
INVALID_AMOUNTAmount below minimum or missing
INVALID_PARAMSRequired parameters missing
INVALID_RANGEmin_amount greater than max_amount
INVALID_NETWORKNetwork must be tron or ton
INVALID_URLMalformed URL
INVALID_WEBHOOK_URLWebhook URL must be a public HTTPS endpoint
SAME_USERBuyer and seller are identical
CANNOT_WITHDRAW_TO_SELFCannot withdraw to yourself
NO_UPDATESNo fields to update
INVALID_ITEMSShop checkout items missing or outside 1–50
INVALID_CLICK_IDShop checkout click_id format is invalid
INVALID_ITEM_TYPEShop item type must be product or channel_ad
INVALID_SLOTChannel ad slot_at is not asap or an ISO time between 24 hours and 60 days from now
INVALID_REPEATrepeat_count / repeat_freq not in the allowed set
INVALID_IMAGEShop ad image_url rejected (URL, host, download, format or size). message gives the reason
INVALID_AD_TEXTShop ad ad_text over 1,024 characters while image_url is set
AD_LINKShop ad ad_text contains a link or email — use button_url
INVALID_BUTTON_URLShop ad button_url is not an http(s) URL, a domain or an @username
INVALID_EXT_UIDCart sync ext_uid missing or malformed
BAD_REDIRECTConnect redirect_uri must be a public HTTPS URL

Resource State

CodeMeaning
PAYMENT_NOT_FOUNDPayment not found
QR_NOT_FOUNDQR payment not found
ESCROW_NOT_FOUNDEscrow not found
DEPOSIT_NOT_FOUNDDeposit not found
WEBHOOK_NOT_FOUNDWebhook log not found
USER_NOT_FOUNDUser not found
BUYER_NOT_FOUNDBuyer not found
SELLER_NOT_FOUNDSeller not found
NO_WEBHOOK_URLWebhook URL not configured
ACTIVE_REQUEST_EXISTSAn active deposit request already exists
ESCROW_PROCESSINGEscrow is currently being processed
PRODUCT_UNAVAILABLEShop product is no longer open for sale
CHANNEL_UNAVAILABLEChannel is not accepting ads
CHECKOUT_NOT_FOUNDShop checkout not found
NOT_FOUNDConnect request code unknown
USER_NOT_CONNECTEDThis ext_uid has not connected PAYchat to your account

Execution

CodeMeaning
INSUFFICIENT_BALANCENot enough balance
REQUEST_IN_PROGRESSDuplicate idempotency key still processing
RELEASE_FAILEDEscrow release failed
CANCEL_FAILEDEscrow cancellation failed
INTERNAL_ERRORUnexpected server error

Client-Side (SDK only)

CodeMeaning
TIMEOUTRequest exceeded the SDK timeout. Never returned by the server

Part 14: Code Examples


bash
npm install paychat-sdk
javascript
import 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

javascript
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

javascript
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

javascript
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)

javascript
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:

bash
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?

ChannelContact
Telegram@paychat_mafia
Email[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)

  1. POST https://paychat.info/api/partner/sandbox/start — no auth. Returns JSON with a claim_url. Limit: 3 issues per IP per day.
  2. GET the claim_url — returns a sandbox api_key (prefix pk_test_, valid 30 days, 1,000 calls/month) plus ready-to-run curl examples.
  3. 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

  1. Load the catalog with GET /shop/products and GET /shop/channels
  2. Show the items in your own shop and cart
  3. Create a checkout with POST /shop/checkouts (or keep a connected user's cart in sync with POST /shop/carts)
  4. Send the buyer to the checkout url — it opens the PAYchat Mini App
  5. The buyer pays inside the Mini App
  6. Follow each deal with the order endpoints and webhooks. Each completed deal sends you shop.order.completed with your commission

Shop Endpoints

MethodEndpointDescription
GET/shop/productsList escrow products open for sale
GET/shop/channelsList Telegram channels accepting ads
POST/shop/checkoutsCreate a Mini App checkout link
POST/shop/cartsSync the cart of a user who connected PAYchat to your site
GET/shop/checkouts/:idRead a checkout and its orders
GET/shop/ordersList 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

ParameterTypeDefaultDescription
limitnumber501–100
offsetnumber0Pagination offset
categorystringalldigital, gaming, service, or other
countrystringall2-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

bash
curl "https://paychat.info/api/partner/shop/products?category=digital&limit=20" \
  -H "x-api-key: pk_your_key"

Product Fields

FieldDescription
idProduct ID — use this as id in a checkout item
post_idFeed post ID
canonical_urlMatching 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
titleProduct title
descriptionProduct description (empty string if none)
categoryCategory, or null
pricePrice in USDT
currencyAlways USDT
imagesArray of absolute image URLs (empty array if none)
stockRemaining quantity. null means unlimited
inspection_daysInspection period in days, or null
share_fee_rateShare fee rate set by the seller — the source of your commission
countryCountry code, or null
viewsView count
paid_amountUSDT already paid on this product
saved_countHow many users saved this product
sellerSeller profile (see Person Object)
created_atListing 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.

FieldValue
nameMasked name: @ab****** (first 2 characters of the username or name), or #1234 Trader (last 4 digits of the Telegram ID) when neither exists
telegram_idAlways null
usernameAlways null
photo_urlAlways null — use a placeholder image
maskedAlways 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

ParameterTypeDefaultDescription
limitnumber501–100
offsetnumber0Pagination offset
sortstringrecrec (Mini App ranking), votes, cheap, or views
categorystringallcrypto, tech, news, biz, ent, edu, life, or other
countrystringallCountry code. Omit for all channels
langstringallChannel language code
qstring—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

FieldDescription
totalNumber of channels matching your filters
categoryThe category filter applied, or null
categoriesChannel count per category, e.g. { "crypto": 120, "news": 45 }
slot_gap_hoursLegacy field, always 0. Ignore it

Channel Fields

FieldDescription
idChannel ID — use this as channel_id in a checkout item
canonical_urlMatching 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
titleChannel title (falls back to the username)
usernameTelegram username, or null
urlhttps://t.me/{username}, or null
membersSubscriber count
avg_viewsAverage views per post
er_percentavg_views ÷ members × 100, one decimal. 0 if either value is missing
pricePrice per run in USDT
pricesLegacy 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
currencyAlways USDT
share_fee_rateShare fee rate for this channel
countryCountry code, or null
categoryOne of the 8 category keys, or null
busy_slotsLegacy field, always an empty array. Ignore it — a channel accepts any number of ads at any time
ownerChannel owner profile (see Person Object). null for channels PAYchat is still bringing on board
created_atRegistration time

members and avg_views refresh about every 6 hours. Cache accordingly.


🧾Create a Checkout

Endpoint: POST /api/partner/shop/checkouts

Request Body

FieldTypeRequiredDescription
itemsarray✅1–50 items
click_idstring❌Your tracking ID, e.g. an ad click ID. 1–128 chars: letters, digits, _ . -. Returned in every order and webhook

Item: product

FieldRequiredDescription
type✅"product"
id✅Product id from GET /shop/products

Item: channel_ad

FieldRequiredDescription
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

SyntaxResultLimit
**text**Bold1–300 characters, one line
__text__Italic1–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

RuleDetail
Protocolhttps only, URL up to 1,000 characters
HostMust resolve to a public address. Private, loopback and internal hosts are rejected
RedirectsNot followed — give the final image URL
Timeout10 seconds
FormatJPEG, PNG or WebP, checked by file content, not by extension
Size5 MB or smaller
WhenDownloaded once at checkout creation. Changing the file at the URL later has no effect
CaptionWith a photo, ad_text is posted as the photo caption, so it is limited to 1,024 characters

Rules

RuleDetail
All or nothingOne invalid item rejects the whole checkout
No slot blockingAny number of ads can be booked in the same channel at any time
PriceChannel price × repeat_count, taken at payment time. The checkout does not lock the price
Run timesRun 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
Guarantee24 h from posting. If the owner removes the post inside that window, the advertiser is refunded pro-rata (see Part 17)
One paymentThe advertiser pays the whole run set once in the Mini App

Example Request

bash
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

json
{
  "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."
}
FieldDescription
idCheckout ID (shop_ + 20 hex characters)
startappSame value — the Mini App start parameter
urlSend the buyer here. It opens the PAYchat Mini App on the shop cart
app_urlOnly 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
itemsValidated 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_idThe click_id you sent, or null
modelive or test, depending on the key used
expires_at7 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

StatusCodeMeaning
400INVALID_ITEMSitems missing, not an array, or outside 1–50
400INVALID_CLICK_IDclick_id format is invalid
400INVALID_ITEM_TYPEtype must be product or channel_ad
400INVALID_SLOTslot_at is not asap or an ISO time between 24 hours and 60 days from now
400INVALID_REPEATrepeat_count not in 1/3/7/14, or repeat_freq not in 1/2/3/7
400INVALID_IMAGEimage_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
400INVALID_AD_TEXTad_text is over 1,024 characters while image_url is set
400AD_LINKad_text contains a link or email. Move the link to button_url
400INVALID_BUTTON_URLbutton_url is not an http(s) URL, a domain or an @username, or is over 300 characters
409PRODUCT_UNAVAILABLEProduct is no longer open for sale, or is not allowed in the shop
409CHANNEL_UNAVAILABLEChannel is not accepting ads (paused, no longer registered, or not allowed in the shop)
500INTERNAL_ERRORUnexpected 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

FieldTypeRequiredDescription
ext_uidstring✅The user's ext_uid from Connect PAYchat
itemsarray✅Same items and rules as POST /shop/checkouts. An empty array clears the cart
click_idstring❌Same as checkouts
BehaviourDetail
ReplaceEach call replaces the previous synced cart for that user
Clearitems: [] returns { "success": true, "cleared": true }
ResponseSame checkout object as POST /shop/checkouts; checkout.url opens the cart directly
AttributionWith a live key, the items count as visited by that user right away (see Attribution & Commission)

Errors

StatusCodeMeaning
400INVALID_EXT_UIDext_uid missing or not in the u_ + 24 hex format
404USER_NOT_CONNECTEDThis ext_uid has not connected PAYchat to your account
—Checkout errorsAny 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.

BehaviourDetail
One event per dealA checkout with several items — or a channel ad with several runs — sends several events
Zero-commission dealsStill reported, with commission: 0

Example Payload

json
{
  "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

FieldDescription
checkout_idCheckout ID (shop_...)
click_idThe click_id you sent, or null
escrow_idID of the completed PAYchat deal
group_idFor a channel ad, the first run of the same purchase (see Part 17). Otherwise equals escrow_id
itemesc:{product id} for a product, ad:{channel_id} for a channel ad
amountAmount of the completed deal in USDT
commissionYour commission in USDT (can be 0)
currencyAlways USDT
completed_atCompletion time

🎯Attribution & Commission

RuleDetail
TrackingA 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
CommissionPaid 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-dealingIf 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
WhenPaid when the deal completes
WhereCredited to the PAYchat balance of the Telegram account linked to your partner account. A Telegram message confirms it
AmountYour 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

BehaviourDetail
CatalogTest keys (pk_test_...) return the same live catalog
CheckoutCreated with mode: "test"
AttributionNot 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

  1. The checkout url opens the PAYchat Mini App on a Shop cart screen. It lists your items, the buyer's balance and the total.
  2. A new user first sets a PIN and date of birth. The cart appears when they finish.
  3. 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.
  4. 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.
  5. 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.
  6. 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:

  1. When a visitor arrives from an ad, store the gclid on your server and link it to a short ID of your own.
  2. Send that short ID as click_id when you create the checkout.
  3. When shop.order.paid or shop.order.completed arrives, find the gclid by click_id and upload an offline conversion to Google Ads, using amount as 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).

EventWhen
shop.order.paidThe buyer paid. Funds are held in escrow
shop.order.ad_postedChannel ad only — the PAYchat bot posted this run in the channel
shop.order.ad_removedChannel ad only — the owner removed the post during the 24 h guarantee
shop.order.disputedThe buyer opened a dispute
shop.order.cancelledThe deal was cancelled and the buyer was refunded
shop.order.completedThe deal completed. Includes your commission (see above)
BehaviourDetail
TimingAll events except completed come from a status check that runs every minute, so they arrive up to about one minute after the change
Ordershop.order.paid is always sent before any other event for the same deal
RefundsThere is no separate "refunded" event. Every refund path ends in shop.order.cancelled
Dispute resolvedA dispute ends with shop.order.completed or shop.order.cancelled. If the deal simply continues, no event is sent; use the status endpoints
TrackingSame visit rules as Attribution & Commission. Test checkouts never produce events

Common data Fields

Every status event carries these fields.

FieldDescription
checkout_idCheckout ID (shop_...)
click_idThe click_id you sent, or null
escrow_idPAYchat deal ID — one channel-ad run, or one product deal
group_idFirst run of the same channel-ad purchase; equals escrow_id otherwise
run_noRun number within the group. null for products
run_countTotal runs in the group. null for products
itemesc:{product id} or ad:{channel_id}
amountDeal amount in USDT (per run for channel ads)

Add status, currency and at (time the change was detected).

json
{
  "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

EventExtra fields
shop.order.ad_postedposted_at, views
shop.order.ad_removedposted_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.

json
{
  "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.

FieldDescription
idCheckout ID
modelive or test
click_idYour click_id, or null
itemsItems as validated at creation
opened_countHow many times the checkout was opened in the Mini App (every open counts, including your own)
expires_atExpiry time
expiredtrue after expires_at
created_atCreation time
ordersArray of Order objects, newest first

List Orders

Endpoint: GET /api/partner/shop/orders

ParameterTypeDefaultDescription
statusstringallpaid, disputed, completed or cancelled
limitnumber501–100
offsetnumber0Pagination offset

Newest orders come first. The response also returns limit and offset.

Order Object

FieldDescription
escrow_idPAYchat deal ID — one channel-ad run, or one product deal
checkout_idThe checkout the buyer opened last before paying
click_idYour click_id, or null
group_idFirst run of the same channel-ad purchase; equals escrow_id otherwise
run_noRun number within the group. null for products
run_countTotal runs in the group. null for products
itemesc:{product id} or ad:{channel_id}
amountDeal amount in USDT (per run for channel ads)
currencyAlways USDT
statuspaid, disputed, completed or cancelled
paid_atPayment time
updated_atLast status change
adChannel ads only, otherwise null. See the table below
FieldDescription
accepted_atWhen the owner accepted, or null
slot_atScheduled post time for this run
asaptrue when the run is posted right after the owner accepts
keep_hoursGuaranteed time in hours (24). The post stays up after it
posted_atWhen the bot posted, or null
removed_atWhen the owner removed the post during the guarantee, or null
last_alive_atLast time the post was confirmed still up
viewsView count of the post, refreshed about every 5 minutes while the run is monitored
livetrue while the post is up and the run is still being monitored
json
{
  "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

StatusCodeMeaning
404CHECKOUT_NOT_FOUNDNo checkout with that ID belongs to your account
500INTERNAL_ERRORUnexpected 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

  1. POST /connect/requests → you get a code and a connect_url
  2. Show the user a Connect PAYchat button that opens connect_url
  3. 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
  4. Poll GET /connect/requests/:code until status leaves pending. On approved, store ext_uid; on rejected or expired, let the user start over

Create a Request

Endpoint: POST /api/partner/connect/requests

FieldTypeRequiredDescription
redirect_uristring❌Public HTTPS URL. After approval the Mini App offers the user a button back to it
json
{
  "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

FieldDescription
statuspending, approved, rejected, or expired
ext_uidu_ + 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

RuleDetail
StableThe same user always gets the same ext_uid on your account
PrivateIt 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 keysRequests made with pk_test_ are marked test mode

Errors

StatusCodeMeaning
400BAD_REDIRECTredirect_uri is not a public HTTPS URL
404NOT_FOUNDUnknown 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.

StepDone byWhere
List channels, build the cartYour siteGET /shop/channels
Create a checkout (ad text, optional photo)Your sitePOST /shop/checkouts
Top up, verify phone, payAdvertiserPAYchat Mini App
Review the ad text and photo, accept or declineChannel ownerTelegram (PAYchat bot message)
Post the ad and confirm it stays upPAYchat botTelegram channel
Refunds, pro-rata settlement, owner payout, disputesPAYchatAutomatic
Follow progressYour sitePolling 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.

code
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 | cancelled

Top-level status is always paid, disputed, completed or cancelled. The finer ad lifecycle is in ad.* on the Order object.


💸Refund Rules

SituationResult
Owner does not answer within 24 hFull refund. Every run not yet posted is cancelled
Owner declinesFull refund. Every run not yet posted is cancelled
Owner pauses the channelRequests the owner has not accepted yet are refunded at once. Accepted runs continue
Advertiser cancelsAllowed 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 runThat 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 hOwner paid pro-rata for the time kept (under 1 h = nothing); advertiser gets the difference
Post removed after 24 hNo effect — the run has already paid out
Low viewsNot 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

TopicDetail
Post stays upPAYchat never removes an ad. The advertiser buys a 24 h guarantee; after that the post remains at the owner's discretion
Paused channelsAn owner can pause a channel. It leaves GET /shop/channels and a checkout on it returns CHANNEL_UNAVAILABLE until the owner resumes
Owner reviewThe 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 channelsA channel you own is tracked like any other: orders and webhooks are sent, with commission: 0
VerificationPhone 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 roomTo 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 API503 on any shop endpoint means the partner API is temporarily paused. Retry later
Test keyspk_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.

DateAreaChange
Nov 1CatalogNew 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 30Checkoutsitems limit raised from 20 to 50
Oct 30CartsNew POST /shop/carts: keep a connected user's cart in sync; it shows in their PAYchat Chat tab
Oct 29CatalogAdult products and adult, gambling, hacking and similar channels are no longer listed; checkouts on them return PRODUCT_UNAVAILABLE / CHANNEL_UNAVAILABLE
Oct 24Channel adsNew button_url / button_text: one link button under the post
Oct 17Rate limitsExceeding a rate limit blocks API access for 10 minutes (TEMPORARILY_BLOCKED); three times within 24 hours deactivates the partner account
Oct 16Channel adsLinks and emails are no longer allowed in ad_text (AD_LINK). Use button_url. Replaces the Sep 16 link formatting
Oct 14CatalogSellers and channel owners are always masked: telegram_id, username and photo_url are null, masked is true
Oct 11CheckoutsNew app_url for single-channel checkouts: opens the channel's ad screen in the Mini App
Oct 7Channel adsEvery ad is a 24 h run; prices is legacy
Oct 2GET /shop/channelsSame order as the Mini App channel list, with new sort, country, lang and q parameters
Sep 24GET /shop/channelscategory filter and field, plus categories and total in the response
Sep 19CommissionCommission is paid only when the buyer first joined PAYchat through your shop. Other shop deals are still tracked, with commission: 0
Sep 19Mini AppTwo or more channel ads in a cart can be paid together with one PIN
Sep 18Channel adsslot_at accepts asap (default) and otherwise must be 24 hours to 60 days ahead. Orders gain ad.asap
Sep 16Channel adschannel_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 16Channel adsad_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 16Orders & webhooksDeals on your own products or channels are now tracked: orders and all shop.order.* webhooks are sent, with commission: 0
Sep 15GET /shop/channelsslot_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 15Channel adsad_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 15DocsRefresh cadence corrected: members / avg_views refresh about every 6 hours; ad.views about every 5 minutes while a run is monitored
Sep 14Channel adsPosts 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 14Channel adsPaused 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 14Channel adsRefund 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 14Channel adsRepeat 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 14WebhooksNew 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 14OrdersOrder object gained the ad block (accepted_at, slot_at, keep_hours, posted_at, removed_at, last_alive_at, views, live)
Sep 14Connect PAYchatNew: 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 13Mini AppAfter topping up from the cart, the buyer now returns to the Shop cart, not the main screen
Sep 12Orders & webhooksNew: GET /shop/checkouts/:id, GET /shop/orders, and status webhooks shop.order.paid / disputed / cancelled (1-minute status sync)
Sep 12Mini AppPhone verification is requested at payment time for a channel ad, then the purchase retries automatically. Nothing to build on your side
Sep 11Partner ShopLaunched: 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