Quickstart
Three calls take a buyer from an empty page to a paid order. The SDK handles tokenization in an isolated context, so card data never touches your server and you stay out of PCI scope.
// Load session, tokenize, pay. That's it. import { createClient } from '@ownaris/headless' const ownaris = createClient({ storeId: 'store_x1' }) const session = await ownaris.checkout.load(token) const card = await ownaris.payment.tokenize(input) await ownaris.payment.pay({ session, card })
Install
The package ships ESM, CJS, and TypeScript declarations. No peer dependencies.
npm i @ownaris/headless
# or
pnpm add @ownaris/headless
yarn add @ownaris/headlessFor a page with no build step, load the bundle directly:
<script type="module"> import { createClient } from 'https://cdn.ownaris.io/headless/v1/index.js' </script>
Authentication
Two key types, two environments. The publishable key is safe in the browser and can only load sessions and tokenize cards. The secret key is server-only and can move money, read customers, and mutate orders.
| Key | Prefix | Scope | Where |
|---|---|---|---|
publishable | pk_live_… / pk_test_… | Load session, tokenize | Browser |
secret | sk_live_… / sk_test_… | Full API | Server only |
webhook | whsec_… | Signature verification | Server only |
const ownaris = createClient({ storeId: 'store_x1', secretKey: process.env.OWNARIS_SECRET_KEY, })
Checkout sessions
A session is the single source of truth for a purchase in flight: line items, totals, tax, shipping, the customer, and any upsell offers attached to it. You create it on the server and hand a short-lived token to the browser.
Create on the server
const { token } = await ownaris.checkout.create({ items: [{ sku: 'hydra-serum', quantity: 2 }], shipping: 'priority', currency: 'usd', customer: { email: 'jordan@store.com' }, })
Load in the browser
load() resolves the token into a live session object with computed totals. It re-resolves whenever you mutate the session, so your UI always renders server-authoritative amounts.
const session = await ownaris.checkout.load(token) session.total // 6890 — minor units, always session.currency // 'usd' session.offers // upsells eligible for this cart await session.addOffer('night-cream') // 1-click bump await session.setShipping('standard')
6890 is $68.90. Never send floats.Payments & routing
Tokenize the card, then pay the session. Routing, cascade, and retry logic live in your dashboard rules — the same pay() call goes through whichever processor wins the route.
const card = await ownaris.payment.tokenize({ number: input.number, exp: input.exp, cvc: input.cvc, }) const result = await ownaris.payment.pay({ session, card }) if (result.status === 'succeeded') { window.location = '/thank-you?order=' + result.orderId } if (result.status === 'requires_action') { await ownaris.payment.handleAction(result) // 3DS challenge }
Result statuses
| Status | Meaning | Next step |
|---|---|---|
succeeded | Authorized and captured. | Redirect to confirmation. |
requires_action | 3DS or issuer challenge pending. | Call handleAction(). |
cascading | Declined; a fallback processor is being tried. | Keep the spinner. A terminal status follows. |
failed | All routes declined. | Show result.decline.message, let them retry. |
Payment methods
Apple Pay and Google Pay mount as drop-in elements and settle through the same routing layer.
await ownaris.payment.mountWallets('#wallets', { session, methods: ['apple_pay', 'google_pay'], onResult: result => console.log(result.status), })
Customers & events
Every payment event writes to one customer record — the same record your email and SMS flows read from. There is no sync step, and no second identity to reconcile.
const customer = await ownaris.customers.get('cus_8f21') customer.ltv // 41200 customer.status // 'active' | 'dunning' | 'churned' customer.timeline // ordered payment + messaging events await ownaris.customers.emit(customer.id, { type: 'cart.abandoned', data: { sessionId: session.id }, })
Emitted events are what your flows subscribe to. Because the payment engine emits into the same stream, a recovery flow fires on an actual decline — not on a browser heuristic.
Webhooks
Register an endpoint in Settings → Developers → Webhooks. Every delivery is signed; verify it before trusting the body.
app.post('/webhooks/ownaris', async (req, res) => { const event = ownaris.webhooks.verify({ payload: req.rawBody, signature: req.headers['ownaris-signature'], secret: process.env.OWNARIS_WEBHOOK_SECRET, }) if (event.type === 'payment.recovered') { await fulfill(event.data.orderId) } res.sendStatus(200) })
| Event | Fires when |
|---|---|
order.paid | A session is authorized and captured. |
offer.accepted | A bump or post-purchase upsell is taken. |
payment.declined | A route declines, before cascade. |
payment.recovered | A retry or cascade attempt succeeds. |
subscription.renewed | A recurring charge settles. |
subscription.churned | Dunning is exhausted and the sub ends. |
Deliveries retry with exponential backoff for 24 hours. Respond 2xx fast and do the work asynchronously — and make your handler idempotent on event.id.
SDK reference
| Method | Context | Returns |
|---|---|---|
createClient(config) | Both | Client instance |
checkout.create(input) | Server | { token, sessionId } |
checkout.load(token) | Browser | Session |
session.addOffer(sku) | Browser | Session |
session.setShipping(rate) | Browser | Session |
payment.tokenize(input) | Browser | Card token |
payment.pay({ session, card }) | Browser | Payment result |
payment.handleAction(result) | Browser | Payment result |
payment.mountWallets(el, opts) | Browser | void |
customers.get(id) | Server | Customer |
customers.emit(id, event) | Server | void |
webhooks.verify(input) | Server | Event |
Errors
Every thrown error carries a stable code. Branch on the code, show message to the buyer only when it is a decline.
| Code | HTTP | Fix |
|---|---|---|
invalid_key | 401 | Wrong environment, or a rolled key. |
session_expired | 410 | Tokens live 30 minutes. Create a new session. |
card_declined | 402 | Terminal decline after cascade. Surface the message. |
no_route_available | 409 | No processor matches the rule set for this currency or method. |
rate_limited | 429 | Back off; the Retry-After header tells you how long. |
Build with AI
The SDK is designed to be generated, not hand-written. The surface is small, the naming is predictable, and the types are shipped in the package — which is most of what a coding model needs to get it right on the first pass.
Give the model the context file
Every published version exposes a flat text file of the full documentation, made for pasting into a prompt or pointing an agent at:
https://docs.ownaris.io/llms.txt # full reference, one file https://docs.ownaris.io/llms-small.txt # quickstart + types only
A prompt that works
Build a one-page checkout with @ownaris/headless. Rules: - Amounts are integers in minor units. Never use floats. - Create the session server-side, pass only the token to the client. - Handle all four statuses: succeeded, requires_action, cascading, failed. Keep the spinner on 'cascading'. - Never put the secret key in client code. Reference: https://docs.ownaris.io/llms.txt
Rules that keep generated code correct
- Minor units everywhere. The single most common generation bug is a float total.
- Split the keys. If a model puts
sk_in a browser bundle, the request is rejected — but fix the prompt, not the symptom. - Never terminate on
cascading. It is an intermediate state; a terminal status always follows. - Idempotent webhook handlers. Retries are guaranteed, duplicates are expected.
Stuck on something a model got wrong? Send us the prompt and the output — bad generations usually mean our docs are ambiguous, and we fix them.