@ionicfi/sdk
v0.4.3
Published
Server-side TypeScript SDK for the Ionic payments API.
Downloads
534
Readme
@ionicfi/sdk
Server-side TypeScript SDK for the Ionic payments API. Create payments, manage customers and subscriptions, and verify webhooks with typed requests and responses.
Install
npm install @ionicfi/sdkRequires Node 18 or later. This is an ES module package; CommonJS projects
can require() it on Node 20.19+ / 22.12+, or use dynamic import() on
older runtimes.
Quickstart
Construct a client with a secret key and create a checkout session:
import { Ionic } from "@ionicfi/sdk";
const secretKey = process.env.IONIC_SECRET_KEY;
if (!secretKey) {
throw new Error("IONIC_SECRET_KEY is required and was not set");
}
const ionic = new Ionic({ token: secretKey });
const session = await ionic.checkout.sessions.create({
mode: "payment",
line_items: [{ price_id: "price_Ab1Cd2Ef3Gh4Ij5Kl6Mn7Op8", quantity: 1 }],
success_url: "https://example.com/return?session_id={CHECKOUT_SESSION_ID}",
cancel_url: "https://example.com/",
});
console.log(session.url); // redirect the buyer hereConnected accounts and payment fields
Use a Platform Connect secret key to read connected accounts:
const platform = new Ionic({ token: process.env.IONIC_CONNECT_SECRET_KEY });
const accounts = await platform.connect.connectedAccounts.list({ limit: 100 });
for await (const account of accounts) {
console.log(account.id, account.business_name);
}
const account = await platform.connect.connectedAccounts.retrieve({ id: accountId });Start a redirect authorization with platform.connect.accountAuthorizations.create()
and exchange the approved callback code with .exchange(). Preserve the original
state and PKCE verifier and validate the callback state before exchanging the code.
These endpoints use the Platform credential directly and do not accept Ionic-Account.
Use a merchant secret key with sessions:write to create a payment-fields session:
const merchant = new Ionic({ token: process.env.IONIC_SECRET_KEY });
const session = await merchant.tokenizationSessions.create({
parent_origin: "https://pay.example.com",
});Register that exact origin as a web domain for the merchant and key mode first.
Return the session to your browser integration and pass it to mountPaymentFields.
Sessions expire after 30 minutes; create a new one after expiry.
Webhooks
Verify inbound deliveries with webhooks.unwrap(). It checks the signature
and replay window, then returns a typed event. Pass the raw request body
exactly as received; a body that's been parsed and re-serialized by framework
middleware will not verify:
import { WebhookParseError, WebhookVerificationError, webhooks } from "@ionicfi/sdk";
app.post("/webhooks/ionic", async (req, res) => {
let event;
try {
event = webhooks.unwrap(req.rawBody, req.headers, process.env.IONIC_WEBHOOK_SECRET);
} catch (err) {
if (err instanceof WebhookVerificationError || err instanceof WebhookParseError) {
return res.status(400).send(`webhook rejected: ${err.message}`);
}
throw err;
}
if (event.type === "checkout.session.completed") {
await fulfill(event.data.object); // typed CheckoutSession
}
res.status(200).json({ received: true });
});An unverifiable delivery throws WebhookVerificationError (bad signature,
missing headers, stale timestamp) or WebhookParseError (authenticated body
that isn't a webhook envelope). Respond 400 in both cases so the sender's
retry logic kicks in. Never 200 a delivery you didn't process.
Fulfill from the webhook, not from the browser redirect. The buyer's
success_url redirect proves they returned to your site; it doesn't prove
the payment settled. Treat success_url/return handling as a fallback that
reconciles state via checkout.sessions.retrieve, and do the actual order
fulfillment when checkout.session.completed arrives.
The signing secret is shown once, in the response from
webhookEndpoints.create. Store it immediately; it isn't retrievable
afterward.
Typed imports
Every resource shape is exported alongside the client:
import type { PaymentIntent, CheckoutSession } from "@ionicfi/sdk";Request deadlines
timeoutInSeconds covers the HTTP request, retry waits, and reading the response
body for resource methods. The default is 60 seconds. A timeout throws
IonicApiTimeoutError; it does not prove a payment failed. Reconcile the
resource state before attempting another mutation. Caller cancellation through
abortSignal remains a separate IonicApiError.
For raw fetch() responses and streaming endpoints, the same deadline includes
all attempts and retry waits until the response is returned. The caller owns
reading or cancelling the returned stream; its body has no SDK deadline.
Caller abort signals still cancel that body after headers arrive. Raw fetch()
preserves the caller's abort reason; resource methods report cancellation as
IonicApiError. The SDK removes its abort listener when the body finishes,
errors, or is cancelled, so the same caller signal can be reused across requests.
Error handling
Every failed API call throws IonicApiError. Catch that one class and branch
on statusCode and the error body; it carries the parsed API error envelope
plus a requestId to quote when contacting support:
import {
CardError,
InvalidRequestError,
ApiError,
IonicApiTimeoutError,
} from "@ionicfi/sdk";
try {
await ionic.paymentIntents.confirm({ id: intentId });
} catch (err) {
// Check the timeout FIRST: IonicApiTimeoutError extends IonicApiError, so a
// broader check would swallow it.
if (err instanceof IonicApiTimeoutError) {
// No response arrived, which does NOT mean the operation failed: it may
// have committed. Reconcile by retrieving the resource rather than
// re-creating it, or you risk charging twice.
await reconcile(intentId);
} else if (err instanceof CardError) {
// err.detail.decline_code carries the issuer's reason.
showDeclineMessage(err.detail?.message);
} else if (err instanceof InvalidRequestError) {
// Retrying unchanged fails identically — fix the request.
log.error("bad request", err.detail?.code, err.requestId);
} else if (err instanceof ApiError) {
// On a mutating call this does NOT prove the operation did not happen.
await reconcile(intentId);
} else {
throw err;
}
}The hierarchy is CardError, InvalidRequestError, AuthenticationError,
PermissionError, NotFoundError, ConflictError, RateLimitError and
ApiError, all extending IonicApiError. There is exactly one of each in the
package: the per-resource names the API reference uses (BadRequestError,
UnauthorizedError, ...) are aliases of these, so IonicApi.checkout.BadRequestError
and InvalidRequestError are the same class and instanceof cannot pick a
wrong one.
Every error carries statusCode, a requestId worth quoting to support, and
detail — the typed error body (code, message, decline_code,
decline_type, retryable, doc_url), or undefined when the response had
no parsable envelope, as with a gateway 502. errorDetail(err) reads the same
value from an unknown.
The specific class comes from the statuses an endpoint documents.
InvalidRequestError, AuthenticationError, PermissionError,
RateLimitError and ApiError are documented on every endpoint, so those
always arrive as their class.
CardError, NotFoundError and ConflictError are documented only where the
endpoint can produce them: a list call cannot 404, and a customer lookup cannot
raise a card decline. Anything undocumented arrives as IonicApiError with the
correct statusCode and detail, so it is still catchable.
Reliability
Idempotency keys are automatic. Every mutating request (POST, PUT, PATCH, DELETE) carries an
Idempotency-Key; the SDK generates one per call when you don't supply your own, so the automatic retries below replay the original operation instead of repeating it (a second charge, a second refund). To also make your own application-level retries safe, pass the same"Idempotency-Key"field with the same request body: the original response is replayed instead of the operation running twice.Retries. Requests that fail with
408,429,502,503, or504are retried automatically with backoff. A bare500is never retried: on a mutating endpoint it can mean the operation already committed.Auto-pagination. List calls on
paymentIntents,charges,refunds,customers,invoices,subscriptions,creditNotes,paymentMethods,setupIntents,catalog.products,catalog.prices,paymentLinks, andcheckout.sessionsandwebhookEndpointsreturn aPagethat exposeshasNextPage()/getNextPage()and is directlyfor await-able. Iteration advances withstarting_aftercursors, so rows created while you page are never skipped or repeated:const page = await ionic.paymentIntents.list({ limit: 100 }); for await (const intent of page) { console.log(intent.id); }If the first request carried an
offset, continuation requests drop it: the API rejectsoffsetcombined withstarting_after, and iteration advances by cursor alone. Webhook-endpoint lists preserve their legacy full-result behavior whenlimitis omitted; the returnedPageis terminal in that case.
Security
- The secret key (
sk_v1_test_…/sk_v1_live_…) authenticates every request in this SDK. Keep it server-side only, read from an environment variable. Never ship it to a browser or commit it to source control. - The webhook signing secret (
whsec_…) is returned once, at endpoint creation. If you lose it, callwebhookEndpoints.rotateSecret()for a new one; the previous secret is revoked immediately, and it isn't recoverable otherwise. - Test keys (
_test_) returnlivemode: falseobjects and never move real money. Live keys (_live_) do. Keep the two separate in your environment configuration and never charge a live key from a test script.
Docs
Full API reference and guides: docs.ionicfi.com
