JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 328
  • Score
    100M100P100Q92081F
  • License MIT

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.

Package Exports

  • @odematik/billing
  • @odematik/billing/next
  • @odematik/billing/package.json
  • @odematik/billing/server

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);
    return user;   // return null → 401
  },
  products: { ... },
  onPaid: async ({ customer }) => { ... },
});

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.


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: 'buyer@example.com',
  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