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

@odematik/billing

v0.19.0

Published

Drop-in React + Next.js checkout for ödematik. One server handler, one button — the PSP iframe is rendered for you, payment is verified server-side, fulfillment runs in your code.

Readme

@odematik/billing

Drop-in React + Next.js checkout for ödematik. One server handler, one button — the PSP iframe (PayTR, iyzico, Stripe) is rendered for you, payment is verified server-side, fulfillment runs in your code.

  • 🔒 No publishable key. Your API key never leaves your server.
  • ⚛️ Works with React 18 and 19, ESM + CJS, full .d.ts
  • 🌍 SSR-safe
  • 🪝 Optional webhook handler auto-mounted (HMAC SHA-256)
  • 🧱 Zero runtime dependencies beyond React

Install

npm install @odematik/billing
pnpm add @odematik/billing

Quick start (Next.js — ~10 lines)

1. Get your API key. Sign in to your ödematik dashboard → Settings → API → "Yeni anahtar". The key starts with wk_.

2. Mount the server handler — define your products + fulfillment in one file:

// app/api/odematik/[...path]/route.ts
import { createOdematikHandler } from '@odematik/billing/next';

export const { GET, POST } = createOdematikHandler({
  products: {
    p100:         { amount: 500,  currency: 'TRY', name: '100 Coin paketi',  vat_rate: 20 },
    plan_monthly: { amount: 129,  currency: 'TRY', name: 'Aylık Üyelik',     vat_rate: 20 },
  },
  onPaid: async ({ productId, customer }) => {
    // Runs after the payment is verified server-side. Idempotent —
    // may fire twice (once from the browser's /verify, once from the
    // webhook). Use your own dedupe (e.g. unique payment_id).
    await fulfillOrder(customer.id, productId);
  },
});

3. Drop in the button anywhere in your React app:

'use client';
import { OdematikButton } from '@odematik/billing';

<OdematikButton
  productId="p100"
  customer={{ id: user.id, email: user.email }}
  onPaid={() => router.refresh()}
>
  100 Coin al — 500₺
</OdematikButton>

4. Env vars — one required, two optional:

ODEMATIK_API_KEY=wk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# ODEMATIK_API_BASE=https://api.odematik.com   # default; only set for self-hosted / staging
# ODEMATIK_WEBHOOK_SECRET=whsec_…              # set if you enable the webhook (see below)

That's it. The button opens a modal, the modal embeds the PSP iframe the backend returns, the buyer pays, and your onPaid runs server-side. Amount and currency are taken from the server-side products map — the browser only sends productId, so prices can't be tampered with.


Production checklist

Authenticate /checkout

Pass an authenticate callback so anonymous callers can't create sessions for arbitrary customers:

createOdematikHandler({
  authenticate: async (req) => {
    const user = await getUserFromRequest(req);
    if (!user) return null;   // null → 401
    return { customerId: user.id };
  },
  products: { ... },
  onPaid: async ({ customer }) => { ... },
});

Returning { customerId } is required if you want the checkout modal's preview screen to show the buyer's saved fatura bilgileri with a "Değiştir" link. The GET /billing endpoint refuses to look up records by browser-supplied id — it relies on the authenticated session to know who's asking. Returning anything else still passes /checkout and /verify auth (backwards compatible) but the saved- billing preview falls back to the empty-form flow.

The buyer still has to enter a real card to actually charge, so the worst case of skipping auth is resource exhaustion / fake payment links — but you should still gate this.

Enable webhooks

For belt-and-suspenders fulfillment (the /verify path can be skipped by a buyer who closes the tab), turn on webhooks:

  1. In your ödematik dashboard: Webhook Endpoints → Add
    • URL: https://your-app.com/api/odematik/webhook
    • Events: payment.success (and others if you care)
    • Copy the secret
  2. Set ODEMATIK_WEBHOOK_SECRET=... in your .env
  3. The bundled handler already accepts webhook POSTs at /api/odematik/webhook — no extra code needed. It verifies the HMAC SHA-256 signature timing-safely and dispatches payment.success to your onPaid.

First-purchase invoicing

ödematik refuses /charge without a billing block on the first purchase from a given customer_id (Turkish invoice law). Collect unvan / VKN / address / etc. up-front and forward via the button:

<OdematikButton
  productId="p100"
  customer={{
    id: user.id,
    email: user.email,
    billing: {
      unvan: 'ACME Yazılım Ltd. Şti.',
      vkn_tckn: '1234567890',
      adres: 'Test Cad. No: 1',
      il: 'İstanbul',
      ilce: 'Kadıköy',
    },
  }}
>
  Satın al
</OdematikButton>

Once a customer has a billing record stored, subsequent purchases don't need to repeat it. The checkout modal's preview screen will show a "Faturalandırma: {unvan} · VKN ••••1234 — Değiştir" row; tapping Değiştir opens the form prefilled with the saved values so the buyer can correct typos or use different fatura bilgisi for a one-off purchase. Submitting the override updates the stored record AND uses the new values on this invoice (always-update policy — there is no per-transaction override that leaves the saved record untouched).

Backend requirement. This preview row is populated by GET /api/odematik/billing, which proxies GET /customers/:id/billing on the ödematik API. The backend must:

  • Implement GET /customers/:id/billing{ billing: BillingInfo | null } (authorized by X-Api-Key; merchant only sees own customers).
  • On every POST /charge (and /subscribe) with a billing block, update the customer's saved billing record — don't treat it as per-transaction-only.

Lower-level API

If you don't use Next.js or need something custom, use the server client directly:

import { OdematikClient, verifyWebhookSignature } from '@odematik/billing/server';

const client = new OdematikClient({
  apiKey: process.env.ODEMATIK_API_KEY!,
  apiBase: process.env.ODEMATIK_API_BASE!,
});

const checkout = await client.charge.create({
  customer_id: 'user_42',
  email: '[email protected]',
  items: [{ name: '100 Coin', quantity: 1, unit_price: 500, vat_rate: 20 }],
});
// checkout.iframeUrl  → embed in browser
// checkout.payment_id → use to verify later

const payment = await client.payments.get(checkout.payment_id);
if (payment.status === 'success') { /* fulfill */ }

verifyWebhookSignature({ body, signature, timestamp, secret }) returns a boolean and can be used inside Express / Fastify / any other server. Pass the raw request body string (not the parsed JSON) and the headers X-Webhook-Signature + X-Webhook-Timestamp.


How payment flows

 buyer clicks <OdematikButton/>
   │
   ▼
 your handler  ── POST /charge ──▶  ödematik backend
                ◀── { iframeUrl, payment_id } ──
   │
   ▼
 modal embeds iframeUrl  →  PSP collects card directly
                              │
                              │   webhook to ödematik
                              ▼
                          ödematik backend updates payment
                              │
                              │   webhook to your handler
                              ▼
                          your handler verifies HMAC, runs onPaid
   │
 buyer sees success         (in parallel) iframe posts "success"
   │                               │
   │  ◀── browser sends /verify ──┤
   ▼
 your /verify runs onPaid (idempotent)

Card data never touches your server or ödematik's. The PSP collects it directly; ödematik only sees post-success metadata (last4, brand, payment_id).


Browser support

Modern evergreen browsers (Chrome, Edge, Firefox, Safari ≥ 14).

License

MIT © ödematik