SDK webhooks
Published release: SDK 0.1.3 is publicly available on npm. Package installation does not activate new backend features.
Do I need a Stripe webhook to know my customer paid?
Section titled “Do I need a Stripe webhook to know my customer paid?”No — not for standard Kordiva hosted checkout. Kordiva processes signed Stripe notifications on its own servers and records the authoritative payment/order state. You do not configure a Stripe signing secret, Stripe webhook endpoint or Stripe account on your website.
0.1.3 adds signed Business Merchant lifecycle webhooks through a separate HMAC verifier. The existing one-time payment-confirmation flow below remains supported. For a custom Node.js, React or Next.js storefront, your authenticated server checks the existing LIVE Kordiva payment-confirmation API with @kordiva/sdk. Your front end calls your own server, never Kordiva with a secret key. A redirect to a success URL or an HTTP 200 from checkout creation does not prove a paid order.
- Call
client.checkout.createHosted()on the server. Persist the returnedsessionId, your authenticated buyer/cart identity and the stableidempotencyKeyin durable application storage before returning onlycheckoutUrlto the browser. The small/api/checkoutsample elsewhere returns just the URL: extend it with this persistence before real production adoption. - After the buyer returns, your frontend calls a protected
GET /api/payment-status?attempt=...endpoint on your own site. Authenticate the shopper; look up an attempt owned by that shopper, never trust asessionIdfrom the query string. - On your server, call
client.payments.confirm({ sessionId, idempotencyKey })with the references from your stored record. Acceptpaidonly whenstatus === 'paid',isPaid === true, the expected identity matches and a paid Kordiva order is bound. Otherwise displaypending,expiredor a retryable unavailable state. - Optionally poll your own backend about every 5 seconds for at most a minute while the shopper waits; then show “Payment processing — check again later.” Use a bounded, authenticated backend reconciliation task for abandoned tabs or slow Stripe events. Do not loop forever or create another charge on timeout.
- Trigger your own site’s receipt, access or delivery once, from its authorized server-side order workflow, only after verified payment and any required Individual fulfillment/entitlement checks. Payment status alone does not authorize an Individual download.
Payment outcomes to handle
Section titled “Payment outcomes to handle”The confirmation API intentionally has a small state model:
| Confirmation | What your backend should do |
| --- | --- |
| paid + isPaid === true + orderId | Verify the stored attempt belongs to the authenticated buyer, then fulfill exactly once. |
| pending | Do not fulfill and do not create another charge. Recheck the same stored attempt with bounded polling or backend reconciliation. |
| expired | Treat that checkout attempt as closed. A new checkout requires a new authorized attempt. |
| Request/API unavailable | Show a temporary status and reconcile later. Do not turn a network/API error into paid or a second checkout. |
SDK 0.1.3 does not expose a generic failed payment-confirmation state. A declined or failed card attempt stays in the hosted payment flow; merchant confirmation exposes only paid, pending or expired. Never infer success from the browser return path.
The following tested Node route uses the same stable actor:attempt identity as the checkout example. You must supply your own real customer authentication and durable findOwnedAttempt lookup; it intentionally provides neither a database nor a customer login service. For best expiry handling, persist the sessionId returned by createHosted. When only idempotencyKey is known, the API can report paid/pending but cannot always infer session expiry.
import type { Kordiva } from "@kordiva/sdk";
/** Example only: implement both callbacks using a durable cart-attempt store * and your existing customer authentication. Never send a Store API key to a browser. */export interface PaymentStatusRouteOptions { client: Kordiva; authorize: (request: Request) => Promise<string | null>; findOwnedAttempt: ( actor: string, attempt: string, ) => Promise<{ sessionId?: string | null } | null>;}const ATTEMPT = /^[A-Za-z0-9_-]{8,64}$/;const ACTOR = /^[A-Za-z0-9_-]{1,48}$/;const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
/** Place behind GET /api/payment-status on a protected Node backend. * The browser polls YOUR authenticated endpoint; Kordiva processes Stripe events. * This endpoint is not a merchant Stripe webhook or an outgoing push notification. */export function createPaymentStatusRoute({ client, authorize, findOwnedAttempt,}: PaymentStatusRouteOptions) { return async function GET(request: Request): Promise<Response> { const headers = { "cache-control": "no-store" }; if (request.method !== "GET") return Response.json({ error: "Method not allowed" }, { status: 405, headers }); if (client.environment !== "LIVE") return Response.json({ error: "LIVE only" }, { status: 403, headers });
const attempt = new URL(request.url).searchParams.get("attempt"); if (!attempt || !ATTEMPT.test(attempt)) return Response.json({ error: "Invalid attempt" }, { status: 400, headers });
try { const actor = await authorize(request); if (!actor || !ACTOR.test(actor)) return Response.json({ error: "Unauthorized" }, { status: 401, headers });
const owned = await findOwnedAttempt(actor, attempt); if (!owned) return Response.json({ error: "Not found" }, { status: 404, headers });
const sessionId = owned.sessionId ?? null; if (sessionId !== null && !UUID.test(sessionId)) throw Error("Invalid stored session identity");
// Matches the actor:attempt identity used by the published checkout example. // Save createHosted().sessionId for accurate expiration results when possible. const idempotencyKey = `${actor}:${attempt}`; const payment = await client.payments.confirm({ ...(sessionId ? { sessionId } : {}), idempotencyKey, }); if ( payment.idempotencyKey !== idempotencyKey || (sessionId && payment.sessionId !== sessionId) ) throw Error("Payment identity mismatch");
if (payment.status === "paid" && payment.isPaid && payment.orderId) return Response.json({ status: "paid", isPaid: true }, { headers }); if (payment.status === "expired" && !payment.isPaid) return Response.json({ status: "expired", isPaid: false }, { headers }); return Response.json({ status: "pending", isPaid: false }, { headers }); } catch { // Fail closed: return neither an invented success nor leaked payment data. return Response.json( { status: "unavailable", isPaid: false }, { status: 503, headers }, ); } };}TEST is different: checkout.createTest() creates an isolated TEST link; payments.confirm() is LIVE-only and rejects TEST keys. Inspect completed TEST orders in the Kordiva TEST dashboard and the Stripe TEST webhook evidence there. Successful TEST payments do not create LIVE orders or payouts.
Kordiva owns Stripe event processing for Kordiva payments. Do not copy Kordiva Stripe secrets into a merchant integration or replace its existing webhook handlers. Confirm merchant payments through the supported server-side confirmation endpoint. A browser return URL is not a trusted event.
Business endpoint setup is in CMS → Settings → API keys → Developers / Webhooks. The SDK verifies events but exposes no endpoint-registration/key-management method; see setup and delivery. Build a bounded backend reconciliation job where required; do not poll from an untrusted browser with a secret key.
An existing, separately approved agentic order channel uses P-256 signed HTTP messages. If Kordiva has explicitly enrolled your integration and provisioned the trusted public key/key ID, the server-only @kordiva/sdk/webhooks export provides verifyOrderWebhook:
- Accept POST only and read the unchanged raw bytes with a host body-size limit.
- Pass
body, requestheaders, the exact signed receiverurl, trustedpublicKeyPemandkeyId. Behind a proxy, use your configured canonical receiver URL; never trust forwarded host headers blindly. - Verify before acting. The verifier checks digest, signature coverage, key, route/query and a maximum five-minute validity window. It is not a Stripe signature verifier.
- Persist
eventRefand applyorderRevisionatomically before processing effects. Signatures do not provide persistent replay protection or guarantee ordered delivery. Acknowledge already processed events without duplicating work. - Rotate pinned public keys through a trusted operational channel; never fetch a key from an event-supplied URL.
This helper grants no agent identity, merchant enrollment, order access or payment authority. Its compatibility is tested against Kordiva’s actual signer.
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.