SDK checkout
Published release: SDK 0.1.3 is publicly available on npm. Package installation does not activate new backend features.
The three npm packages
Section titled “The three npm packages”@kordiva/sdk— server-only Node client, using your secret Store API key for catalog,checkout.createTestin TEST,checkout.createHostedandpayments.confirmin LIVE.@kordiva/checkout— browser-safe exact-origin URL validation and checkout navigation, with no key or payment processing.@kordiva/react— React/Next.js Buy button that calls your protected backend and hands off the URL.
Typical flow: Buy button → your authenticated backend → Kordiva server SDK → existing Kordiva/Stripe hosted checkout → server-side confirmation. A checkout return URL is not payment confirmation. Your CMS storefront, direct API and Checkout Links remain supported without SDK.
- Authenticate the customer on your backend and authorize the cart. Select approved product UUIDs/variants and quantities; never accept a browser-supplied amount as the price.
- For LIVE, call
client.checkout.createHosted({ lineItems, idempotencyKey, checkoutLanguage: 'en' }). The SDK forcesstripe_hosted. The returned Kordiva/q/…handoff leads into the existing hosted flow; it is not necessarily a direct Stripe URL. - Persist the returned
sessionIdtogether with the stable attempt and authenticated cart ownership in protected durable storage. Serialize checkout creation per cart. Do not generate a new key just because a request timed out. - Pass only
{ url: session.checkoutUrl }to the browser and check the exact trusted origin. - After return, call
client.payments.confirm({ sessionId })on your server.pendingorexpiredis not paid. Authorize the customer to view that order before returning any confirmation details. - Fulfill through the appropriate Kordiva product flow. Individual tax/location/held-payment and digital-delivery guards still apply; a paid projection alone does not grant download access.
How does my site show successful payment? Kordiva receives Stripe’s signed events. Your server stores the hosted session UUID and the authenticated cart attempt. Your UI then calls a protected payment-status route on your own server, which calls LIVE client.payments.confirm({ sessionId, idempotencyKey }). Only a confirmed paid state and the applicable fulfillment permissions allow delivery. 0.1.3 also provides a separate signed Business lifecycle channel; use the Business guides below for recurring integration. Read payment status and webhook responsibilities for a tested protected Node status handler and bounded polling guidance.
No automatic POST retries occur, including with an idempotency key. Backend replay searches are bounded, key-specific and expire; they are not an atomic exactly-once guarantee. Reconcile uncertain results before an operator-controlled retry with the same payload/key. Key rotation changes the replay identity.
The tested handler below is a single-product integration example. Before public deployment, supply real session/cart authorization, a request-size limit, per-user rate limiting, a durable cart lock and reconciliation storage. It does not implement your entire commerce backend.
import type { Kordiva } from "@kordiva/sdk";export interface CheckoutRouteOptions { client: Kordiva; /** Product is selected from your approved server-side catalog. */ productId: string; storefrontOrigin: string; /** Authenticate your customer session AND authorize this cart; return a stable opaque actor ref. */ authorize: (request: Request) => Promise<string | null>;}/** Shared Web Request/Response handler for a Node backend or Next.js App Router. */export function createCheckoutRoute({ client, productId, storefrontOrigin, authorize,}: CheckoutRouteOptions) { return async function POST(request: Request): Promise<Response> { const headers = { "cache-control": "no-store" }; if (request.method !== "POST") return Response.json( { error: "Method not allowed" }, { status: 405, headers }, ); if (request.headers.get("origin") !== storefrontOrigin) return Response.json( { error: "Origin rejected" }, { status: 403, headers }, ); const actor = await authorize(request); if (!actor || !/^[A-Za-z0-9_-]{1,48}$/.test(actor)) return Response.json({ error: "Unauthorized" }, { status: 401, headers }); if (!request.headers.get("content-type")?.startsWith("application/json")) return Response.json( { error: "JSON required" }, { status: 415, headers }, ); const raw = await request.text(); if (raw.length > 1024) return Response.json( { error: "Request too large" }, { status: 413, headers }, ); let input: unknown; try { input = JSON.parse(raw); } catch { return Response.json({ error: "Invalid JSON" }, { status: 400, headers }); } const attempt = input && typeof input === "object" && "attempt" in input ? input.attempt : null; if (typeof attempt !== "string" || !/^[A-Za-z0-9_-]{8,64}$/.test(attempt)) return Response.json( { error: "Stable cart attempt required" }, { status: 400, headers }, ); try { if (client.environment === "TEST") { const checkout = await client.checkout.createTest({ productId, testSaleMarket: "AU", }); return Response.json({ url: checkout.url }, { headers }); } const checkout = await client.checkout.createHosted({ lineItems: [{ productId, quantity: 1 }], idempotencyKey: `${actor}:${attempt}`, checkoutLanguage: "en", }); return Response.json({ url: checkout.checkoutUrl }, { headers }); } catch { // Production host supplies per-actor rate limiting and a durable cart lock. return Response.json( { error: "Checkout unavailable. Reconcile this attempt before retrying.", }, { status: 502, headers }, ); } };}Related guides
Section titled “Related guides”SDK · Checkout · Security · API
Business integration
Section titled “Business integration”Recurring checkout and subscription reads → Signed Business events → Raw-body verification → Business refunds → API reference. Individual MoR follows its separate refund process.
Individual Merchant of Record: disclosure before Buy
Section titled “Individual Merchant of Record: disclosure before Buy”Your own site may keep its branding. For Individual sales, Kordiva, LLC is the legal seller / Merchant of Record; the creator provides digital content. Fetch the canonical disclosure on your backend before rendering Buy, and display the returned text and links next to that action. Business/TP returns a non-applicable result and keeps its existing merchant model.
GET /public/catalog/stores/:storeId/buyer-disclosure?locale=enAuthorization: Bearer <SERVER_ONLY_STORE_API_KEY>// Backend / Next.js server component only. Never import this module into a client bundle.import type { Kordiva } from "../../../../../../packages/kordiva-sdk/src/index.js";export async function loadBuyerDisclosure( client: Kordiva, locale: "mk" | "en",) { return client.buyerDisclosure.get({ locale });}import React from "react";import type { BuyerDisclosure } from "../../../../../../packages/kordiva-sdk/src/types.js";/** Presentation only: consume server-returned wording and links, never reconstruct legal policy. */export function BuyerDisclosureBlock({ disclosure,}: { disclosure: BuyerDisclosure;}) { if (!disclosure.applicable) return null; return ( <aside aria-label="Purchase information"> <p>{disclosure.shortDisclosure}</p> <nav aria-label="Purchase policies"> <a href={disclosure.links.purchaseTerms}>Purchase Terms</a>{" "} <a href={disclosure.links.refunds}>Refund Policy</a>{" "} <a href={disclosure.links.privacy}>Privacy</a>{" "} <a href={disclosure.links.support}>Purchase Support</a> </nav> </aside> );}const escape = (v: string) => v.replace( /[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[ c ]!, );export function renderBuyerDisclosureHtml(disclosure: BuyerDisclosure): string { if (!disclosure.applicable) return ""; return `<aside aria-label="Purchase information"><p>${escape(disclosure.shortDisclosure)}</p><nav aria-label="Purchase policies">${[ ["purchaseTerms", "Purchase Terms"], ["refunds", "Refund Policy"], ["privacy", "Privacy"], ["support", "Purchase Support"], ] .map( ([key, label]) => `<a href="${escape(disclosure.links[key as keyof typeof disclosure.links])}">${label}</a>`, ) .join(" ")}</nav></aside>`;}<!-- Plain HTML: your backend escapes and inserts the returned text and URLs. --><aside aria-label="Purchase information"> <p><!-- buyerDisclosure.shortDisclosure, escaped --></p> <a href="{{purchaseTerms}}">Purchase Terms</a> <a href="{{refunds}}">Refund Policy</a> <a href="{{privacy}}">Privacy</a> <a href="{{support}}">Purchase Support</a></aside>Use locale: "mk" for Macedonian checkout. Consume returned values; do not copy legal wording or construct policy URLs. The authenticated read uses the existing LIVE API-key boundary and may update existing key-use telemetry only. Do not expose the key in browser code. Tested backend, escaped HTML and React implementations are in apps/store/src/lib/examples/individual-buyer-disclosure.
Kordiva checkout independently displays the mandatory disclosure and requires acknowledgment plus a server-issued preflight proof. The external block does not replace this gate. Privacy is a notice; no statutory refund or cancellation rights are waived. A browser redirect does not prove payment: keep using payments.confirm() on your backend.
The new SDK method requires a future SDK release; current published versions may not contain it. Until then the read-only REST endpoint is the integration contract after an approved server deployment. Email delivery remains a separate follow-up.
Individual confirmation emails: planned follow-up
Section titled “Individual confirmation emails: planned follow-up”This change does not enable Kordiva email delivery. The current canonical Individual payment-success path finalizes the Order and returns before the shared email/PDF pipeline; Stripe receipts depend on separate provider configuration. Future buyer confirmation will use finalized Order.billingEmail and the immutable checkout locale, not seller country, Store email language or Stripe locale: auto. Creator notifications will independently use Store email language. Delivery must be queued through durable unique finalization transitions, not sent directly by the webhook. Bank-payout and funds-availability notifications remain separate.