npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tinker-payments-node-sdk

v0.6.0

Published

Node.js SDK for Tinker Payments API

Readme

Tinker Payments Node SDK

Node.js 18+ SDK with automatic authentication and TypeScript declarations.

Install and configure

npm install tinker-payments-node-sdk
const { TinkerPayments, errors } = require('tinker-payments-node-sdk');
const client = new TinkerPayments('pk_test_xxx', 'sk_test_xxx');

TypeScript uses import { TinkerPayments, errors, type PaymentInitiateRequest } from 'tinker-payments-node-sdk'. Test key prefixes select https://sandbox-api.tinkerpayments.com/v1/; other keys select https://api.tinkerpayments.com/v1/. Override with { baseUrl, fetch } as the constructor's third argument. Keep secret keys on the server.

Payments

Run asynchronous examples inside an async function or an ES module.

const payment = await client.transactions().initiate({
  amount: 1200, currency: 'KES', gateway: 'mpesa',
  merchantReference: 'ORDER-1001',
  returnUrl: 'https://merchant.example/payment/return',
  customerPhone: '+254712345678'
});
const result = await client.transactions().query({
  reference: 'TP-REF-123', gateway: 'mpesa'
});

Payments use currency units. returnUrl is the browser destination; configure webhooks separately in the dashboard. Mpesa requires customerPhone.

Plan subscriptions

const subscriptions = client.subscriptions();
await subscriptions.createPlan({
  name: 'Pro', amount: 1000, currency: 'KES', intervals: ['monthly']
});
await subscriptions.listPlans();
await subscriptions.create({
  plan_id: 'plan_123', gateway: 'stripe',
  customer: { external_customer_id: 'cust_123', email: '[email protected]' }
});
await subscriptions.list('plan_123', 'cust_123');
await subscriptions.cancel('sub_123');

Managed recurring billing

Checkout uses exact integer minor-unit prices (15000 = EUR 150.00), a month or year interval, and a quantity. The SDK does not convert prices or apply discounts. Responses are unwrapped data maps/objects.

| Method | Purpose | |---|---| | checkout | Create an exact-price recurring checkout | | expireCheckout | Expire an abandoned checkout | | billingResource | Read a managed billing resource | | changeQuantity | Change the current quantity | | scheduleQuantity | Schedule a quantity change | | clearScheduledQuantity | Remove a scheduled quantity change | | setCancelAtRenewal | Set cancellation at renewal; pass false to clear it | | setTrialEnd | Move the next charge to a new trial end | | endTrial | End a trial now and charge the full period | | billingOperation | Recover the durable result by idempotency key | | paymentMethodPortal | Create a short-lived hosted payment-method link | | billingPaymentHistory | Read one page of captured payments and adjustments |

import type { ManagedCheckout } from 'tinker-payments-node-sdk';

const billing = client.subscriptions();
const checkout: ManagedCheckout = {
  external_customer_id: 'workspace-123', email: '[email protected]', name: 'Buyer',
  currency: 'EUR', unit_amount: 15000, quantity: 3, interval: 'year',
  return_url: 'https://example.com/billing', idempotency_key: 'checkout-123'
};
await billing.checkout(checkout);
await billing.changeQuantity('resource-id', 4, 'quantity-123');
await billing.setCancelAtRenewal('resource-id', false, 'resume-123');
await billing.paymentMethodPortal('resource-id', 'https://example.com/billing', 'portal-123');
const page = await billing.billingPaymentHistory('resource-id', '', 0);
// After an uncertain command response, recover using the SAME key:
const operation = await billing.billingOperation('quantity-123');

unit_amount, quantity and snapshot must be JavaScript safe integers. Fractional or unsafe values are rejected before sending. ManagedCheckout is an exported TypeScript interface using the API's snake_case fields. Custom fetch responses may expose headers.get(name) to preserve request IDs and Retry-After; native fetch does so.

Trials with a required payment method

Set trial_days (1 to 730) or an exact trial_end (a Date, 48 hours to 730 days ahead; cannot be combined with trial_days) to collect a card at checkout and charge first when the trial ends. The SDK sends trial_days only when it is above 0 and trial_end only when set, so checkouts without a trial keep working against servers released before trial support. A gateway without trial support returns HTTP 422 TRIAL_UNSUPPORTED with outcome: not_applied. Limiting a customer to one trial is the app's responsibility.

await billing.checkout({ ...checkout, idempotency_key: 'checkout-456', trial_days: 14 });

Expiring an abandoned checkout

An abandoned checkout link can still be completed later and create a second subscription. expireCheckout(id, key) expires an open checkout so it can no longer be paid. It has three outcomes: an open checkout becomes expired (status: "expired", managed_state.status: "canceled"); an already expired checkout is returned unchanged, so retrying is safe; a completed checkout returns HTTP 409 CHECKOUT_COMPLETED with outcome: not_applied, in which case read the checkout with billingResource and use its subscription. A non-checkout ID returns HTTP 400 INVALID_ID. An expired checkout sends no subscription.cancelled webhook.

await billing.expireCheckout('checkout-id', 'expire-checkout-1');

Changing the trial end

setTrialEnd(id, end, key) moves the next charge to end (1 hour to 730 days ahead). A trialing subscription can move earlier or later; a paid (active) subscription only to a time on or after current_period_end, and it then becomes trialing with nothing charged. Clear scheduled seat changes first. endTrial(id, key) ends a trial now and charges the full period immediately; if the charge fails the subscription becomes past_due. State-dependent rejections return HTTP 502 BILLING_GATEWAY_ERROR with outcome: not_applied.

await billing.setTrialEnd('resource-id', new Date(Date.now() + 30 * 86_400_000), 'trial-end-1');
await billing.endTrial('resource-id', 'end-trial-1');

Ending a trial early sends invoice.paid and payment.updated (not subscription.renewed). Extending a paid subscription produces a €0 invoice.paid.

Checkout and billing resource responses include a provider-neutral managed_state object: status (pending, trialing, active, past_due, paused or canceled), trial_ends_at, current_period_end and cancel_at_period_end. Read it instead of raw provider fields; grant access while status is trialing or active and current_period_end is in the future. Managed subscription webhooks carry billing_status and trial_ends_at, and subscription.trial_will_end arrives up to three days before the trial ends. The trial's €0 invoice sends invoice.paid with amount 0 and is not a payment.

Use a stable key per mutation and reuse it with the same payload after a lost response. Recover through the operation lookup; missing or pending results remain uncertain. Only an explicit outcome of not_applied confirms no mutation.

The payment-method portal changes payment details only; quantity, price and overdue invoice payment are separate actions. It requires an app-owned managed subscription with a dedicated customer. Reuse the operation key after a lost response and use a new key for a deliberately new session after expiry.

For payment history, keep the returned snapshot across next_cursor pages until has_more is false. Amounts are integer minor units and timestamps are UTC RFC3339. History covers captured cash, tax-exclusive eligible amounts, refunds, and dispute principal withdrawals/reinstatements (reverses_id identifies the reversed withdrawal).

Quarantine items with reconciliation_reason and verify every page before committing a scan. The snapshot is an event cutoff, not an immutable provider snapshot: rescan from the first page to discover later adjustments, then apply facts idempotently by payment/adjustment ID. Zero-cash invoices advance pagination without an item. Processing/dispute fees are excluded from eligible principal; currency conversion and customer credits need explicit reconciliation.

Refunds, wallet withdrawals and checkout cancellation

The app owner must enable withdrawal requests in app settings. A payout is a withdrawal from the merchant's wallet to its own active, verified payout method. Amounts use currency units: withdrawing JPY 100 with a fee of 2 reserves 100 and pays 98. Obtain a quote and pass its fee_amount as expected_fee_amount; a fee conflict includes a refreshed quote in the structured API error. No method accepts arbitrary recipient banking details.

Supply a stable Idempotency-Key for every refund, payout or cancellation request. Keys are trimmed and must be 1–255 UTF-8 bytes. Reuse the same key and semantic request after transport failures; a changed request conflicts. A replay returns the original resource and fee snapshot. Start a new key for a new intended operation.

A refund's pending_review/approved state and a payout's pending_approval/approved state are not completion. Only refunded, paid or provider-confirmed cancelled establishes completion. Preserve cancellation outcome_unknown and poll by ID; do not blindly submit another request. Unsupported cancellation raises HTTP 422 CHECKOUT_CANCELLATION_UNSUPPORTED and retains the persisted request_id/status in error details. Lifecycle conflicts use HTTP 409; replaying a persisted rejection retains its 422/409 result.

Refund history includes refundable amounts and refund_capability. Cancellation history includes capability, the newest 100 requests and has_more; retrieve older known requests by ID. Payout listing accepts page, per_page (maximum 100), status and currency. Reads are scoped to the app; payout reads show only requests attributed to that app.

const tx = client.transactions();
const review = await tx.refund(paymentId, { reason: 'requested_by_customer', amount: 100 }, 'refund-order-42');
console.log(review.refund.status);
const destinations = await client.payouts().methods();
const destination = destinations.find(method => method.status === 'active' && method.is_verified);
if (!destination) throw new Error('Configure a verified payout method in the dashboard');
const quote = await client.payouts().quote({ payout_method_id: destination.id, amount: 100, currency: destination.currency });
const requested = await client.payouts().requestPayout({ payout_method_id: destination.id, amount: 100, currency: destination.currency, expected_fee_amount: quote.fee_amount }, 'withdrawal-42');
console.log((await client.payouts().get(requested.withdrawal.id)).status);
try {
  const result = await tx.cancelCheckout(unpaidPaymentId, { reason: 'Customer left checkout' }, 'cancel-order-42');
  console.log((await tx.getCancellation(unpaidPaymentId, result.cancellation.id)).status);
} catch (error) {
  if (error instanceof errors.ApiError) console.log(error.errorCode, error.details);
  else throw error;
}

transactions() also exposes refunds, getRefund and cancellations; payouts() exposes list for paginated tracking. Public request and result interfaces are exported from the package. errors.ApiError.details preserves the full response envelope, including nested error.details.

Cancellation resources expose payment_conflict when later successful payment evidence conflicts with expiry. Treat successful payment as authoritative and use the refund workflow where appropriate.

Webhooks

const event = client.webhooks().handle(rawBody);
if (!client.webhooks().verifySignature(event, process.env.TINKER_WEBHOOK_SECRET)) {
  throw new Error('Invalid webhook signature');
}
const transaction = event.toTransaction(); // null for setup/lifecycle events

Verify the signature before consuming fields or converting the event to a transaction; reuse the parsed event rather than reading the request body twice. Deduplicate by event ID. The SDK verifies HMAC-SHA256 over the envelope excluding security, using the server's field order, map ordering and escaping.

After verification, acknowledge setup.webhook_test (source: onboarding) with HTTP 200, 202 or 204; it needs no payment lookup or fulfillment. The signed lifecycle events refund.updated.v1, withdrawal.updated.v1 and checkout.cancellation.updated.v1 carry data.resource with source: payment. Their typed resources never convert into payment transactions. Poll request IDs if delivery is delayed.

Errors and response metadata

API errors retain httpStatus, errorCode, providerCode, requestId, retryAfter, outcome and the full envelope in details. Request IDs prefer the response body over X-Request-ID; Retry-After remains its original string. Non-JSON HTTP failures retain status/headers and an uncertain outcome. Only an explicit not_applied outcome confirms no mutation. Managers unwrap response data; use getLastMeta() for request metadata and client.getLastAuthMeta() for authentication metadata.

try {
  await client.transactions().query({ reference: 'TP-REF-123', gateway: 'mpesa' });
} catch (error) {
  if (error instanceof errors.ApiError) console.error(error.errorCode, error.details);
  else throw error;
}

Development

npm ci
npm run lint
npm test
npm pack --dry-run

npm test builds dist and checks the public declarations.