MPChatMPChat/Docs

Payment Integration

This guide covers integrating the MP Merchant checkout flow into web apps, mobile apps, and chat bots — including the hosted payment page, real-time SSE updates, and idempotent order creation.

How it works

The payment flow has four stages:

1
Create Order
POST /v1/orders
2
Customer Pays
Hosted checkout
3
Blockchain
TRC20 / ERC20
4
Webhook
payment.confirmed

Creating an order

Use POST /v1/orders to create a payment order. All amounts are in USDT.

Request parameters
amountrequired
string
Payment amount in USDT. Use a decimal string, e.g. "99.99".
currencyrequired
string
Must be "USDT".
networkrequired
string
Blockchain network: "TRC20" (TRON) or "ERC20" (Ethereum). TRC20 is recommended for lower fees.
description
string
Human-readable description shown on the checkout page.
return_url
string
URL to redirect the customer after payment. See security note below.
webhook_url
string
URL to receive payment event notifications. Overrides the default webhook URL for this order.
metadata
object
Arbitrary key-value pairs stored with the order. Returned in webhook events.
idempotency_key
string
Unique key to safely retry order creation without creating duplicates. See Idempotency.
tolerance_percent
number· default: 0.01
Acceptable underpayment tolerance as a decimal (0.01 = 1%). Orders within tolerance are marked PAID.

Handling the payment URL

Every order response includes a payment_url. This is the hosted checkout page where customers complete their payment. Choose the integration approach that fits your platform:

Integration
JavaScript
// Full redirect (recommended)
window.location.href = order.payment_url;

// Or open in a new tab
window.open(order.payment_url, '_blank');

// Or embed in an iframe
<iframe
  src={order.payment_url}
  width="100%"
  height="600"
  frameBorder="0"
/>

💡 MPChat deep links

When running inside the MPChat app, use the mpchat://pay?order={order_id} deep link to open the payment natively. Detect the user agent with navigator.userAgent.includes('MPChat').

Real-time updates (SSE)

Instead of polling, subscribe to order events via Server-Sent Events (SSE). The connection closes automatically when the order reaches a terminal state.

Subscribe to order events
const es = new EventSource(
  `/v1/orders/${orderId}/events`,
  { headers: { Authorization: `Bearer ${keyId}:${ts}:${sig}` } }
);

es.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('Status update:', data.status);

  if (['PAID', 'EXPIRED', 'UNDERPAID', 'FROZEN'].includes(data.status)) {
    es.close(); // Terminal state reached
  }
};

es.onerror = () => es.close();

return_url handling

After payment (or expiry), the customer is redirected to your return_url. The order ID is appended as a query parameter.

text
https://acme.com/payment/return?order_id=ord_01HQ...

🚨 Security: Never trust the redirect

The redirect to return_url is not a payment confirmation. A malicious user could craft a redirect URL manually. Always verify payment status server-side by calling GET /v1/orders/{id} or by relying on webhooks.

Idempotency

To safely retry order creation (e.g., after a network timeout), pass anidempotency_key. If you retry with the same key, the original order is returned without creating a duplicate.

bash
curl -X POST https://api.mpchat.com/v1/orders \
  -H "Authorization: Bearer ..." \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "99.99",
    "currency": "USDT",
    "network": "TRC20",
    "idempotency_key": "checkout_session_a1b2c3"
  }'

Idempotency keys are scoped to your merchant account and expire after 24 hours. Use a value unique to your checkout session (e.g., your internal cart or session ID).

Error handling with retries

Retry with exponential backoff
import time
import requests

def create_order_with_retry(payload, max_retries=3):
    for attempt in range(max_retries):
        try:
            response = requests.post(
                "https://api.mpchat.com/v1/orders",
                json=payload,
                headers={"Authorization": get_auth_header()},
                timeout=10,
            )
            response.raise_for_status()
            return response.json()
        except requests.exceptions.Timeout:
            if attempt == max_retries - 1:
                raise
            time.sleep(2 ** attempt)  # 1s, 2s, 4s
        except requests.exceptions.HTTPError as e:
            if e.response.status_code < 500:
                raise  # Client errors don't retry
            if attempt == max_retries - 1:
                raise
            time.sleep(2 ** attempt)

Next steps