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
In Open Platform โ Application details, select a backend/confidential client, choose merchant_payments.create (create merchant collections) and merchant_payments.read (read collection results), then submit an application or revision. Your account must own an active merchant; the platform binds that merchant automatically.
Existing applications do not need a new client: add the scopes to the original applicationโs details and select Submit change for review. Current configuration remains effective while the revision is pending. Administrators review applications in Admission review โ Application review, or use Adjust scopes with a review reason on an issued client. Scopes are never added automatically. A public client must first submit a confidential-client type revision; administrators cannot grant these server scopes directly to public clients.
After approval, check the effective scopes in the client configuration and obtain a new Client Credentials token with the two collection scopes. Existing tokens do not gain new scopes. Catalog-order payments.create and payments.read cannot replace them. 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. Payment flow and method selection
Your server submits the invoice amount and currency. TDCloud owns amount validation, available payment methods, provider adaptation and settlement. The default flow does not require channel discovery or a channel identifier: buyers choose a method at the returned checkout. currency specifies the invoice currency; the platform handles the actual payment method and wallet settlement.
Optional: restrict to one channel
Existing integrations that need a fixed method can use this directory and pass payment_no. A channelโs currency is its default display currency, not the precision authority for invoice amounts. Do not use it to convert an invoice amount.
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 | string | Yes for new integrations | Positive decimal amount string, such as "10.50"; the platform validates currency precision and converts it as described below |
currency | string | Yes | Invoice currency enabled by the platform, in uppercase; TDCloud determines available payment methods |
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 |
Optional fields can be omitted or set to null. Empty description, payment_no, and return_url values after trimming are treated as omitted. expires_in: 0 is invalid, while notify_enabled: false explicitly disables notifications for this order. Notification URLs and signing secrets must still be supplied together, and secrets must not contain whitespace.
Amount examples
Submit a decimal amount string. Merchants do not need to fetch currency precision or convert to minor units. TDCloud validates and converts exactly using its currency rules; it never rounds an amount to fit the currency.
| Invoice amount | Request currency | Request amount | Response amount |
|---|---|---|---|
| 10.50 USD | USD | "10.50" | "10.50" |
| 10.50 TDC | TDC | "10.50" | "10.50000000" |
| 100 JPY (when enabled) | JPY | "100" | "100" |
amountmust be a JSON string, at most 64 bytes, containing digits with an optional single decimal point. Signs, whitespace, commas, currency symbols, scientific notation,.5and1.are invalid. Zero and negative amounts cannot create an order.- Nonzero fractional digits cannot exceed the platformโs precision for that currency. For example, USD
"10.001"and JPY"100.5"are rejected. Redundant trailing zeros are accepted:"10.5","10.50"and"10.5000"represent the same amount. - The platform checks representable amount limits, currency availability and payment methods. Failures use
invalid_amount,invalid_amount_precision,currency_unavailableorchannel_unavailable, respectively; it does not silently change the amount or currency. - Creation, detail, list and newly generated status notifications return a normalized
amountstring at the orderโs stored precision. Compareamountas a decimal value and verifycurrency. String length is not an amount comparison; do not use binary floating point or live rates to reprice historical orders.
Legacy compatibility
Existing integrations can continue sending only a positive integer amount_minor, in invoice-currency minor units, at most 9007199254740991 and within platform amount limits. Submit exactly one of amount or amount_minor: even zero or null in the other field is rejected. New integrations use amount.
Responses retain amount_minor, currency_decimals and actual channel information for legacy reconciliation. New integrations do not need them for conversion before creation. Retrying an equivalent amount across representations with the same idempotency key returns the original order. Previously stored notification bodies remain unchanged on retries; support legacy amount fields for historical notifications or query the order for amount.
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":"10.50","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": "10.50",
"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, merchant order reference, collection identifier, invoice amount, countdown and methods. TDC payments also show the exact debit converted at the creation-time price and the signed-in buyerโs available balance; the invoice amount remains unchanged. Selecting a method displays its subtotal, payment fee and payer total for review. Only explicit confirmation creates or resumes the payment page, or debits TDC balance. Switching methods alone does not start payment or lock a channel. Payment event IDs, provider trade numbers and merchant settlement data remain in merchant-authenticated lookup and notifications. Method identifiers are specific to the order; submitting arbitrary channel IDs cannot grant payment access.
A definitive rejection of initial checkout creation releases reservations and permits another method. Timeouts, server failures and retries after an earlier ambiguous response keep the original attempt locked to avoid a second payable session.
Idempotency and retries
The same merchant, idempotency_key and normalized parameters return the same order. Amounts are normalized by value; redundant trailing zeros do not change the request. Retries use the original orderโs stored precision even if the currency rules or availability have changed. 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. When a creation result is unknown and no session identifier is available, TDCloud reconciles the original provider records. The order becomesexpiredonly after confirming no payment, with a status notification according to the orderโs notification configuration. - 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.
Once the original collection is confirmed expired, create a new collection attempt for the same merchant business order using a new idempotency_key, preserving its external_reference, amount and currency. A new business order is not required; retain the original collection and historical payment associations.
Payment confirmation after expiry
expires_at limits new payment starts; it is not a cutoff for accepting evidence of payments that occurred. Provider payment and TDCloud session closure can race, and asynchronous payments can be confirmed later. After TDCloud verifies collection and completes the corresponding accounting, an order previously reported as expired can still become paid. A successfully closed provider session cannot accept further checkout payments, but late payment results must still be checked.
- Do not treat
expiredas a permanent state that rejects subsequentpaid, or delete the association between the collection and your business order. Both notifications and historical queries must supportexpired โ paid. - The new
paidstate has a higherstatus_versionand produces a new status notification when notifications are enabled for that order. With notifications disabled or delivery unsuccessful, server queries and periodic reconciliation can discover the change; unlimited polling is not required. - An older
expirednotification must not overwrite a newerpaidstate for the same collection. Comparestatus_versiononly within one TDCloud collection order, never across separate payment attempts. - Keep earlier attempts in reconciliation after creating a new one. One business order can have multiple collection orders. Record each confirmed payment separately by its stable payment
event_id, while deduplicating fulfillment, subscriptions and entitlements by your business order. Different payment events do not authorize duplicate fulfillment. - If a business order is already closed or fulfilled when another payment is confirmed, retain the payment fact and route it through your refund or manual resolution process. Do not silently discard it. TDCloud does not automatically refund because a business order is closed or a payment is confirmed late.
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": "10.50",
"currency": "USD",
"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": "10.50",
"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.
For example, expired at version 2 can be followed by paid at version 3 for the same collection. A later redelivery of version 2 must not restore the expired state. Notifications and queries must share payment-accounting and business-fulfillment deduplication so concurrent processing or replay cannot fulfill twice. A newer confirmed payment must still be recorded and handled even if your business order has timed out.
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 net settlement after merchant fees, as an eight-decimal fixed-point integer; not payment evidence |
return_url | string, optional | Stored merchant destination |
| fee_status | string | unselected, quoted, or legacy (existing order fees) |
| fee_version | integer | Fee snapshot version; existing orders use 0 |
| payment_amount / payment_currency | string, optional | Buyer total including buyer fees, and actual payment currency; decimal strings |
| buyer_fee | string, optional | Buyer surcharge in payment_currency, excluded from merchant income |
| merchant_fee | string, optional | Merchant fee in payment_currency |
| merchant_fee_tdc | string | Merchant fee in TDC, as an eight-decimal fixed-point integer |
| gross_settlement_tdc | string | Invoice TDC conversion before merchant fees; an eight-decimal fixed-point integer |
Confirmed-payment response example (verification fields only):
{
"order_no": "MP_EXAMPLE",
"external_reference": "merchant-order-123",
"payment_no": "CHANNEL_NO",
"amount": "10.50",
"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
Invoice amount, buyer total and net settlement
amount, amount_minor and currency always identify the original business invoice. Choosing a payment method does not change them. Plugins still validate their business order against these fields. payment_amount and payment_currency identify the buyer total, including applicable fees and any TDC conversion.
Platform channels use both configured fee groups: buyer fees increase the payer total; merchant fees reduce the invoiceโs TDC conversion. Only the net amount credits the merchant wallet and enters its hold policy. Buyer fees are not merchant income. Checkout shows method totals and buyer fees before redirecting; merchant details show gross conversion, merchant fees and net settlement.
Fees use the actual payment currency. Add the fixed fee to the fee base before computing its percentage; the percentage cap limits only the percentage component. Buyer fees use the invoice payment subtotal; platform-channel merchant fees use the buyer total. Round fees to the currencyโs smallest unit, with a one-unit minimum for positive fees. An all-zero configuration remains free. For TDC, first convert the invoice at its saved rate, then apply the TDC channel fees. A method cannot be used if merchant fees consume all invoice proceeds.
For example, a CNY 10.00 invoice at a saved rate of 1 CNY = 1 TDC, with both groups set to fixed 4 plus 4% and neither cap reached:
| Item | Amount |
|---|---|
| Invoice | CNY 10.00 |
| Buyer fee | CNY 4.56 |
| Buyer total | CNY 14.56 |
| Gross invoice conversion | 10.00 TDC |
| Merchant fee | 4.74 TDC |
| Net settlement | 5.26 TDC |
This example is not a fixed platform tariff or exchange rate. Amounts, fee rules, conversion and holds are saved at creation. Selecting a method previews its saved quote; payment confirmation binds the method. Until selection there is no single payer total or net settlement quote. Existing orders retain their original payment, verification and settlement amounts; current fees are never retroactively charged.
Invoice fields remain unchanged in lookup and notifications. TDCloud verifies providers against the buyer total including fees. Refunds and chargebacks also verify the original buyer total. A full refund may exceed net frozen proceeds; guarantee and other funds cover the difference under platform rules. Collected fees are not returned. Additional external nonrefundable costs must represent new losses, not charge again for fees already covered by the buyer-total difference.
Platform channels convert at the creation-time rate, deduct merchant fees and post net 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, legacy amount fields and list filters |
| 400 | invalid_amount | Use a valid positive decimal string within platform limits, with only one amount representation; returns field: "amount" and a description |
| 400 | invalid_amount_precision | Nonzero fractional digits exceed currency precision; no rounding is performed; returns field: "amount" and a description |
| 422 | currency_unavailable | Invoice currency is unknown or disabled; returns field: "currency" and a description |
| 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 | self_payment_not_allowed | Merchant owners cannot pay their own collections with TDC balance; use a different payer or method |
| 422 | insufficient_balance | Buyer available TDC balance is insufficient; top up or choose another method |
| 422 | provider_checkout_rejected | Provider definitively rejected initial checkout creation; reservations are released and another method may be selected |
| 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 original order and attempt; this is not a successful payment |
| 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.