SDKs
Flint publishes two official server SDKs, both generated from Flint's public OpenAPI contract and released from flint-pay/flint-sdks. They cover all 497 operations at API version 2026-09-07, and they send that version on every request.
| Package | Install | Runtime |
|---|---|---|
@flintpay/node | npm install @flintpay/node | Node.js 22+, TypeScript 5.9+, ESM |
flintpay/flint | composer require flintpay/flint | PHP 8.2+ with the cURL and JSON extensions |
Every request carries a credential, so both packages belong on a server, a worker, or another trusted backend. A Flint API key never goes in browser or mobile code.
Neither is required. Every guide here shows the HTTP request, and the CLI drives the same API from a terminal or a CI job.
Create a client#
A client needs baseUrl, an authMode, and the credentials for that mode. It reads no environment variables and discovers no credentials: whatever you pass is what it sends.
import { Client } from "@flintpay/node";
const flint = new Client({
baseUrl: "https://api.withflintpay.com",
authMode: "merchant",
credentials: {
merchant: { BearerAuth: 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',
authMode: 'merchant',
credentials: ['merchant' => ['BearerAuth' => getenv('FLINT_API_KEY')]],
));
close() releases the client's cURL handle. One client handles sequential calls within one PHP execution context, so do not share it across threads or fibers.
The key decides the environment: a flint_test_... key runs against your sandbox and a flint_live_... key runs against your live account. Constructing a client performs no network I/O.
Authentication modes#
Set authMode and the matching credentials entry on the client, or per request. The mode has to be one the operation accepts.
merchantcarries your API key as a bearer token underBearerAuth. Most operations use it.merchantKeycarries the same key asX-API-Key, underApiKeyHeader.customercarries a customer session underCustomerSessionBearer, for the/v1/meroutes.checkoutcarries a checkout session's ID and secret underCheckoutSessionIDHeaderandCheckoutSessionSecretHeader, scoped to that session's own order.invoicecarries a buyer's invoice access token underInvoiceAccessTokenBearer.onboardingcarries an onboarding session underOnboardingSessionBearer, for the routes you call before an API key exists.
Send one mode per request. Combining checkout credentials with Authorization returns 400 AMBIGUOUS_AUTH.
Make a call#
Methods live on client.api and are named for the operation IDs in the OpenAPI contract. A JSON request body goes under body; path parameters, query parameters, and headers sit beside it as siblings.
const result = await flint.api.createOrder({
"Idempotency-Key": "order-2026-09-10-0001",
body: {
line_items: [
{
name: "Premium Widget",
quantity: "1",
unit_price_money: { amount: "2500", currency: "USD" },
},
],
},
});
const order = result.data.data;
console.log(order.order_id, result.meta.requestId);
use Flint\ApiCreateOrderInput;
$result = $flint->api->createOrder(new ApiCreateOrderInput([
'Idempotency-Key' => 'order-2026-09-10-0001',
'body' => [
'line_items' => [[
'name' => 'Premium Widget',
'quantity' => '1',
'unit_price_money' => ['amount' => '2500', 'currency' => 'USD'],
]],
],
]));
$order = $result->data->getData();
echo $order->get('order_id'), ' ', $result->meta['requestId'];
The generated reference lists every operation with its input and response types, for Node and for PHP. Each package also ships a runnable example per operation.
Field names and money#
Inputs use the API's wire names, so unit_price_money, not unitPriceMoney.
Exact numbers are strings. Amounts, quantities, and 64-bit integers are "2500" rather than 2500, which keeps them out of floating point on the way to the wire. They are still integers in the smallest currency unit, so "2500" is $25.00. Money & currency covers the model.
Results and errors#
Every call resolves to a result with data, meta, and the raw response. data is the parsed response body, so a resource sits at result.data.data and the response's own metadata sits beside it. Node exposes result metadata as properties (result.meta.requestId); PHP uses array keys ($result->meta['requestId']). PDF routes return Uint8Array in Node and binary-safe strings in PHP.
Failures throw SdkError with kind, outcome, and retryAllowed. outcome is one of:
not_sent: the request never left the client, so nothing changed.response: Flint answered, and the error describes that answer.unknown: the request may have reached Flint. On a write, reconcile against the resource and your stored idempotency key before resubmitting rather than sending it again.
Error handling covers the API error codes behind these exceptions.
Request options#
The second argument to any method carries per-request settings: a plain object in Node, a RequestOptions in PHP.
timeoutMsnumberBounds one attempt, including reading the body. Defaults to 10000.
deadlineMsnumberBounds the whole call, including waits. Defaults to 30000. It is a duration, not a timestamp.
authModestringSelects a mode for this request alone, with credentials supplying the values.
headersobjectPer-request headers. They stay local to the request rather than mutating client state.
signalAbortSignalNode only. PHP uses a Cancellation token checked during cURL progress. Canceling stops local work, not the remote operation.
Idempotency and retries#
The SDKs send each request once and never generate an idempotency key for you. Retrying is yours to do.
Send Idempotency-Key in the input object, alongside the path and query parameters, and persist it before the first attempt so the retry after a lost response reuses it:
await flint.api.createOrder({
"Idempotency-Key": storedKey,
body: { line_items: [/* … */] },
});
Idempotency covers how to choose keys and how long Flint honors them. Disable retries in any transport or HTTP library you wrap around the SDK, so one logical attempt does not become several.
Pagination#
List routes return one page. Read next_page_token from the response and send it back as page_token until it comes back empty:
let pageToken: string | undefined;
do {
const result = await flint.api.listOrders({ page_size: 100, page_token: pageToken });
for (const order of result.data.data) {
console.log(order.order_id, order.status);
}
pageToken = result.data.next_page_token;
} while (pageToken);
use Flint\ApiListOrdersInput;
$pageToken = null;
do {
$result = $flint->api->listOrders(new ApiListOrdersInput(
array_filter(['page_size' => 100, 'page_token' => $pageToken])
));
foreach ($result->data->getData() as $order) {
echo $order->get('order_id'), PHP_EOL;
}
$pageToken = $result->data->has('next_page_token')
? $result->data->get('next_page_token')
: null;
} while ($pageToken);
Use each token once and in order. Pagination covers sorting, filtering, and what a cursor holds.
Webhook verification#
The client verifies a signature against the raw request bytes:
const { event } = flint.verifyWebhook(rawBody, headers, [
process.env.FLINT_WEBHOOK_SECRET!,
]);
It checks the webhook-id, webhook-timestamp, and webhook-signature headers with a 300 second tolerance, and it accepts a list of secrets so you can rotate one without dropping deliveries. Pass the bytes your server received: verifying reserialized JSON fails.
Verification is not deduplication. Webhooks covers the delivery guarantees and the webhook_event_id you store to make handling idempotent.
Next steps#
- Accept your first payment for the same flow in raw HTTP
- CLI for the same API with no application code
- Embedded payments with Stripe Elements for collecting a payment in your own UI
- Build your own checkout for the merchant-owned checkout flow
- Authentication for keys, scopes, and modes
- Orders API reference and Payments API reference
