Payment Integration Checklist and Recovery

Implement atomic webhook fulfilment, recover missed payment events, manage API credentials and check your integration before launch.

What authorizes fulfilment

A confirmed TRON transaction is blockchain evidence; it is not by itself a match to your order. Dedyx checks the asset, recipient, exact amount and payment window. A checkout status is useful customer feedback, but a screenshot, browser redirect or public-token response must not authorize a business action.

Your backend verifies the signed PAID webhook and checks it against the saved payment_id and exact expected_amount. Alternatively, it reconciles through the authenticated payment details endpoint and verifies PAID, payment_id, merchant_wallet and amount. Both paths must use the same durable order-level fulfilment guard.

Atomic event recording and local fulfilment

Download the PostgreSQL schema and the asyncpg transaction helper. These tables belong to your merchant database. Adapt the local entitlement to your product; the example does not modify Dedyx tables or connect at import.

PostgreSQL · merchant-side example tables
-- Example tables in YOUR merchant database, not the Dedyx service database.
CREATE TABLE merchant_orders (
    order_id text PRIMARY KEY,
    payment_id text UNIQUE NOT NULL,
    merchant_wallet text NOT NULL,
    expected_amount numeric(10,2) NOT NULL CHECK (expected_amount > 0),
    fulfilment_mode text NOT NULL CHECK (fulfilment_mode IN ('local', 'external')),
    accepted_at timestamptz
);
CREATE TABLE merchant_payment_events (
    event_id text PRIMARY KEY,
    order_id text NOT NULL REFERENCES merchant_orders(order_id),
    received_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE merchant_entitlements (
    order_id text PRIMARY KEY REFERENCES merchant_orders(order_id),
    granted_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE merchant_delivery_jobs (
    order_id text PRIMARY KEY REFERENCES merchant_orders(order_id),
    idempotency_key text UNIQUE NOT NULL,
    delivered_at timestamptz
);
Python + asyncpg · verified input only
"""Merchant PostgreSQL + asyncpg example. No connection or API call at import.

Call only after raw-body signature verification OR an authenticated details GET.
The connection belongs to your merchant database; install the adjacent SQL there.
"""
from decimal import Decimal, InvalidOperation


async def accept_paid(conn, payment, *, event_id=None):
    """Commit before returning 2xx; let DB errors cause a retryable non-2xx.

    event_id: verified webhook ID; None: reconciliation via authenticated API.
    Validate header/payload event IDs before calling this function.
    For reconciliation also require merchant_wallet from the details response.
    """
    if payment.get('status') != 'PAID':
        raise ValueError('Payment is not PAID')
    if event_id is not None and (not isinstance(event_id, str) or not event_id):
        raise ValueError('Invalid event ID')
    if event_id is not None and payment.get('event_id') != event_id:
        raise ValueError('Event ID mismatch')
    try:
        amount = Decimal(str(payment['expected_amount']))
    except (KeyError, InvalidOperation):
        raise ValueError('Invalid amount') from None
    if not amount.is_finite() or amount <= 0:
        raise ValueError('Invalid amount')
    async with conn.transaction(isolation='read_committed'):
        # Lock the order: distinct events and reconciliation serialize too.
        order = await conn.fetchrow(
            'SELECT * FROM merchant_orders WHERE payment_id=$1 FOR UPDATE',
            payment.get('payment_id'),
        )
        if order is None or amount != order['expected_amount']:
            raise ValueError('Unknown order or amount mismatch')
        if event_id is None or 'merchant_wallet' in payment:
            if payment.get('merchant_wallet') != order['merchant_wallet']:
                raise ValueError('Wallet mismatch')
        if event_id is not None:
            inserted = await conn.fetchval(
                'INSERT INTO merchant_payment_events(event_id,order_id) '
                'VALUES($1,$2) ON CONFLICT(event_id) DO NOTHING RETURNING event_id',
                event_id, order['order_id'],
            )
            if inserted is None:
                previous = await conn.fetchval(
                    'SELECT order_id FROM merchant_payment_events WHERE event_id=$1',
                    event_id,
                )
                if previous != order['order_id']:
                    raise ValueError('Event belongs to another order')
        if order['accepted_at'] is not None:
            return 'already_accepted'
        if order['fulfilment_mode'] == 'local':
            # Local business action in the SAME transaction as event insertion.
            await conn.execute(
                'INSERT INTO merchant_entitlements(order_id) VALUES($1)',
                order['order_id'],
            )
        else:
            # No external network call inside this transaction.
            await conn.execute(
                'INSERT INTO merchant_delivery_jobs(order_id,idempotency_key) '
                'VALUES($1,$2)', order['order_id'], 'fulfil:' + order['order_id'],
            )
        await conn.execute(
            'UPDATE merchant_orders SET accepted_at=now() WHERE order_id=$1',
            order['order_id'],
        )
    return 'accepted'

Capture the raw body, verify the signature with the Python or Node.js helper, compare header/payload event IDs, then call accept_paid with the verified payload and event_id. Return 2xx only after the function commits. A verified duplicate returns 2xx. Reject invalid signatures; an unknown order or mismatched amount needs investigation, not a successful acknowledgement. Database failures must produce a retryable non-2xx response.

A row lock serializes deliveries for the same order. The event record, local access grant and accepted_at commit together. A crash before commit rolls everything back; a crash after commit but before the HTTP response causes a harmless repeat. A different event ID for the same accepted order cannot grant access twice. Concurrent reconciliation uses the same lock and guard.

If the order has not been saved yet when a webhook arrives, persist the order association or reconcile it before acknowledging. Do not mark an unknown payment as delivered.

External fulfilment needs an idempotent worker

For fulfilment_mode=external, the same transaction inserts a delivery job with a stable fulfil:ORDER_ID key. It records acceptance of the payment, not completion of external delivery. A worker must claim jobs with a lease or row lock, call the destination with that same idempotency key and mark delivered_at only after confirmed success.

If the worker crashes after the external action but before recording success, it retries with the same key. The destination must deduplicate that key or provide a way to reconcile the action. A database transaction alone cannot guarantee one external side effect. If the destination cannot do either, send uncertain jobs for manual reconciliation instead of blindly repeating delivery. Keep customer-facing delivery status separate from payment acceptance.

Recover a missed webhook

Read authenticated payment details
curl https://api.dedyx.com/api/v1/payments/YOUR_PAYMENT_ID/details \
  -H "X-API-Key: YOUR_API_KEY"

Start with your stored payment_id. If details are PAID, compare the wallet and exact amount with your order, then call the same accept_paid helper with event_id=None. Do not invent a webhook event ID. A later webhook still records its real event ID and cannot repeat the business action.

A failed webhook does not cancel a payment. Delivery normally has up to eight attempts. Read the state with GET /api/v1/payments/{payment_id}/webhooks. After repairing your receiver, POST /api/v1/merchant/webhook/events/{event_id}/retry starts a new cycle only for an exhausted undelivered event, with the same event ID and payload. Five retry requests per hour per account; pending and delivered events return 409. Changing the receiver URL sends outstanding attempts to the new endpoint; exhausted events still need an explicit retry. See delivery diagnostics. Historical PAID records without an outbox event are not automatically resent.

For a wider outage, paginate your merchant payment records, reconcile with your durable orders and respect the shared read limit. Store reconciliation progress. EXPIRED, CANCELED and REVIEW require investigation; a timely transfer discovered after terminal expiry is not automatically promoted to PAID. Do not create a replacement order or request another transfer until uncertain funds are reconciled.

Three separate credentials

  • API key: X-API-Key for merchant endpoints. Rotate with POST /api/v1/merchant/rotate-key using the separate secret-key header. The old API key stops working immediately; securely save the one returned new_api_key and update your backend workers together.
  • Rotation secret: recovery authority for API-key rotation. Store separately from the API key. There is no self-service rotation/recovery endpoint for this secret; contact support if it is lost or compromised. After ownership verification, support can replace both API credentials while preserving your merchant ID, quota and history. Old keys stop working. Never send credentials through a support chat.
  • Webhook signing key: verifies inbound events. Configure the receiver first and save the full whsec_... key with key_id; the secret is shown once. Rotate via POST /api/v1/merchant/webhook/rotate-key with X-API-Key. New attempts use the new key immediately; retain the old key for five minutes for in-flight requests during a planned rotation. If the old key is compromised, reject it instead and reconcile uncertain events through authenticated details. API-key rotation does not rotate this key.

Prepare secure storage and key-ID selection before rotating. If a response containing a new secret is lost, perform another authorized rotation and securely retain the returned value. Webhooks may fail briefly until the new signing key reaches the receiver; reconcile or request an authenticated merchant retry if attempts are exhausted. Do not retry an uncertain rotation indiscriminately. No API or signing credentials belong in the browser, URLs or logs.

Inactive accounts cannot use authenticated API endpoints or key rotation; existing payment checks, public status and webhook delivery continue. Credential recovery preserves an inactive status until support explicitly reactivates the account.

Sharing a payment link

Anyone who receives payment_url can view the address, amount, description, status and expiry and can pay it. Forwarding the link does not bind the payer to your customer account and does not create a receipt. Share it only with intended recipients; keep personal information out of description and avoid logging full checkout URLs.

A canceled link does not undo a transfer or remove public payment details. Do not assume cancellation revokes visibility. Refunds and attribution disputes are handled by the merchant. Sequential address reuse has additional attribution limits.

Resolve 409 before retrying

  • Webhook not configured: configure the HTTPS receiver before creating a payment.
  • Wallet and amount occupied by PENDING/CHECKING/REVIEW: finish or reconcile that order, use a different amount, or use another address you control.
  • Wallet belongs to another merchant: resolve ownership with support.
  • Idempotency key reused with changed fields: restore the original persisted request; use a new key only for an intentionally new order.
  • Cancellation unavailable: read authenticated details and handle the current state; cancellation cannot reverse funds.

Do not blindly retry 409. For timeouts, 429 and temporary 5xx, keep the original request and key, honor Retry-After and bound retries. The default idempotency replay retention is 24 hours; after retention reconcile uncertain creation before issuing another request. Error and rate-limit reference.

Launch checklist

  • Persist each order, exact amount, wallet and stable idempotency key; save the API response before redirecting.
  • Configure a public HTTPS webhook on port 443, save the signing key securely and capture the original request bytes before JSON parsing.
  • Reject altered bodies, stale timestamps, wrong event IDs and unknown key IDs; match the authenticated event to the saved order.
  • Test repeated and concurrent events, distinct events for one order, and a crash before/after commit. A product must be granted once.
  • Test reconciliation followed by delayed webhook delivery; test unknown orders and wrong amount/wallet.
  • For external delivery, verify stable destination idempotency and worker recovery after an uncertain response.
  • Test timeout after creation, 429/Retry-After, HTML or JSON gateway errors, temporary 503 and terminal 409.
  • Check beta expiry, total/daily creation quotas and shared read limits; status reads and idempotent replays do not consume creation quota.
  • Explain USDT TRC-20, exact received amount, separate network fees, expiry/cancel, late and partial transfers. Establish merchant reconciliation/refund support.
  • Test receiver outage, API-key and signing-key rotation, restart recovery and merchant retry after exhausted webhook attempts.

These are merchant integration checks. They do not replace the service operator’s production security, backup and end-to-end acceptance checks.

Download the API contract

Download the public OpenAPI JSON, generated offline from the application routes and request models. Import it into your own API tooling. Production Swagger remains disabled. Some dynamic responses have no generated schema; their fields and operational rules are described in the API reference.

Handle CHECKING and wallet preparation

Save expires_at and verification_until. CHECKING means the sending window ended and the service is checking for a timely transfer for 10 more minutes. Do not request a second payment or fulfill from a timer. Wait for PAID or reconcile final EXPIRED. Concurrent orders can use different amounts on the same address; the same amount stays reserved through checking.

The default QR contains the address only. Wallet-specific preparation is experimental and does not prove payment. Always check recipient, USDT, TRON and the exact received amount, especially after exchange withdrawal fees. Wallet compatibility.