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.
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
- Generate a test key (
tsd_test_) from Dashboard → API keys, and add a webhook endpoint from Dashboard → Webhooks (copy itswhsec_secret). - Create a payment from your server. We return a
hosted_urlto send the shopper to. - Complete the test payment with
POST /api/v1/payments/:id/test_completeand verify thepayment.paidwebhook reaches your endpoint. - 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 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.
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.
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.
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/1.1 201 Created Idempotent-Replayed: true Content-Type: application/json ...
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.
{
"error": {
"type": "invalid_request_error",
"code": "invalid_field",
"message": "Number must be less than or equal to 10000",
"param": "amount_usd"
}
}| Status | Type | Common codes |
|---|---|---|
| 400 | invalid_request_error | invalid_json, invalid_field, over_payment_limit |
| 401 | authentication_error | missing_authorization, invalid_authorization, invalid_api_key, revoked_api_key |
| 403 | authentication_error | kyc_required, test_required, live_key_used, live_payment |
| 404 | invalid_request_error | not_found |
| 409 | conflict / invalid_request_error | reference_conflict, request_in_progress, already_finalized |
| 429 | invalid_request_error | rate_limited (120/min per key) |
| 500 | api_error | create_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
| Field | Type | Description |
|---|---|---|
| amount_usdrequired | number | USD amount to charge. Between 0.01 and 10,000, at most 2 decimal places. |
| chain | string | Network to receive on: BSC (default and only supported network at this time). Tron (TRC-20) is temporarily unavailable; passing TRON returns chain_not_supported. |
| reference | string | Your 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_minutes | number | Window in which the customer can pay. 1–60. Default 30. |
| success_url | string | URL the hosted checkout redirects the shopper to after a successful payment. |
| cancel_url | string | URL shown on the expired/cancelled hosted page. |
| customer_email | string | Shopper email. Stored for your reference; no email is sent yet. |
| metadata | object | Arbitrary key-value pairs. Returned verbatim on retrieve and in webhook payloads. |
Request
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
{
"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).
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.
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.
| Field | Type | Description |
|---|---|---|
| id | string | Tasdid identifier. |
| reference | string | Server-generated public key (TSD-XXXXXX). Shown on QRs/receipts and in the hosted URL. |
| merchant_reference | string|null | Your order id from the request, or null if you didn't send one. |
| status | enum | PENDING · CONFIRMING · PAID · OVERPAID · UNDERPAID · EXPIRED. Manually-reviewed payments may also surface SUBMITTED, UNDER_REVIEW, REJECTED, CANCELLED, or SETTLED — treat unknown values defensively. |
| mode | string | live or test — matches the key that created it. |
| amount_usd | number | Requested amount. |
| amount_usdt | number | USDT-equivalent at the locked rate. |
| locked_rate | number | USDT per USD locked at creation (1.0 in v1). |
| chain | string | BSC — the network the payment settles on. |
| asset | string | Always USDT in v1. |
| deposit_address | string | Where the customer sends crypto. |
| qr_uri | string | For QR rendering — an EIP-681 URI for BSC; the raw deposit address (no URI scheme) for Tron. |
| hosted_url | string | Public branded checkout URL (on your Tasdid app origin). |
| source | string | How it was created: api, dashboard, or link. |
| success_url | string|null | Where the hosted checkout sends the shopper after success. |
| cancel_url | string|null | Shown on the expired/cancelled hosted page. |
| customer_email | string|null | Shopper email, if collected. |
| on_chain_txs | array | Observed transfers: tx_hash, from_address, to_address, amount, confirmations, block_number. amount is a decimal string (full on-chain precision). |
| paid_at | string | RFC3339 timestamp when status reached PAID. Null otherwise. |
| expires_at | string | When the payment window closes. |
| created_at | string | RFC3339 timestamp when the payment was created. |
| metadata | object | Whatever 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.
{
"id": "evt_a1b2c3d4e5f6a7b8c9d0e1f2",
"type": "payment.paid",
"created": 1717450000,
"data": { /* a Payment object */ }
}Event types
| Type | Fires when |
|---|---|
payment.confirming | The watcher first observes the on-chain transfer (before full confirmations). Useful for showing "payment detected" UI to the shopper. |
payment.paid | Required confirmations reached and the exact amount (or more) was received. Fulfil the order on this event. |
payment.overpaid | Same as paid but with extra credited. The full amount is already in the merchant ledger. |
payment.underpaid | Customer sent less than requested. Not auto-credited — reach out to them or ignore. |
payment.expired | Window 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>:
Tasdid-Signature: t=1717450000,v1=86c1a7b3e2c5... Tasdid-Event-Type: payment.paid Tasdid-Event-Id: evt_a1b2c3...
Node
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
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
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);
}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.
| Attempt | Wait before retry |
|---|---|
| 1 → 2 | 30 seconds |
| 2 → 3 | 2 minutes |
| 3 → 4 | 10 minutes |
| 4 → 5 | 30 minutes |
| 5 → 6 | 2 hours |
| 6 → 7 | 6 hours |
After 7 failed attempts the delivery is marked failed. You can replay it manually from the dashboard's deliveries log.