Introduction

The Tasdid API lets your website or app accept crypto payments without ever touching a wallet, private key, or chain library. Create a payment with one HTTP call, redirect your customer to the returned hosted_url, and receive a signed webhook the moment the network confirms it.

Custodial. Tasdid holds funds on your behalf between confirmation and your next withdrawal. Your KYC must be approved before you can create live payments.

Base URL: https://api.tasdid.com (production). During development, use whatever host you self-host —http://localhost:3001 if you're running locally.

Quick start

  1. Generate a test key (tsd_test_) from Dashboard → API keys, and add a webhook endpoint from Dashboard → Webhooks (copy its whsec_ secret).
  2. Create a payment from your server. We return a hosted_url to send the shopper to.
  3. Complete the test payment with POST /api/v1/payments/:id/test_complete and verify the payment.paid webhook reaches your endpoint.
  4. Swap in a live key (tsd_live_, needs approved KYC). That first test payment also unlocks live mode (see the go-live gate under Authentication).
cURL — minimal payment
curl https://api.tasdid.com/api/v1/payments \
  -H "Authorization: Bearer tsd_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "amount_usd": 72.50,
    "success_url": "https://yourshop.com/thanks",
    "cancel_url":  "https://yourshop.com/cart",
    "metadata": { "order_id": "ORD-1234" }
  }'

Authentication

Every API request must include a bearer token in the Authorization header.

http
Authorization: Bearer tsd_live_abcdef...

Keys are scoped to a single merchant account. Anyone holding the key can create payments and read your payment history — treat it like a password.

  • Generate, rotate, and revoke keys from your dashboard.
  • The full key is shown only once at creation.
  • Revoking a key invalidates it immediately on every request.
  • Use HTTPS in production. Never embed the key in client-side code.

Test vs live keys

Keys come in two modes. tsd_test_ keys create test payments — fully simulated, never on-chain, never credited to your balance — so you can wire up your integration before KYC. tsd_live_ keys move real money and require approved KYC.

Go-live gate. Your first live payment is blocked with 403 test_required until you've completed at least one successful test payment (create a test payment, then call /test_complete — see below). This proves your integration works before real funds flow.

Idempotency

All POST endpoints accept an Idempotency-Key header. Send the same key on a retry and the API returns the original response — no duplicate payment gets created.

http
POST /api/v1/payments
Idempotency-Key: 06f3eba8-31dc-4d6f-a3f0-7c6e3f8b9001

The cached response is also flagged on replay so you can confirm it's a hit:

http
HTTP/1.1 201 Created
Idempotent-Replayed: true
Content-Type: application/json
...
Use a fresh UUID per logical request. Reusing one across two different orders is the most common bug — you'll get the first order's response back, not the second.

Only successful responses are cached. If a call errors (e.g. 409 reference_conflict), nothing is stored — fix the request and retry with the same key. Keys are also scoped per mode, so a test and a live call can safely share a key.

Errors

Errors come back as JSON with a consistent envelope.

json
{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_field",
    "message": "Number must be less than or equal to 10000",
    "param": "amount_usd"
  }
}
StatusTypeCommon codes
400invalid_request_errorinvalid_json, invalid_field, over_payment_limit
401authentication_errormissing_authorization, invalid_authorization, invalid_api_key, revoked_api_key
403authentication_errorkyc_required, test_required, live_key_used, live_payment
404invalid_request_errornot_found
409conflict / invalid_request_errorreference_conflict, request_in_progress, already_finalized
429invalid_request_errorrate_limited (120/min per key)
500api_errorcreate_failed

Create a payment

POST /api/v1/payments

Creates a payment request. Returns the deposit address, the EIP-681 QR URI, and a hosted checkout URL.

Request body

FieldTypeDescription
amount_usdrequirednumberUSD amount to charge. Between 0.01 and 10,000, at most 2 decimal places.
chainstringNetwork to receive on: BSC (default and only supported network at this time). Tron (TRC-20) is temporarily unavailable; passing TRON returns chain_not_supported.
referencestringYour order id (3–40 chars). Stored as merchant_reference and returned under that field — not as reference. Optional and not auto-generated. Must be unique per merchant; a duplicate returns 409 reference_conflict.
expires_in_minutesnumberWindow in which the customer can pay. 1–60. Default 30.
success_urlstringURL the hosted checkout redirects the shopper to after a successful payment.
cancel_urlstringURL shown on the expired/cancelled hosted page.
customer_emailstringShopper email. Stored for your reference; no email is sent yet.
metadataobjectArbitrary key-value pairs. Returned verbatim on retrieve and in webhook payloads.

Request

bash
curl https://api.tasdid.com/api/v1/payments \
  -H "Authorization: Bearer tsd_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "amount_usd": 72.50,
    "chain": "BSC",
    "reference": "ORD-1234",
    "expires_in_minutes": 30,
    "success_url": "https://yourshop.com/thanks?order=ORD-1234",
    "cancel_url":  "https://yourshop.com/cart",
    "customer_email": "buyer@example.com",
    "metadata": { "order_id": "ORD-1234", "sku": "T-shirt-L" }
  }'

Response — 201

json
{
  "id": "cmpy3q1k0000a8j2hxs1mr2y",
  "reference": "TSD-CJBMXP",
  "merchant_reference": "ORD-1234",
  "status": "PENDING",
  "mode": "live",
  "amount_usd": 72.5,
  "amount_usdt": 72.5,
  "locked_rate": 1,
  "chain": "BSC",
  "asset": "USDT",
  "deposit_address": "0xC59829e89Eb32Dcd34bbaD8f7555954a25A62f07",
  "qr_uri": "ethereum:0x705b...c3bf@97/transfer?address=0xC598...&uint256=72500000000000000000",
  "hosted_url": "https://app.tasdid.com/pay/TSD-CJBMXP",
  "source": "api",
  "success_url": "https://yourshop.com/thanks?order=ORD-1234",
  "cancel_url":  "https://yourshop.com/cart",
  "customer_email": "buyer@example.com",
  "metadata": { "order_id": "ORD-1234", "sku": "T-shirt-L" },
  "on_chain_txs": [],
  "paid_at": null,
  "expires_at": "2026-06-04T08:30:00.000Z",
  "created_at": "2026-06-04T08:00:00.000Z"
}

Retrieve a payment

GET /api/v1/payments/:id

:id can be the payment id(cmp...) or the human reference (TSD-XXXXXX).

bash
curl https://api.tasdid.com/api/v1/payments/TSD-CJBMXP \
  -H "Authorization: Bearer tsd_live_..."

Returns the same shape as create, with on_chain_txs populated as confirmations arrive.

Complete a test payment

POST /api/v1/payments/:id/test_complete

Simulates a customer paying — flips a test payment to PAID and fires the payment.paid webhook, with no chain or ledger involvement. Requires a tsd_test_ key and a test-mode payment. This is also how you clear the go-live gate.

bash
curl -X POST https://api.tasdid.com/api/v1/payments/TSD-CJBMXP/test_complete \
  -H "Authorization: Bearer tsd_test_..."

Errors: 403 live_key_used (live key), 403 live_payment (the payment is live), and 409 already_finalized (not PENDING).

The Payment object

Every payment returned by the API has these fields. Webhook payloads carry the same shape under data.

FieldTypeDescription
idstringTasdid identifier.
referencestringServer-generated public key (TSD-XXXXXX). Shown on QRs/receipts and in the hosted URL.
merchant_referencestring|nullYour order id from the request, or null if you didn't send one.
statusenumPENDING · CONFIRMING · PAID · OVERPAID · UNDERPAID · EXPIRED. Manually-reviewed payments may also surface SUBMITTED, UNDER_REVIEW, REJECTED, CANCELLED, or SETTLED — treat unknown values defensively.
modestringlive or test — matches the key that created it.
amount_usdnumberRequested amount.
amount_usdtnumberUSDT-equivalent at the locked rate.
locked_ratenumberUSDT per USD locked at creation (1.0 in v1).
chainstringBSC — the network the payment settles on.
assetstringAlways USDT in v1.
deposit_addressstringWhere the customer sends crypto.
qr_uristringFor QR rendering — an EIP-681 URI for BSC; the raw deposit address (no URI scheme) for Tron.
hosted_urlstringPublic branded checkout URL (on your Tasdid app origin).
sourcestringHow it was created: api, dashboard, or link.
success_urlstring|nullWhere the hosted checkout sends the shopper after success.
cancel_urlstring|nullShown on the expired/cancelled hosted page.
customer_emailstring|nullShopper email, if collected.
on_chain_txsarrayObserved transfers: tx_hash, from_address, to_address, amount, confirmations, block_number. amount is a decimal string (full on-chain precision).
paid_atstringRFC3339 timestamp when status reached PAID. Null otherwise.
expires_atstringWhen the payment window closes.
created_atstringRFC3339 timestamp when the payment was created.
metadataobjectWhatever you put in at creation (≤4KB).

How webhooks work

Configure one or more HTTPS endpoints in your dashboard. We POST JSON to each one whenever a payment's status changes. Use webhooks instead of polling — they're lower latency and less load on both sides.

Event envelope
{
  "id": "evt_a1b2c3d4e5f6a7b8c9d0e1f2",
  "type": "payment.paid",
  "created": 1717450000,
  "data": { /* a Payment object */ }
}
We send the same event to every active endpoint. Each endpoint gets its own retry queue, so a flaky URL doesn't affect your other integrations.

Event types

TypeFires when
payment.confirmingThe watcher first observes the on-chain transfer (before full confirmations). Useful for showing "payment detected" UI to the shopper.
payment.paidRequired confirmations reached and the exact amount (or more) was received. Fulfil the order on this event.
payment.overpaidSame as paid but with extra credited. The full amount is already in the merchant ledger.
payment.underpaidCustomer sent less than requested. Not auto-credited — reach out to them or ignore.
payment.expiredWindow closed before any payment was confirmed. Cancel the order on your end.

Verifying signatures

Every delivery includes a Tasdid-Signature header with a timestamp and an HMAC-SHA256 of <timestamp>.<raw-body>:

http
Tasdid-Signature: t=1717450000,v1=86c1a7b3e2c5...
Tasdid-Event-Type: payment.paid
Tasdid-Event-Id: evt_a1b2c3...

Node

javascript
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyTasdidSignature(rawBody, header, secret) {
  const match = /^t=(\d+),v1=([a-f0-9]+)$/.exec(header ?? "");
  if (!match) return false;
  const [, t, v1] = match;
  const expected = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  return v1.length === expected.length &&
    timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

Python

python
import hmac, hashlib, re

def verify_tasdid_signature(raw_body: bytes, header: str, secret: str) -> bool:
    m = re.match(r"^t=(\d+),v1=([a-f0-9]+)$", header or "")
    if not m: return False
    t, v1 = m.group(1), m.group(2)
    expected = hmac.new(
        secret.encode(),
        f"{t}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, v1)

PHP

php
function verify_tasdid_signature(string $rawBody, ?string $header, string $secret): bool {
  if (!$header || !preg_match('/^t=(\d+),v1=([a-f0-9]+)$/', $header, $m)) {
    return false;
  }
  [, $t, $v1] = $m;
  $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
  return hash_equals($expected, $v1);
}
Verify against the raw request body, not a parsed object. Even a re-serialised JSON differs by a single whitespace character and the signature will fail.

Retry policy

A delivery is considered successful if your endpoint returns a 2xx status code within 10 seconds. Anything else — timeout, 4xx, 5xx, network error — is retried.

AttemptWait before retry
1 → 230 seconds
2 → 32 minutes
3 → 410 minutes
4 → 530 minutes
5 → 62 hours
6 → 76 hours

After 7 failed attempts the delivery is marked failed. You can replay it manually from the dashboard's deliveries log.

Need a feature, found a bug, or want to integrate something the docs don't cover? Reach the team from tasdid.com.