SDKs
Flint ships two official server SDKs: @flintpay/node for TypeScript and JavaScript, and flintpay/flint for PHP. Both are generated from the same pinned API contract that produces the API reference, so all 497 public operations are typed methods the day they ship, under the names and field names you see in the reference. Nothing is hand-ported and nothing lags behind the API.
Each method returns the resource itself. Create an order and you get the order back. Path IDs are positional, request fields are flat, money is an exact string, and TypeScript knows which credential every operation accepts before you run it. A first payment fits in one file, shown below.
npm
@flintpay/node
npm install @flintpay/node
Node.js 22+ as ESM, with declarations for TypeScript 5.9+. Plain JavaScript works without TypeScript. Package page on npm.
Packagist
flintpay/flint
composer require flintpay/flint:0.3.0-beta.1
PHP 8.2+ with the cURL and JSON extensions, and no framework dependency. Package page on Packagist.
Every request carries your API key, so the SDKs belong on a server, a worker, or another trusted backend. A key never goes in browser or mobile code.
Create a client#
A client takes the API hostname and your key. It reads no environment variables and discovers no credentials: what you pass is what it sends. Constructing one makes no network calls.
import { Client } from "@flintpay/node";
const flint = new Client({
baseUrl: "https://api.withflintpay.com",
apiKey: process.env.FLINT_API_KEY!,
});
<?php
require __DIR__ . '/vendor/autoload.php';
use Flint\{Client, ClientOptions};
$flint = new Client(new ClientOptions(
baseUrl: 'https://api.withflintpay.com',
apiKey: getenv('FLINT_API_KEY'),
));
The client owns one reusable cURL handle. Call $flint->close() when a script is done with it, and give each thread or fiber its own client.
The key decides the environment. A flint_test_ key runs against its sandbox and a flint_live_ key runs against your live account, on the same hostname with the same code.
Take a payment#
Create an order, open a Flint-hosted checkout for it, send the buyer there, and read the paid order back. Three calls, each returning the object you asked for.
const order = await flint.orders.create(
{
line_items: [
{
name: "Design consultation",
quantity: "1",
unit_price_money: { amount: "2500", currency: "USD" },
},
],
},
{ idempotencyKey: "first-payment-order-001" },
);
const launch = await flint.checkoutSessions.create(
{
order_id: order.order_id,
redirects: {
success_redirect_url: "https://example.com/thanks",
cancel_redirect_url: "https://example.com/checkout",
},
},
{ idempotencyKey: "first-payment-checkout-001" },
);
// Send the buyer here, unchanged. The URL carries the token that admits them.
const hostedUrl = launch.checkout_access.hosted_url;
// After they pay, the order says so.
const paid = await flint.orders.get(order.order_id);
console.log(paid.payment_status); // "paid"
use Flint\RequestOptions;
$order = $flint->orders->create([
'line_items' => [[
'name' => 'Design consultation',
'quantity' => '1',
'unit_price_money' => ['amount' => '2500', 'currency' => 'USD'],
]],
], new RequestOptions(idempotencyKey: 'first-payment-order-001'));
$launch = $flint->checkoutSessions->create([
'order_id' => $order->order_id,
'redirects' => [
'success_redirect_url' => 'https://example.com/thanks',
'cancel_redirect_url' => 'https://example.com/checkout',
],
], new RequestOptions(idempotencyKey: 'first-payment-checkout-001'));
// Send the buyer here, unchanged. The URL carries the token that admits them.
$hostedUrl = $launch->checkout_access->hosted_url;
// After they pay, the order says so.
$paid = $flint->orders->get($order->order_id);
echo $paid->payment_status; // paid
orders.create returns the order, with its totals already computed. checkoutSessions.create returns the session and the buyer's access to it, and hosted_url is the page that collects the order's outstanding balance. Pay it with a test card, and orders.get comes back paid. In production, fulfill from the order.paid webhook rather than the redirect, since a buyer can close the tab before it fires.
Accept your first payment walks the same three calls over raw HTTP, with the hosted page and the response bodies.
What a call looks like#
Every method follows the same shape, so once you have made one call you have made them all.
- Path IDs come first, in URL order.
flint.orders.get(orderId),flint.customers.update(customerId, { email }),flint.orders.createPaymentIntent(orderId, { amount_money }). - Then one flat params object. Body fields, query parameters, and headers sit together under the API's wire names, so
unit_price_moneyandpage_size, never camelCase. - Then request options. A plain object in Node, a
RequestOptionsin PHP. Idempotency keys, timeouts, and credential overrides live here. - You get the payload back.
orders.getreturns the order. Operations whose response names more than one thing return that named result:orders.payreturns{ order, payment_attempt }, andorders.createPaymentIntentreturns{ payment_intent, payment_collection }. The reference states the return for each method.
When you need the HTTP layer, every method has a WithResponse companion that returns the complete body, the response metadata, and the raw text:
const response = await flint.orders.getWithResponse(orderId);
response.body.data.order_id;
response.meta.status; // 200
response.meta.requestId; // quote this in a support request
Upgrading from the 0.2 beta
Methods moved from client.api onto resources, the body wrapper is gone, and calls return the payload directly instead of a result envelope. The migration guide maps the old calls to the new ones.
Exact money#
Amounts, quantities, and other 64-bit integers are strings in the SDK: "2500", not 2500. A value never passes through floating point between your code and Flint, and the generated types hold you to it. They are still integers in the currency's minor unit, so "2500" is $25.00. Money & currency covers the model.
Errors#
A failed call throws SdkError. It carries Flint's error code, the HTTP status, and the request ID, plus one field that matters more than the rest: outcome, which says whether the request reached Flint.
import { SdkError } from "@flintpay/node";
try {
await flint.refunds.create(
{
payment_intent_id: paymentIntentId,
amount_money: { amount: "500", currency: "USD" },
reason: "requested_by_customer",
},
{ idempotencyKey: savedKey },
);
} catch (error) {
if (!(error instanceof SdkError)) throw error;
console.error(error.code, error.meta?.status, error.meta?.requestId);
if (error.outcome === "unknown") {
// The refund may exist. Read it back before sending the same key again.
}
}
use Flint\{RequestOptions, SdkError};
try {
$flint->refunds->create([
'payment_intent_id' => $paymentIntentId,
'amount_money' => ['amount' => '500', 'currency' => 'USD'],
'reason' => 'requested_by_customer',
], new RequestOptions(idempotencyKey: $savedKey));
} catch (SdkError $error) {
error_log($error->errorCode . ' ' . ($error->meta['requestId'] ?? ''));
if ($error->outcome === 'unknown') {
// The refund may exist. Read it back before sending the same key again.
}
}
not_sent: the request never left the client, so nothing changed. Fix the input and call again.response: Flint answered, andcodeis the API's error code, such asINVALID_PAGE_TOKEN. Error handling lists the ones to branch on.unknown: the request may have reached Flint. Read the resource back before resubmitting with the same key.
A declined card is not an error. It arrives as a payment attempt in the response, which Handling declines covers.
Retries and idempotency#
Every call is exactly one request. The SDK never retries on its own and never invents an idempotency key, so a timeout can never turn into two charges. Pass the key you saved for the action in request options, and the SDK sends it on that call:
await flint.orders.create(params, { idempotencyKey: savedKey });
Retrying is then a matter of calling again with the same key and the same input. Every write that declares Idempotency-Key in the contract, 278 of them, accepts the key this way, and a required key is checked before the request leaves your process. Idempotency covers choosing keys and how long Flint honors them.
Request options#
The last argument to any method carries settings for that call alone. Nothing here mutates the client.
idempotencyKeystringSent as Idempotency-Key. Save it with the action before the first attempt and reuse it on retry.
timeoutMsnumberBounds one attempt, including reading the body. Defaults to 10000.
deadlineMsnumberBounds the whole call, including every page of a paginated iteration. Defaults to 30000. It is a duration, not a timestamp.
maxPagesnumberCaps a listItems or listPages iteration at this many pages. maxItems caps the items instead.
headersobjectExtra headers for this request only.
apiKeystringA different merchant key for this call. authMode and credentials select another credential mode instead.
signalAbortSignalNode only. PHP takes a Cancellation token that the client checks during transfer. Canceling stops the local work, not the remote operation.
Pagination#
Every list operation has an Items companion that follows next_page_token for you and yields one resource at a time, keeping your filters on every page:
for await (const order of flint.orders.listItems(
{ payment_status: "paid", page_size: 100 },
{ maxItems: 1000, deadlineMs: 60000 },
)) {
console.log(order.order_id, order.pricing_amounts.total_money);
}
foreach ($flint->orders->listItems(
['payment_status' => 'paid', 'page_size' => 100],
new RequestOptions(maxItems: 1000, deadlineMs: 60000),
) as $order) {
echo $order->order_id, PHP_EOL;
}
Iteration is lazy, so the next page is fetched only when you reach it. listPages yields whole pages with their metadata when you want the envelope, and a plain list call fetches one page. Pagination covers sorting, filtering, and what a cursor holds.
Verify a webhook#
The client verifies a delivery against the exact bytes your server received:
const { event, known } = flint.verifyWebhook(rawBody, headers, [
process.env.FLINT_WEBHOOK_SECRET!,
]);
if (known) {
// event is typed by its event_type.
}
$verified = $flint->verifyWebhook($rawBody, $headers, [
getenv('FLINT_WEBHOOK_SECRET'),
]);
$event = $verified['event'];
It checks the webhook-id, webhook-timestamp, and webhook-signature headers with a 300 second tolerance and throws SdkError on a mismatch. It takes a list of secrets, so you can rotate one without dropping a delivery. known is false for an event type newer than your SDK version; store it rather than treating it as handled. Pass the raw request body, since verifying reserialized JSON fails on every provider.
Verification is not deduplication. Webhooks covers registering an endpoint and the webhook_event_id you store to process each event once.
Credential modes#
apiKey is the merchant mode, and it covers most of the API. Operations that act for a buyer, a checkout, or an account that does not have a key yet take a different credential, and each method's TypeScript signature only accepts the modes that operation allows.
merchantBearerAuthYour API key as a bearer token. apiKey sets this mode for you.
merchantKeyApiKeyHeaderThe same key sent as X-API-Key.
customerCustomerSessionBearerA customer session, for the /v1/me routes.
checkouttwo headersCheckoutSessionIDHeader and CheckoutSessionSecretHeader, for a checkout session acting on its own order.
invoiceInvoiceAccessTokenBearerA buyer's invoice access token.
onboardingOnboardingSessionBearerThe routes you call before an API key exists.
Set a mode on the client, or on one request:
const orders = await flint.me.listOrders(
{},
{ authMode: "customer", credentials: { CustomerSessionBearer: sessionToken } },
);
Send one mode per request. Authentication covers keys, scopes, and what each mode can reach.
Versions#
Both packages are generated for one API version and send it as Flint-Version on every request, so a response shape never changes underneath a deployed app. The current release of both is 0.3.0-beta.1, built for API version 2026-09-07. Upgrading the SDK is how you move API versions, and the version table shows which release carries which. API versions and upgrades covers what changes between versions.
Every operation, with an example#
Each package ships a runnable script per operation, named for the resource and method, so the example for any call is one file away: node/examples/orders-create.ts or php/examples/orders-create.php. The generated reference documents every method with its parameters, its return, and the credential modes it accepts.
Node reference
Every method on @flintpay/node, with parameters, returns, and examples.
PHP reference
Every method on flintpay/flint, with parameters, returns, and examples.
Both packages are released from flint-pay/flint-sdks, where the contract they were generated from is checked in beside them.
Next steps#
- Embedded payments with Stripe Elements: collect the card in your own UI and let Flint confirm it.
- Build your own checkout: the merchant-owned checkout flow.
- Webhooks: register an endpoint and fulfill from
order.paid. - CLI: the same API from a terminal or a CI job, including forwarding sandbox webhooks to localhost.
- Orders API reference and Payments API reference: every field on the objects these calls return.
