DOCUMENTATION

Load a session, tokenize, pay.

Ownaris is the headless backend for checkout, payments, and CRM. One client, three namespaces — and no assumptions about your frontend.

V1 · UPDATED AUGUST 2026

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.

checkout.js
// 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 })
01Create a storeCreate your account, then copy the store ID and publishable key from Settings → Developers.
02Connect a processorStripe, NMI, Airwallex and 50+ others. Routing rules are configured in the dashboard, not in code.
03Build the surfaceReact, Vue, plain HTML — the SDK is framework-agnostic and ships its own types.
04Go liveSwap the test key for the live key. Same code path, same payloads.

Install

The package ships ESM, CJS, and TypeScript declarations. No peer dependencies.

terminal
npm i @ownaris/headless
# or
pnpm add @ownaris/headless
yarn add @ownaris/headless

For a page with no build step, load the bundle directly:

index.html
<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.

KeyPrefixScopeWhere
publishablepk_live_… / pk_test_…Load session, tokenizeBrowser
secretsk_live_… / sk_test_…Full APIServer only
webhookwhsec_…Signature verificationServer only
Never ship a secret key to the client. If one leaks, roll it from Settings → Developers — the old key stops working immediately.
server.js
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

POST /v1/checkout/sessions
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.

checkout.js
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')
Amounts are always integers in the currency's minor unit — 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.

pay.js
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

StatusMeaningNext step
succeededAuthorized and captured.Redirect to confirmation.
requires_action3DS or issuer challenge pending.Call handleAction().
cascadingDeclined; a fallback processor is being tried.Keep the spinner. A terminal status follows.
failedAll 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.

wallets.js
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.

customers.js
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.

webhook.js
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)
})
EventFires when
order.paidA session is authorized and captured.
offer.acceptedA bump or post-purchase upsell is taken.
payment.declinedA route declines, before cascade.
payment.recoveredA retry or cascade attempt succeeds.
subscription.renewedA recurring charge settles.
subscription.churnedDunning 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

MethodContextReturns
createClient(config)BothClient instance
checkout.create(input)Server{ token, sessionId }
checkout.load(token)BrowserSession
session.addOffer(sku)BrowserSession
session.setShipping(rate)BrowserSession
payment.tokenize(input)BrowserCard token
payment.pay({ session, card })BrowserPayment result
payment.handleAction(result)BrowserPayment result
payment.mountWallets(el, opts)Browservoid
customers.get(id)ServerCustomer
customers.emit(id, event)Servervoid
webhooks.verify(input)ServerEvent

Errors

Every thrown error carries a stable code. Branch on the code, show message to the buyer only when it is a decline.

CodeHTTPFix
invalid_key401Wrong environment, or a rolled key.
session_expired410Tokens live 30 minutes. Create a new session.
card_declined402Terminal decline after cascade. Surface the message.
no_route_available409No processor matches the rule set for this currency or method.
rate_limited429Back 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:

llms.txt
https://docs.ownaris.io/llms.txt        # full reference, one file
https://docs.ownaris.io/llms-small.txt  # quickstart + types only

A prompt that works

prompt
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.