General merchant collection
When your system owns its orders, subscriptions and fulfillment, your merchant server can create a TDCloud collection order for a dynamic amount and send the buyer to its checkout. No TDCloud catalog product is required. TDCloud confirms payment and settlement; your system updates its own business orders and entitlements.
1. Prepare merchant server credentials
Use a confidential client bound to your active merchant, with approved merchant_payments.create and merchant_payments.read scopes. Existing applications do not gain these scopes automatically; request a platform scope adjustment. All OpenAPI routes on this page require a client_credentials token, not a user-authorized token. Do not submit a buyer ID or merchant ID.
Use the complete service URLs assigned in Developer Center; see Quickstart. Keep the Secret and Access Token on your server.
curl -X POST "{TOKEN_ENDPOINT}" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_id={CLIENT_ID}" \
--data-urlencode "client_secret={CLIENT_SECRET}" \
--data-urlencode "scope=merchant_payments.create merchant_payments.read"Use the returned access_token, effective scope and expires_in. Client Credentials does not issue a user Refresh Token; obtain another token with server credentials after expiry. Public clients and clients without a merchant binding cannot use this flow.
2. Discover available channels
GET {API_BASE_URL}/openapi/v1/merchant-payment-channels
Scope: merchant_payments.read. HTTP 200 returns {"items": [...]}; the array is empty when no channel is available.
curl "{API_BASE_URL}/openapi/v1/merchant-payment-channels" \
-H "Authorization: Bearer {ACCESS_TOKEN}"| Field | Type | Meaning |
|---|---|---|
payment_no | string | Actual channel identifier for creation, not its display name |
name | string | Channel name |
provider | string | Currently stripe or dog-coin (TDC) |
owner | string | platform or merchant |
currency | string | Payment currency |
login_required | boolean | Whether the channel itself requires sign-in; checkout also requires sign-in if the merchant disables guest purchases |
test_mode | boolean | Whether this is an explicitly assigned Stripe test channel |
Availability depends on merchant ownership, enablement and debug grants. Initial providers are TDC and Stripe with a signing Webhook configured. TDC always requires the buyer to sign in to TDCloud. The merchant controls guest purchases for other channels. This directory is not a channel-discovery or quote API for catalog orders.
3. Create a collection order
POST {API_BASE_URL}/openapi/v1/merchant-payments
Scope: merchant_payments.create. Headers: Authorization: Bearer {ACCESS_TOKEN} and Content-Type: application/json.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
external_reference | string | Yes | Your business-order reference; nonempty after trimming, at most 191 bytes, immutable |
idempotency_key | string | Yes | Retry key for this creation attempt, at most 128 bytes, unique within the merchant |
amount_minor | integer | Yes | Positive integer in payment-currency minor units; at most 9007199254740991 and representable at platform internal precision |
currency | string | Yes | Enabled, channel-supported currency; use its uppercase code |
payment_no | string | No | Restricts the order to this channel when supplied; omit or leave empty to let the buyer choose an available method at checkout |
description | string | No | Buyer-visible payment description; at most 500 bytes, trimmed. Do not include internal IDs, keys or sensitive information |
expires_in | integer | No | Fixed payment lifetime in seconds, 60β7200; defaults to 1800 (30 minutes) |
notify_enabled | boolean | No | Omit to use explicit parameters or merchant defaults; false disables this order and cannot be combined with URL/secret; true requires a valid notification configuration |
notify_url | string | No | Public HTTPS notification endpoint; supply with notify_secret, see status notifications |
notify_secret | string | No | Independent random signing secret, 32β256 bytes without whitespace |
return_url | string | No | Absolute HTTPS destination after confirmed payment, at most 2048 bytes; see below |
Amount examples
This endpoint uses amount_minor, which differs from the fixed eight-decimal amounts used by catalog-order APIs.
| Payment amount | currency | amount_minor | Returned currency_decimals |
|---|---|---|---|
| 10.50 USD | USD | 1050 | 2 |
| 10.50 TDC | TDC | 1050000000 | 8 |
Convert with integer or decimal arithmetic using the currency precision, rather than rounding floating-point multiplication. Verify returned amount, currency and precision after creation; do not reprice an existing order using live rates.
curl -X POST "{API_BASE_URL}/openapi/v1/merchant-payments" \
-H "Authorization: Bearer {ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
--data '{"external_reference":"merchant-order-123","idempotency_key":"merchant-order-123-attempt-1","amount_minor":1050,"currency":"USD","description":"Order description","expires_in":1800,"return_url":"https://merchant.example/orders/merchant-order-123?source=tdcloud#payment"}'Success: HTTP 201. An identical retry also returns 201 and the same order.
{
"order_no": "MP_EXAMPLE",
"external_reference": "merchant-order-123",
"payment_no": "",
"amount_minor": 1050,
"currency": "USD",
"currency_decimals": 2,
"status": "pending",
"test_mode": false,
"checkout_url": "https://tdcloud.cc/checkout/merchant/MP_EXAMPLE?token=CHECKOUT_TOKEN",
"return_url": "https://merchant.example/orders/merchant-order-123?source=tdcloud#payment",
"expires_at": "2026-10-08T14:00:00+08:00"
}Identifiers, URLs and dates above are examples. Save the mapping between order_no and your business order, then send the buyer to the actual returned checkout_url. It is not payment evidence; do not expose its token in logs. expires_at is fixed at creation; provider sessions, page refreshes and retries cannot extend it.
Buyer payment-method selection
Omit payment_no for the default checkout flow. TDCloud snapshots available channels and their amount, exchange-rate and fee rules, then returns one checkout URL. The buyer chooses an available method; a single available method is shown directly. Supply payment_no to restrict an order to one channel. Historical fixed-channel orders keep their original method.
Checkout shows only merchant-eligible methods that satisfy the actual paying accountβs authorization. Methods requiring sign-in display a login prompt; guests cannot spend TDC balances. TDC can pay orders in other enabled currencies at the saved creation-time rate, with the actual TDC charge displayed. The original order amount and currency remain unchanged. Merchant-owned channel fees are checked during creation and checked/reserved again when starting payment.
The first provider attempt locks the selected method. Refreshes and repeated clicks resume the same attempt; an uncertain provider response cannot be bypassed by choosing another method. Selecting or refreshing never extends expiry. Before selection, server-side payment_no is empty and test_mode is false; lookup reports the actual channel and mode after selection without changing the original requestβs idempotency contract.
Buyer checkout displays merchant name, payment description, amount, countdown and methods. External references, TDCloud reconciliation IDs, payment events, provider trade numbers and wallet settlement data remain in merchant-authenticated lookup and notifications. Method identifiers are specific to the order; submitting arbitrary channel IDs cannot grant payment access.
Idempotency and retries
The same merchant, idempotency_key and normalized parameters return the same order. Changing amount, currency, channel, external reference, description, lifetime or return URL returns HTTP 409 payment_conflict. Merchant namespaces are separate; external_reference alone is not a deduplication key.
After a timeout, retry with the original key and parameters. A missing response does not justify creating another payable order with a new key. Reconcile the original order and provider result before starting a genuinely new payment attempt.
Payment timeout
expires_inis a creation-time lifetime in seconds: 60β7200 (1 minuteβ2 hours), default 1800 (30 minutes). Explicit1800equals omission; changing to another lifetime for the same key returns409 payment_conflict.- At the fixed
expires_at, checkout starts are rejected with410 payment_expired. Same-key retries return the original order and deadline. The cap applies to new orders; historical orders retain their original deadline. Queries and reconciliation remain available. - Orders without a provider attempt become
expiredwhen read, listed, opened at checkout or swept by the worker. Started attempts first becomereview; the worker checks payment, closes unpaid open Stripe sessions, and releases reserved fees and credit exactly once only after verifying provider expiry. - Asynchronous payments, provider lookup/closure failures and unknown session-creation responses remain
review. Never treat uncertainty as non-payment. Verified late payments becomepaidafter wallet/accounting confirmation. Unknown responses without a session identifier need administrator reconciliation in Stripe using the TDCloud order number. - The worker sweeps every minute and retries provider failures. Stripe requires at least 30 minutes for a session, so its deadline can be later than the collection deadline; TDCloud actively closes the session without extending the collection order. Local expiry does not guarantee simultaneous closure of an external provider link. See Stripe session expiryΒ and expiring a sessionΒ .
Checkout displays a countdown and removes the payment action at expiry. Keep querying and reconciling the original order for review or suspected late payment; confirm the previous attempt has ended before creating another.
4. Integrate the payment return URL
The merchant server supplies return_url during creation, after which it is immutable. Leading/trailing whitespace is trimmed. It must be an absolute HTTPS URL without credentials, backslashes or unencoded whitespace/control characters. Omitted, empty or absent on historical orders means the existing checkout flow remains.
This differs from OAuth redirect_uri: it does not need OAuth callback registration and may contain an order-specific path, query and fragment. Adding return_url or status=paid to the checkout browser URL does not override the stored destination or confirm payment.
The flow is:
- The buyer visits TDCloud
checkout_url, signing in or paying as a guest according to merchant policy. - Stripe completion or cancellation first returns to TDCloud checkout. TDC also confirms payment there.
- Checkout queries the server. Only
paidtriggers automatic navigation to the storedreturn_url, with a βReturn to merchantβ link retained. - Pending asynchronous payments continue to be queried. Cancellation, expiry, refunds and chargebacks do not trigger a successful return. Browser cancellation does not guarantee an immediate terminal order status.
- Your return page calls your server, which queries TDCloud, checks amount, currency and stable
event_id, and updates the business order idempotently.
The original path, query and fragment are preserved. TDCloud does not append a token, order number or payment status. Include your own business reference in the supplied URL and use the return only to initiate a server query. A success page, browser parameters or a created provider session is not fulfillment evidence.
5. Server-side status notifications
Notifications are optional and off by default. Server-side lookup remains available. Browser return URLs and server notifications are independent; neither is required.
Merchant defaults
Use Merchant Center β Payment and settlement β Collection status notifications to configure a public HTTPS endpoint. Saving an endpoint generates a separate signing secret, shown only once. Store it in your receiver, implement verification, then enable default notifications. Reading settings never reveals the secret; rotate it if lost.
| Creation parameters | Notification behavior |
|---|---|
| No notification parameters | Use enabled merchant defaults; otherwise do not send |
notify_enabled: false | Disable this order even if defaults are enabled; do not also supply a URL or secret |
notify_enabled: true | Use an explicit URL/secret pair or enabled merchant defaults; reject creation if neither is available |
Paired notify_url / notify_secret | Override merchant defaults with the supplied destination and key |
The effective URL and key are fixed at creation. Changing, disabling or rotating merchant defaults affects new orders only. Existing orders and replays retain their original configuration; keep old receiver keys until historical notifications finish. Retrying an existing idempotency key never rereads defaults or changes the order.
Per-order configuration
Supply both notify_url and notify_secret at creation to receive HTTPS POST notifications. The endpoint must be public HTTPS, at most 2048 bytes, without credentials, fragments, private-network targets or redirects. Generate an independent random signing secret of 32β256 bytes without whitespace; do not use the OAuth Client Secret. Both fields participate in idempotency and cannot be changed after creation.
{
"external_reference": "merchant-order-123",
"idempotency_key": "merchant-order-123-attempt-1",
"amount_minor": 1050,
"currency": "USD",
"payment_no": "CHANNEL_NO",
"notify_url": "https://merchant.example/hooks/tdcloud",
"notify_secret": "MERCHANT_GENERATED_RANDOM_SECRET_AT_LEAST_32_BYTES"
}If merchant defaults are disabled, omitting both fields sends no notifications. If defaults are enabled, use notify_enabled: false to disable notifications for this order. The secret never appears in creation, query, checkout or delivery-record responses.
Payload and signature verification
Each status change emits merchant_payment.status_changed, including initial pending, confirmed paid, review, expired, refunded and chargeback. Accounting, order status and the notification event commit together; delivery failure does not reverse a confirmed payment.
{
"id": "MN_NOTIFICATION_EXAMPLE",
"type": "merchant_payment.status_changed",
"api_version": 1,
"created_at": "2026-10-08T07:45:00Z",
"data": {
"order_no": "MP_EXAMPLE",
"external_reference": "merchant-order-123",
"payment_no": "CHANNEL_NO",
"amount_minor": 1050,
"currency": "USD",
"currency_decimals": 2,
"status": "paid",
"status_version": 2,
"test_mode": false,
"expires_at": "2026-10-08T08:00:00Z",
"event_id": "MPE_PAYMENT_EXAMPLE",
"provider_trade_no": "cs_EXAMPLE",
"occurred_at": "2026-10-08T07:45:00Z"
}
}data is the collection snapshot at that transition, without checkout tokens, return destinations or notification secrets. Envelope id identifies the notification; data.event_id identifies the confirmed payment. created_at is notification creation time; payment time is data.occurred_at.
| Header | Meaning |
|---|---|
X-TDCloud-Event-ID | Notification identifier matching body id |
X-TDCloud-Timestamp | Unix seconds for this delivery attempt |
X-TDCloud-Signature | sha256= followed by the lowercase hexadecimal HMAC-SHA256 digest |
Sign timestamp + "." + raw HTTP request body bytes using the signing key effective at creation (the merchant default key or explicit notify_secret). Compare signatures in constant time and reject timestamps more than 5 minutes from the current time. Synchronize server clocks and do not reserialize JSON before verification. Python example:
import hashlib
import hmac
import time
# raw_body is the untouched HTTP request body; secret is stored server-side.
timestamp = request.headers["X-TDCloud-Timestamp"]
provided = request.headers["X-TDCloud-Signature"]
expected = "sha256=" + hmac.new(
secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256
).hexdigest()
if abs(time.time() - int(timestamp)) > 300 or not hmac.compare_digest(expected, provided):
raise ValueError("Invalid TDCloud notification")After verification, match the reference, amount, currency and test_mode, and deduplicate by notification id. Deliveries may repeat or arrive out of order. Per-order status_version increases monotonically; older versions must not overwrite newer state. Queries also return status_version. Payment accounting remains idempotent by data.event_id; only paid confirms payment. Refunds and chargebacks retain the original payment event identifier.
Acknowledgment, retries and replay
Durably store the notification before returning HTTP 2xx; the response body is ignored. Each attempt waits at most 8 seconds. Timeouts, connection failures, 3xx, 4xx and 5xx retry, without following redirects. After the initial asynchronous attempt, retry delays are 1 minute, 5 minutes, 15 minutes, 30 minutes, 1 hour, 2 hours, 4 hours, 6 hours and 8 hours: at most 10 automatic attempts. Interrupted workers can recover. Retries retain the identifier and body, with a fresh signed timestamp.
Merchants can also enter a collection order number in Merchant Center β Payment and settlement β Collection notification records to view delivery results and recent attempts, and replay an exhausted event. The page only accesses the current merchantβs orders.
| Method and path | Scope | Purpose |
|---|---|---|
GET /openapi/v1/merchant-payments/{order_no}/notifications | merchant_payments.read | This merchantβs delivery records; HTTP 200 returns {"items": [...]} |
POST /openapi/v1/merchant-payments/{order_no}/notifications/{event_id}/retry | merchant_payments.create | Requeue a notification after automatic attempts are exhausted; HTTP 202 returns {"accepted": true} |
Records are newest status version first. limit defaults to 20, permits 1β100 and falls back to 20 outside that range. Fields include notification event_id, status_version, the event snapshot, delivery_status (pending / delivered / failed), cumulative attempts, next_attempt_at, last_http_status, last_error, and the latest 20 delivery_attempts. A missing finished_at indicates an interrupted or ongoing attempt. Replay retains the event and order state; requests for delivered or pending events do not enqueue extra copies. Replay is limited to 30 requests per client per minute.
Use notifications together with queries and periodic reconciliation. Exhausted delivery attempts do not mean payment failure.
6. Query and reconcile on the server
| Method and path | Scope | Purpose |
|---|---|---|
GET /openapi/v1/merchant-payments/{order_no} | merchant_payments.read | Read one order belonging to your merchant; HTTP 200 returns an order object |
GET /openapi/v1/merchant-payments | merchant_payments.read | Filter by your merchantβs external reference or TDCloud order number; HTTP 200 returns {"items": [...]} |
The list requires at least one of external_reference and order_no. Supplying both requires both to match. limit defaults to 20, accepts 1β100, and falls back to 20 for invalid/out-of-range values. Results are newest first, without page numbers, totals or full merchant-order pagination.
curl "{API_BASE_URL}/openapi/v1/merchant-payments/{ORDER_NO}" \
-H "Authorization: Bearer {ACCESS_TOKEN}"
curl --get "{API_BASE_URL}/openapi/v1/merchant-payments" \
-H "Authorization: Bearer {ACCESS_TOKEN}" \
--data-urlencode "external_reference=merchant-order-123" \
--data-urlencode "limit=20"An order includes the creation-response fields. Additional payment fields are:
| Field | Type | Meaning |
|---|---|---|
status | string | Collection status, as below |
status_version | integer | Per-order status version; historical unchanged orders may have 0 |
event_id | string, optional | Stable, unique confirmed-payment event identifier for idempotent merchant posting |
provider_trade_no | string, optional | Provider transaction identifier; TDC uses TDC:{order_no} |
occurred_at | string or null | Confirmed payment time in RFC 3339; preserve its supplied time zone |
settlement_tdc | string, optional | Platform-channel locked settlement TDC as an eight-decimal fixed-point integer; not payment evidence |
return_url | string, optional | Stored merchant destination |
Confirmed-payment response example (verification fields only):
{
"order_no": "MP_EXAMPLE",
"external_reference": "merchant-order-123",
"payment_no": "CHANNEL_NO",
"amount_minor": 1050,
"currency": "USD",
"currency_decimals": 2,
"status": "paid",
"event_id": "MPE_EXAMPLE",
"provider_trade_no": "pi_EXAMPLE",
"occurred_at": "2026-10-08T13:45:00+08:00"
}expires_at is also RFC 3339, not Unix seconds. Preserve identifiers as strings. Status notifications can trigger merchant processing; retain bounded server queries and periodic reconciliation.
| Status | Meaning and action |
|---|---|
pending | Unresolved; keep checking, do not fulfill or infer failure from your local timer |
paid | Payment and merchant accounting are confirmed; check reference, amount and currency and post once by event_id |
review | Deadline elapsed but provider payment or reconciliation remains unresolved; unknown creation responses require platform reconciliation, never collect again with a new key |
expired | Expired after checking for payment; a verified late payment can still transition to paid, so retain exception reconciliation |
refunded | Confirmed refund, original payment event retained; apply your business refund/entitlement policy |
chargeback | Confirmed chargeback, original payment event retained; handle disputes and business state |
Do not apply catalog ordersβ numeric status mapping. There is currently no generic failed/cancelled state or public cancellation API. Preserve unknown future statuses for reconciliation rather than treating them as paid.
7. Settlement, fees and debugging
Platform channels convert at the creation-time rate and post TDC to the merchant wallet. The platform assigns each merchant a 0β30 day hold; zero allows immediate availability. Merchant-owned channels receive funds themselves, without duplicate TDCloud wallet credit. Platform fees are checked at creation and checked/reserved again before a payment link is obtained, using available TDC followed by approved credit. Insufficient funding rejects the transaction.
A complaint first prevents that orderβs frozen sales funds from being released. Confirmed losses use that orderβs frozen funds, guarantee deposit, available balance and outstanding recovery balance in sequence. Nonrefundable fees can make the loss exceed the frozen amount. The platform verifies refunds, chargebacks and complaints; changing your business order does not perform platform accounting. No merchant self-service refund/withdrawal OpenAPI is documented here. See Payment and settlement.
test_mode: true identifies a Stripe test channel explicitly assigned by an administrator. The buyer is not charged real Stripe funds, but normal TDCloud order, accounting, wallet TDC and hold flows still run. Administrator identity alone grants no access. Test accounts, products and fulfillment are maintained by the administrator.
A merchant-only configuration allows all buyers at those merchants on the selected test channel. User-only allows those paying users at any merchant. When both lists are populated, both conditions must match. Empty lists grant no access when neither is populated; multiple IDs match any entry within each list.
Server-side channel discovery and creation have no buyer identity and check merchant eligibility first. Test channels with a paying-user restriction return login_required: true in the directory. Successful creation does not authorize arbitrary buyers. Checkout checks the actual TDCloud authenticated account and rechecks before issuing or resuming a provider link; a claimed buyer ID cannot grant debug access. User restrictions require sign-in even if guest checkout is enabled. Previously issued sessions may succeed after revocation; verified receipts continue to settle normally.
8. Errors, limits and integration checks
Errors include at least a string error. HTTP 202 provider_result_pending is not payment success. Branch on HTTP status and error code, not translated text.
| HTTP | error | Action |
|---|---|---|
| 400 | invalid_notify_config | Enabling an order requires a URL/secret pair or enabled merchant defaults; disabling cannot include a URL or secret |
| 400 | invalid_notify_url | Use a public HTTPS endpoint without credentials, fragments or private targets |
| 400 | invalid_notify_secret | Supply an independent random 32β256-byte secret without whitespace |
| 503 | notification_unavailable | Retry later with the original idempotency key |
| 400 | invalid_request | Check parameters, amount and list filters |
| 400 | invalid_expires_in | Use an integer lifetime of 60β7200 seconds; returns error_description and field: "expires_in" |
| 400 | invalid_return_url | Fix the HTTPS destination; also returns error_description and field: "return_url" |
| 401 | invalid_token | Obtain a valid OAuth token |
| 403 | insufficient_scope, client_credentials_required | Check approved scopes and token actor |
| 403 | merchant_inactive | Request re-enablement of the bound merchant |
| 403 | login_required | The buyer must sign in at checkout |
| 403 | payment_debug_access_denied | Check the test-channel grant and enablement |
| 404 | payment_not_found | Check identifier and merchant ownership without inferring another merchantβs resources |
| 409 | payment_conflict | Parameters conflict for the original key; checkout state conflicts can also use this code |
| 410 | payment_expired | No further payment starts; query and reconcile the original order before retrying |
| 409 | provider_result_mismatch | Ask the platform to reconcile session, amount and currency |
| 422 | channel_unavailable, currency_rate_unavailable | Check channel, currency and platform rate |
| 422 | merchant_fee_funding_unavailable | Check configured fees, available wallet funds and approved credit |
| 202 | provider_result_pending | Preserve and reconcile the existing order |
| 429 | rate_limited | Wait according to Retry-After |
| 503 | rate_limit_unavailable | Rate-limit service is unavailable; use bounded backoff |
| 500 | server_error | Keep sanitized context and contact the platform; retry creation with its original idempotency key |
Creation allows 60 requests per client per minute. Detail, filtered list and channel directory share 300 requests per client per minute. Checkout limits by IP are 180 reads and 30 starts per minute.
Check dynamic amounts, payment returns, identical retries and conflicting parameters, merchant permissions, guest checkout, payment timeout and asynchronous results. Update each business order only once per stable event_id, and handle refunds and chargebacks in your business system. TDCloud verifies Stripe Webhooks.