Express.js
Node.js 20+ / Express 5 · 4 files
Server-side HMAC client, trusted order lookup, checkout creation, and 303 redirect.
Download projectimport crypto from 'node:crypto';
const CHECKOUT_PATH = '/api/v1/merchant/checkout/sessions';
export class TenkiPayError extends Error {
constructor(message, status, details) {
super(message);
this.name = 'TenkiPayError';
this.status = status;
this.details = details;
}
}
export class TenkiPayClient {
constructor({ baseUrl, publicKey, secretKey, timeoutMs = 15_000 }) {
if (!publicKey || !secretKey) {
throw new Error('TenkiPay server credentials are not configured.');
}
this.baseUrl = baseUrl.replace(/\/$/, '');
this.publicKey = publicKey;
this.secretKey = secretKey;
this.timeoutMs = timeoutMs;
}
async createCheckoutSession(payload, idempotencyKey) {
if (!idempotencyKey) {
throw new Error('A stable idempotency key is required for checkout creation.');
}
return this.#request('POST', CHECKOUT_PATH, payload, idempotencyKey);
}
async retrieveCheckoutSession(sessionId) {
const path = `${CHECKOUT_PATH}/${encodeURIComponent(sessionId)}`;
return this.#request('GET', path);
}
async #request(method, path, payload = null, idempotencyKey = null) {
// Serialize exactly once: these same bytes are signed and sent to TenkiPay.
const body = payload === null ? '' : JSON.stringify(payload);
const timestamp = new Date().toISOString();
const canonical = [timestamp, method.toUpperCase(), path, body].join('\n');
const signature = crypto
.createHmac('sha256', this.secretKey)
.update(canonical, 'utf8')
.digest('hex');
const response = await fetch(`${this.baseUrl}${path}`, {
method,
headers: {
Accept: 'application/json',
...(body ? { 'Content-Type': 'application/json' } : {}),
'X-TenkiPay-Key': this.publicKey,
'X-TenkiPay-Timestamp': timestamp,
'X-TenkiPay-Signature': signature,
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {})
},
...(body ? { body } : {}),
signal: AbortSignal.timeout(this.timeoutMs)
});
const result = await response.json().catch(() => ({}));
if (!response.ok) {
throw new TenkiPayError(
result.message || 'TenkiPay rejected the request.',
response.status,
result.errors || null
);
}
return result.data;
}
}