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 compromisedQ: What are the fees
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 (37)
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 |
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 |
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 |
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) |
Two 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 (8)
| 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 |
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.
Is my metadata included in webhooks
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 |
429 | Rate or monthly limit exceeded |
500 | Server error (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 |
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 |
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 |
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 |
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: No. Webhook payloads carry IDs and amounts only. Fetch metadata with GET /payments/:link_id, GET /escrows/:id, or GET /qr-payments/:id.
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] |
Changelog
| Date | Version | Changes |
|---|---|---|
| 2025-01-07 | 1.0.0 | Initial release |
| 2025-01-10 | 2.0.0 | Holdings-based discount system |
| 2026-08-13 | 2.0.1 | Base URL moved to paychat.info. Documented QR, escrow, and deposit endpoints (37 total). Payment id is now the queryable link_id. metadata persisted for payments, QR, and escrow. Idempotency added to QR and escrow. Free plan, per-endpoint rate limits, spend limits, and the full 38-code error reference published. |
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