TypeScript SDK
Call the merchant API and verify webhooks from TypeScript or JavaScript with the @turmite/sdk package.
@turmite/sdk is the TypeScript client for the merchant API. It also verifies webhooks. It has no runtime dependencies. The source is in sdk/typescript in the repository.
Install
npm install @turmite/sdk
The SDK uses fetch and Web Crypto. It runs on Node.js 20 or later, Deno, Bun and Cloudflare Workers. It is an ES module.
Use the SDK on your server only. The API key must not reach a browser.
Create a client
import { Turmite } from '@turmite/sdk';
const turmite = new Turmite({
baseUrl: 'https://pay.example.com',
apiKey: process.env.TM_API_KEY!,
});
| Option | Description |
|---|---|
baseUrl |
Your site URL, with its path if it has one. A checkout domain also works |
apiKey |
The project API key (tm_…) |
fetch |
A replacement for the global fetch |
timeoutMs |
Request timeout in milliseconds. The default is 30000 |
Create an invoice
const invoice = await turmite.createInvoice({
price_amount: 25,
price_currency: 'usd',
order_id: 'ORDER-1001',
order_description: 'Order 1001',
ipn_callback_url: 'https://shop.example.com/turmite/ipn',
success_url: 'https://shop.example.com/orders/ORDER-1001',
cancel_url: 'https://shop.example.com/cart',
});
// Send the customer to the checkout.
redirect(invoice.invoice_url);
Create a payment
const payment = await turmite.createPayment({
price_amount: 25,
price_currency: 'usd',
pay_currency: 'usdttrc20',
order_id: 'ORDER-1001',
ipn_callback_url: 'https://shop.example.com/turmite/ipn',
idempotency_key: 'ORDER-1001',
});
// Show these on your own page, or send the customer to payment.payment_url.
console.log(payment.pay_address, payment.pay_amount);
const latest = await turmite.getPayment(payment.payment_id);
The server sends payment_id as a string in the createPayment() response and as a number in getPayment() and listPayments(). See The payment object.
Methods
| Method | Endpoint |
|---|---|
status() |
GET /v1/status |
currencies() |
GET /v1/currencies |
fullCurrencies() |
GET /v1/full-currencies |
merchantCoins() |
GET /v1/merchant/coins |
minAmount({ currency_from, currency_to, fiat_equivalent? }) |
GET /v1/min-amount |
estimate({ amount, currency_from, currency_to }) |
GET /v1/estimate |
createPayment(body) |
POST /v1/payment |
getPayment(paymentId) |
GET /v1/payment/{payment_id} |
listPayments(params) |
GET /v1/payment |
createInvoice(body) |
POST /v1/invoice |
createInvoicePayment(body) |
POST /v1/invoice-payment |
balance() |
GET /v1/balance |
auth(email, password) |
POST /v1/auth |
createPayout(body, options?) |
POST /v1/payout |
verifyPayout(batchId, verificationCode, options?) |
POST /v1/payout/{batch_id}/verify |
getPayout(batchId) |
GET /v1/payout/{batch_id} |
listPayouts(params, options?) |
GET /v1/payout |
validateAddress(body) |
POST /v1/payout/validate-address |
payoutFee({ currency, amount? }) |
GET /v1/payout/fee |
The field names are the same as in the API. See Merchant API.
Payouts
auth() signs in a dashboard user and keeps the token for createPayout(), verifyPayout() and listPayouts(). The token is valid for 5 minutes. To use another token, pass { token } as the last argument of these methods. createPayout() and verifyPayout() need a user who is a project admin.
await turmite.auth('[email protected]', password);
const batch = await turmite.createPayout({
withdrawals: [
{ address: 'TQrZ9wBzhXnHdAdKwRBfdMH2RiTjH6yGtW', currency: 'usdttrc20', amount: '250.00', unique_external_id: 'refund-1042' },
],
});
await turmite.verifyPayout(batch.id, totpCode);
See Payouts.
Verify webhooks
The SDK verifies both signatures. Pass the raw body, before JSON parsing. A string, a Uint8Array (for example a Node.js Buffer) or an ArrayBuffer works.
| Function | Checks | Returns |
|---|---|---|
verifyWebhook(rawBody, headers, secret, options?) |
x-turmite-timestamp and x-turmite-signature |
The parsed body. Throws WebhookVerificationError when a check fails |
verifyIpnSignature(rawBody, signature, secret) |
The key-sorted HMAC-SHA512 signature, default header x-turmite-sig |
true or false. It never throws for a bad signature or body |
isPaid(status) |
Nothing | true only for finished |
verifyWebhook refuses a timestamp that is more than 300 seconds from the local clock. Change it with { toleranceSeconds }. verifyIpnSignature makes the key-sorted JSON from the raw body and keeps the text of each number, so the check also works for a number such as 1e-6. verifyWebhook throws a TypeError when the body is not raw, for example a parsed object.
import express from 'express';
import { isPaid, verifyIpnSignature, verifyWebhook, type PaymentWebhook, type WebhookEvent } from '@turmite/sdk';
const app = express();
// Per-payment IPN (ipn_callback_url): timestamp signature, project IPN secret.
app.post('/turmite/ipn', express.raw({ type: 'application/json' }), async (req, res) => {
let payment: PaymentWebhook;
try {
payment = await verifyWebhook<PaymentWebhook>(req.body, req.headers, process.env.TM_IPN_SECRET!);
} catch {
return res.sendStatus(400);
}
if (isPaid(payment.payment_status)) await markOrderPaid(payment.order_id, payment.payment_id);
res.sendStatus(200);
});
// Project endpoint: envelope body, signed with the endpoint's own secret.
app.post('/turmite/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const event = await verifyWebhook<WebhookEvent>(req.body, req.headers, process.env.TM_WEBHOOK_SECRET!).catch(() => null);
if (!event) return res.sendStatus(400);
if (event.event === 'payment.finished') await markOrderPaid(event.data.order_id, event.data.payment_id);
res.sendStatus(200);
});
// Existing code that checks the compatible signature.
app.post('/legacy/ipn', express.raw({ type: 'application/json' }), async (req, res) => {
const ok = await verifyIpnSignature(req.body, req.get('x-turmite-sig'), process.env.TM_IPN_SECRET!);
if (!ok) return res.sendStatus(400);
res.sendStatus(200);
});
Make markOrderPaid idempotent. See Webhooks.
The package also exports the header names as TIMESTAMP_HEADER, SIGNATURE_HEADER and IPN_SIGNATURE_HEADER, and canonicalJson(), which returns the key-sorted JSON of a raw body.
Errors
A failed request throws TurmiteError. It has status (the HTTP status, or 0 when no response came), code, message and body.
import { TurmiteError } from '@turmite/sdk';
try {
await turmite.createPayment({ price_amount: 0, price_currency: 'usd', pay_currency: 'btc' });
} catch (err) {
if (err instanceof TurmiteError) {
console.error(err.status, err.code, err.message);
}
}
code is the API error code, for example INVALID_REQUEST_PARAMS. The SDK adds these codes of its own:
| Code | Cause |
|---|---|
NETWORK_ERROR |
The request failed before a response came |
TIMEOUT |
No response within timeoutMs |
TOKEN_REQUIRED |
createPayout(), verifyPayout() or listPayouts() was called before auth() and without a token |
HTTP_<status> |
An error response without a JSON code |
WebhookVerificationError has a code: missing_header, invalid_timestamp, timestamp_out_of_range, invalid_signature or invalid_body.
Types
The package exports a type for each request and response, for example Payment, PaymentWebhook, PaymentStatus, Invoice, Payout, PayoutStatus, PayoutBatch and WebhookEvent. It also exports the option types TurmiteOptions, TokenOptions, FetchLike, VerifyWebhookOptions, HeadersLike, RawBody and WebhookErrorCode. The types follow the API reference.