@tap-payments/node
v0.1.4
Published
Official Node.js & TypeScript SDK for the Tap Payments API.
Readme
Tap Payments Node.js SDK
The official Node.js & TypeScript SDK for the Tap Payments API. Convenient typed access to the API from server-side JavaScript/TypeScript — auth, retries, timeouts, and error handling handled for you.
⚠️ This SDK is for server-side use only. It uses your secret API key, which must never be exposed in a browser, mobile app, or any client-side code. See Security.
Contents
- Requirements
- Installation
- Quick start
- Configuration
- The redirect flow (3DS / KNET / mada)
- Per-request options
- Automatic retries & idempotency
- Error handling
- Response metadata
- Custom HTTP client
- Security
- Compatibility
- API coverage
- Support
- License
Requirements
- Node.js 18+ (uses the built-in global
fetch). For older runtimes, supply a custom HTTP client or afetchpolyfill. - Works with TypeScript (types bundled), ESM, and CommonJS.
Installation
npm install @tap-payments/node
# or
yarn add @tap-payments/node
# or
pnpm add @tap-payments/nodeQuick start
ESM / TypeScript
import Tap from '@tap-payments/node';
const tap = new Tap(process.env.TAP_SECRET_KEY!);
const charge = await tap.charges.create({
amount: 10,
currency: 'KWD',
customer: {
first_name: 'Ahmed',
last_name: 'Elsharkawy',
email: '[email protected]',
phone: { country_code: '965', number: '51234567' },
},
source: { id: 'src_all' },
redirect: { url: 'https://example.com/checkout/return' },
});
console.log(charge.id, charge.status);CommonJS
const { Tap } = require('@tap-payments/node');
const tap = new Tap(process.env.TAP_SECRET_KEY);The API key is read, in order, from the constructor string, the apiKey config field, or the TAP_SECRET_KEY environment variable.
Configuration
Pass a key string, a config object, or both:
const tap = new Tap({
apiKey: 'sk_test_XXXX',
timeout: 30_000, // ms; default 80_000
maxRetries: 3, // default 2 (set 0 to disable)
telemetry: true, // default true; anonymous SDK metrics, never PII/payloads
appInfo: {
name: 'acme-storefront',
version: '1.4.2',
url: 'https://acme.example',
},
defaultHeaders: { 'X-My-Header': 'value' },
});| Option | Type | Default | Description |
| ---------------- | ----------------------- | ---------------------------- | -------------------------------------------- |
| apiKey | string | process.env.TAP_SECRET_KEY | Your secret key (sk_test_… / sk_live_…). |
| baseUrl | string | https://api.tap.company | API origin. |
| apiVersion | string | v2 | API version segment. |
| timeout | number | 80000 | Per-request timeout in milliseconds. |
| maxRetries | number | 2 | Automatic retries for transient failures. |
| httpClient | HTTPClient | FetchHTTPClient | Custom transport (see below). |
| telemetry | boolean | true | Send anonymous SDK telemetry headers. |
| appInfo | AppInfo | — | Your app info, appended to the User-Agent. |
| defaultHeaders | Record<string,string> | {} | Headers merged into every request. |
Inspect the resolved config (with the key masked) via tap.getConfig().
The redirect flow (3DS / KNET / mada)
Most Tap payments require the customer to complete an off-session step. When they do, the created charge is returned with status INITIATED and a transaction.url:
const charge = await tap.charges.create({
amount: 25.5,
currency: 'SAR',
customer: { first_name: 'Ahmed', email: '[email protected]' },
source: { id: 'src_all' },
redirect: { url: 'https://example.com/checkout/return' }, // required for these flows
post: { url: 'https://example.com/webhooks/tap' }, // optional webhook
});
if (charge.transaction?.url) {
// 1. Redirect the customer's browser to this URL.
// 2. Tap sends them back to your `redirect.url` when done.
// 3. Confirm the final status from your `post.url` webhook or by retrieving the charge.
return res.redirect(charge.transaction.url);
}Per-request options
Every resource method accepts an optional final argument to override client settings for that call:
const controller = new AbortController();
await tap.charges.create(params, {
idempotencyKey: 'order-1024', // stable de-duplication key
timeout: 10_000, // override client timeout
maxRetries: 0, // disable retries for this call
apiKey: 'sk_test_other', // use a different key
headers: { 'X-Trace-Id': 'abc' }, // extra headers
signal: controller.signal, // cancel the request
});Automatic retries & idempotency
The SDK automatically retries transient failures — network errors, timeouts, HTTP 429, and 5xx — up to maxRetries, using exponential backoff with jitter and honoring the Retry-After header.
To make those retries safe, every mutating request (e.g. charges.create) automatically includes an idempotency key, so a charge is never created twice. Supply your own idempotencyKey (e.g. your order id) to also de-duplicate retries you trigger yourself.
Error handling
Every failure throws a typed subclass of TapError. Catch the base class, or narrow to a specific type:
import Tap, {
TapError,
TapAuthenticationError,
TapInvalidRequestError,
TapRateLimitError,
} from '@tap-payments/node';
try {
await tap.charges.create(params);
} catch (err) {
if (err instanceof TapInvalidRequestError) {
console.error('Validation failed:', err.code, err.message);
} else if (err instanceof TapAuthenticationError) {
console.error('Bad API key.');
} else if (err instanceof TapRateLimitError) {
console.error('Rate limited — retry later.');
} else if (err instanceof TapError) {
console.error(`[${err.type}] ${err.message} (request ${err.requestId})`);
} else {
throw err;
}
}| Error class | When | HTTP |
| ------------------------ | ------------------------------------------------ | ----------- |
| TapAuthenticationError | Missing/invalid/revoked API key | 401 |
| TapPermissionError | Key lacks permission / feature not enabled | 403 |
| TapInvalidRequestError | Malformed request or failed validation | 400/422 |
| TapNotFoundError | Resource not found | 404 |
| TapRateLimitError | Rate limit exceeded | 429 |
| TapAPIError | Tap server error | 5xx |
| TapConnectionError | Network failure, timeout, or abort (no response) | — |
| TapConfigurationError | SDK misconfiguration (e.g. missing key) | — |
Each error exposes type, code, statusCode, requestId, headers, and raw. Its toJSON() redacts the Authorization header, so logging an error never leaks your key.
Tap's business code (e.g. "1100", "1117") can be resolved to a message with the bundled reference — handy when a server description isn't present:
import {
describeErrorCode,
describeResponseCode,
CHARGE_RESPONSE_CODES,
} from '@tap-payments/node';
describeErrorCode('1100'); // → "Header values are missing"
describeResponseCode(CHARGE_RESPONSE_CODES, charge.response?.code); // → "Captured"Response metadata
Every returned object carries a non-enumerable lastResponse with HTTP details, without interfering with JSON.stringify:
const charge = await tap.charges.create(params);
charge.lastResponse.statusCode; // 200
charge.lastResponse.requestId; // 'req_…' — quote this in support tickets
charge.lastResponse.retries; // how many retries were neededCustom HTTP client
Provide any object implementing HTTPClient to route requests through a proxy, add instrumentation, or run without a global fetch:
import Tap, { type HTTPClient } from '@tap-payments/node';
const httpClient: HTTPClient = {
async request(req) {
const res = await myFetch(req.url, {
method: req.method,
headers: req.headers,
body: req.body,
signal: req.signal,
});
return {
statusCode: res.status,
headers: Object.fromEntries(res.headers),
body: await res.text(),
};
},
};
const tap = new Tap({ apiKey: 'sk_test_XXXX', httpClient });Security
- Secret keys are backend-only. Never ship
sk_test_…/sk_live_…to a browser, mobile app, or public repo. Publishable keys (pk_…) are for client-side tokenization only — the SDK warns if one is used here. - Keys are never logged. The
Authorizationheader is redacted in error serialization, andtap.getConfig()masks the key. - TLS everywhere. The default base URL is HTTPS; the SDK warns on non-HTTPS, non-localhost base URLs.
- Idempotency on mutating requests prevents duplicate charges on retry.
- Load keys from environment variables or a secrets manager (
TAP_SECRET_KEY), not source code.
See SECURITY.md to report a vulnerability.
Compatibility
- Runtimes: Node.js 18, 20, 22 (and any runtime with a global
fetch, or a customhttpClient). - Modules: ships both ESM (
import) and CommonJS (require) builds via theexportsmap. - Types: first-class TypeScript declarations (
.d.ts) are bundled. - Dependencies: zero runtime dependencies.
API coverage
Every method returns the API object augmented with non-enumerable lastResponse
HTTP metadata, and accepts an optional final RequestOptions argument.
| Resource | Accessor | Methods |
| --------- | --------------- | ---------------------------------------------------------------------------- |
| Resource | Accessor | Methods |
| --------- | --------------- | ---------------------------------------------------------------------------- |
| Charges | tap.charges | create · retrieve · update · list · download |
| Authorize | tap.authorize | create · retrieve · update · void · list · download |
| Tokens | tap.tokens | create · retrieve |
| Cards | tap.cards | verify · list · retrieve · delete |
| Refunds | tap.refunds | create · retrieve · update · list · download |
| Intents | tap.intents | create · retrieve · update · list · cancel |
| Invoices | tap.invoices | create · retrieve · update · list · remind · finalize · cancel |
| Files | tap.files | create · retrieve |
| Disputes | tap.disputes | download |
// A token (any variant — card, saved card, Apple Pay, … — is one method):
const token = await tap.tokens.create({
card: { number: '5123450000000008', exp_month: 5, exp_year: 27, cvc: '100' },
});
// Authorize a hold, then void it:
const auth = await tap.authorize.create({
amount: 10,
currency: 'KWD',
customer: { first_name: 'Ahmed', email: '[email protected]' },
source: { id: 'src_all' },
});
await tap.authorize.void(auth.id);
// An invoice (due/expiry are in MILLISECONDS since the epoch; order.items required):
const invoice = await tap.invoices.create({
due: Date.now() + 30 * 86_400_000,
expiry: Date.now() + 60 * 86_400_000,
customer: { first_name: 'Ahmed', email: '[email protected]' },
order: {
amount: 25,
currency: 'KWD',
items: [{ name: 'Consulting', quantity: 1, amount: 25 }],
},
});
// Refund a captured charge:
const refund = await tap.refunds.create({
charge_id: 'chg_XXXX',
amount: 10,
currency: 'KWD',
reason: 'requested_by_customer',
});Downloads and file uploads
- Download endpoints return the raw response body so you can save it:
charges.download,authorize.download, anddisputes.downloadresolve to{ data: string }(CSV text) — write it to a.csvfile. files.createuploads viamultipart/form-data. Pass aBlobor aUint8Array(with afilename); the SDK builds the form for you.
Escape hatch
The client also exposes a typed low-level escape hatch for endpoints not yet wrapped:
const result = await tap.request<MyType>(
'POST',
'some/endpoint',
body,
options,
);Support
This SDK is built and maintained by Tap Payments.
- 📚 Documentation: developers.tap.company
- 💬 Help & integration support: tap.company or your Tap account manager
- 🐞 Issues & recommendations: open a GitHub issue, or reach the maintainer at [email protected]
- 🔒 Security reports: see SECURITY.md
License
MIT © Tap Payments
