Quickstart
Six steps from an empty project to an invoice you can hand a buyer. Each one links to the reference page that describes it in full, so this page stays short on purpose.
You will need a TEDR Pay merchant account and a backend that can keep a secret — the signing key must never reach a browser or a mobile app.
1. Issue an API key
The key is created from your dashboard: creating one is a session-only route, so there is no way to bootstrap it from a backend. Getting an API key walks through the screens, and the API keys reference describes the endpoints behind them.
The create response carries the whole key row; four of its fields matter here. privateKey identifies the key and travels in every request. secretKey signs your requests. webhookSecret is what TEDR Pay signs its callbacks with. And id is what the update, rotate and revoke calls are addressed to — keep it beside the rest.
The two secrets are shown once.
secretKey and webhookSecret come back only in the create and rotate responses — a read of the key never includes them, and there is no way to fetch them again. Store them before you close the screen. If you lose one, rotate the key: that replaces privateKey, secretKey and webhookSecret all at once — so the KeyId your requests send changes too — and keeps the same id, name and webhookUrl. Revoking instead leaves you with no active key until you create another one, and the new one starts without your webhook URL.
2. Sign your first request
Your backend holds no session, so every request carries an HMAC instead. The signed string is Timestamp + originalUrl + body concatenated with no separators, and the result goes out in four headers beside Content-Type: application/json. The helper below wraps that once; every later step calls it.
import { createHmac } from 'node:crypto';
// PRIVATE_KEY and SECRET_KEY hold the key's privateKey and secretKey.
const apiOrigin = 'https://dev-app.tedr.com';
const pathPrefix = '/api';
const signedFetch = async (path, method, payload) => {
// Sign the path exactly as it goes out, prefix and query string included.
const url = pathPrefix + path;
const timestamp = String(Date.now());
// Serialize once: sign this exact string and send this exact string.
const body = payload === undefined ? '' : JSON.stringify(payload);
const signature = createHmac('sha256', SECRET_KEY)
.update(timestamp + url + body)
.digest('hex');
return await fetch(apiOrigin + url, {
method,
headers: {
'Content-Type': 'application/json',
KeyId: PRIVATE_KEY,
Timestamp: timestamp,
Algorithm: 'sha256',
'X-Signature': signature,
},
body: body || undefined,
});
};
The timestamp has to stay close to the server clock — five minutes by default, in either direction — and the body you sign must be the same string you send — handing the object to something that serializes it again produces different bytes and a 401 SIGNATURE_INVALID. Authentication has the full rules, the cURL and Python equivalents, and the list of routes that accept a signature.
3. Create an invoice
One route creates invoices of every type; type in the body picks which. PAYME is the simplest — a one-off invoice for a fixed amount — and needs nothing but an amount, a string in your merchant profile's base currency. Validation is strict: an unknown field or an unknown type answers 400 VALIDATION_ERROR.
const response = await signedFetch('/invoices', 'POST', {
type: 'PAYME',
amount: '150.00',
description: 'Order 2041',
});
const { invoice } = await response.json();
The 201 answer is a deliberately small envelope: it identifies the invoice and points at its payment page, and does not repeat the amount you just sent:
{
"invoice": {
"businessInvoiceId": "6f1c1e2c-9b4c-4a2e-9f8b-0d5a1b2c3d4e",
"type": "PAYME",
"status": "ACTIVE",
"businessInvoiceUrl": "https://pay.tedr.com/pay/PbQ7x...",
"createdAt": "2026-07-30T10:15:00.000Z",
"expiresAt": "2026-08-29T10:15:00.000Z"
}
}
Store businessInvoiceId against your own order: it is what every later request is addressed to. expiresAt says when the invoice lapses, and is null for a type that never expires. The other three types — itemised, donation and subscription — are on the Invoices page.
4. Send the buyer to the payment page
businessInvoiceUrl is a hosted page on a different host from the API. Redirect the buyer there, or put the address in the email or message that asks for payment — there is nothing to embed and nothing to build.
Everything your integration does not have to handle happens on that page: the buyer picks a cryptocurrency out of the ones the invoice allows, gets a quoted rate and an address, and sends the transfer. Rates and addresses are not part of the create response for exactly that reason.
5. Check whether it was paid
Two reads answer that, and both accept an API key signature. Reading the invoice gives you its current status together with the first page of payments recorded against it; the payments route pages through the rest, newest first, with cursor and a limit of up to 100.
const id = invoice.businessInvoiceId;
const detail = await (await signedFetch(`/invoices/${id}`, 'GET')).json();
const payments = await (
await signedFetch(`/invoices/${id}/payments?limit=20`, 'GET')
).json();
One payment row is one actual transfer, so a partly paid invoice has several — each with its amount, txHash and paidAt. A row's amount is that single transaction in the currency it was paid in, so adding rows up yourself mixes currencies on any invoice that accepts more than one. Settle against totalBaseAmount, which the server has already converted into the baseCurrency alongside it. Both routes, their parameters and their error codes are on the Invoices page.
Poll these reads; don't trust a callback.
TEDR Pay can call a webhook URL when something changes, but the scheme for verifying those callbacks is not published yet, so your endpoint cannot tell a genuine one from a forged one. Use a callback as a hint that something moved, and let these two reads decide what actually happened — Webhooks explains the whole picture.
6. Where to go next
- Invoices — the remaining three invoice types, the list route with its filters and search, and the cancel, reactivate and delete commands.
- Transactions — the history behind the balance, one row per business event, for reconciliation against your own books.
- Webhooks — how to point notifications at your endpoint, and why they are a signal rather than proof.
- Withdrawals — moving the settled balance out, including the email confirmation step.
- Errors — every code the API returns, worth reading before you write the failure branches.
