@palinde/sdk
v1.1.2
Published
Official Palinde JavaScript SDK for headless storefronts (Framer, Webflow, React, anywhere JS runs).
Maintainers
Readme
@palinde/sdk
Official JavaScript SDK for the Palinde public commerce API. Build a complete headless storefront for any Palinde merchant — from any framework, anywhere JavaScript runs.
npm install @palinde/sdkNo build step? Use the CDN:
<script type="module">
import { PalindeSDK } from "https://cdn.jsdelivr.net/npm/@palinde/sdk@1/dist/index.mjs";
</script>What it does
Every endpoint of the Palinde public API is exposed as a typed method. The cart token persists automatically in localStorage, so users don't lose
their cart between page loads. Variants, promo codes, fees, taxes, delivery options — all handled. Payment goes through Paystack; you just redirect.
60-second example
import { PalindeSDK } from "@palinde/sdk";
const palinde = new PalindeSDK({ key: "pk_live_..." });
// 1. Show the store (read currency, branches, fee tiers)
const store = await palinde.store.get();
const branchId = store.branches.find((b) => b.isDefault)?._id;
// 2. List products (pass branchId for branch-scoped stock)
const { items: products } = await palinde.products.list({ page: 1, branchId });
// 3. Pick a variant if the product has them
const product = await palinde.products.get(products[0].sku, { branchId });
const skuToBuy = product.hasVariants ? product.variants[0].sku : product.sku;
// 4. Add to cart (cart auto-created + persisted)
await palinde.carts.ensure({ branchId });
await palinde.carts.addItem({ sku: skuToBuy, quantity: 2 });
// 5. Preview totals
const quote = await palinde.carts.quote();
console.log(`Total: ${quote.currency} ${quote.total}`);
// 6. Checkout → redirect to Paystack
const { paymentUrl } = await palinde.checkout.create({
customer: { phone: "0241234567", name: "Akua" },
});
window.location.href = paymentUrl;After payment Paystack redirects to <your-callback-url>?orderRef=ORD-...&status=success. Your thank-you page:
const params = new URLSearchParams(window.location.search);
const order = await palinde.orders.get(params.get("orderRef"), customerPhone);
// or, poll until confirmed:
const order = await palinde.orders.waitForPayment(params.get("orderRef"), customerPhone);Initialize
new PalindeSDK({
key: "pk_live_...", // required — the merchant generates this in their dashboard
baseUrl: "https://api.palinde.com/api/v1", // default
timeoutMs: 30_000, // per-request timeout
cartStorageKey: "palinde:cartToken", // localStorage key; pass `null` to disable (server-side)
onError: (err) => Sentry.capture(err),
});| Option | Default | Notes |
| ---------------- | -------------------------------- | ---------------------------------------------------------------------------------------- |
| key | — | Publishable API key. pk_live_… (all keys are live) |
| baseUrl | https://api.palinde.com/api/v1 | Override for self-hosted / dev environments |
| timeoutMs | 30000 | Aborts requests longer than this |
| cartStorageKey | "palinde:cartToken" | Where the SDK persists the cart token. null disables — required for Node / Deno / edge |
| onError | — | Hook fired on every error. Useful for telemetry |
Reference
palinde.store
| Method | Returns | Description |
| ------- | -------------- | ------------------------------------------------------------------ |
| get() | StoreProfile | Merchant identity — branding, branches, currency, hours, fee tiers |
palinde.products
| Method | Returns | Description |
| -------------------------------------------------------------- | ------------------------ | ---------------------------------------------------------------------------------------- |
| list({ page?, limit?, category?, search?, tag?, branchId? }) | ProductListPage | Paginated list. Pass branchId for branch-scoped stock. |
| get(sku, { branchId? }) | Product | Single product including its variants[]. branchId scopes stock on parent + variants. |
| all({ category?, search?, tag?, branchId? }) | AsyncIterable<Product> | Async iterator over every page |
ProductListPage is { items, page, limit, total, pages }.
for await (const p of palinde.products.all({ category: "Bags", branchId })) {
console.log(p.sku, p.hasVariants ? p.variants.length + " variants" : "no variants");
}Variants: when product.hasVariants is true, the customer must pick a variant before checkout. Use the variant's sku (not the parent's) for
all subsequent cart, availability, and checkout calls. The parent SKU will be rejected with available: false, hasVariants: true.
palinde.availability
| Method | Returns | Description |
| ------------------------------------- | -------------------------- | ----------------------------------------------------------------- |
| check({ sku, quantity, branchId? }) | AvailabilityResult | Stock + price for one SKU. SKU can be a product or variant. |
| checkBulk({ items, branchId? }) | AvailabilityBulkResponse | Stock + price for many SKUs in one call. Useful for cart refresh. |
const r = await palinde.availability.check({ sku: "BAG-001-RED-L", quantity: 2, branchId });
if (!r.available) showError(r.reason);
else updateLineTotal(r.lineTotal);If the SKU is a parent product with variants, the response has available: false, hasVariants: true, and
reason: "This product has variants — please choose a specific variant to order."
palinde.carts
| Method | Returns | Description |
| --------------------------------------- | ---------------- | -------------------------------------------------- |
| create({ branchId? }) | Cart | Create new anonymous cart, persist token |
| current() | Cart | Read cart for the persisted token |
| get(cartToken) | Cart | Read a specific cart |
| ensure({ branchId? }) | Cart | Get current cart OR create one |
| getCurrentToken() | string \| null | Read the persisted token (no API call) |
| clearCurrent() | void | Forget the persisted token |
| addItem({ sku, quantity, branchId? }) | Cart | Add a product or variant SKU |
| updateItem({ itemId, quantity }) | Cart | Change quantity (note: itemId, not SKU) |
| removeItem({ itemId }) | Cart | Remove a line |
| clear() | Cart | Remove every line |
| applyPromo({ promoCode }) | Cart | Apply promo (validated against current contents) |
| clearPromo() | Cart | Remove applied promo |
| quote() | CartQuote | Live totals — subtotal, discounts, fee, tax, total |
All methods accept an optional cartToken to override the persisted one, and an optional signal: AbortSignal for cancellation.
palinde.checkout
| Method | Returns | Description |
| --------------------------------------------------- | ---------------- | ------------------------------------------ |
| create({ customer, promoCode?, idempotencyKey? }) | CheckoutResult | Submit order, get Paystack hosted-page URL |
const { paymentUrl, orderRef, total, expiresAt } = await palinde.checkout.create({
customer: { phone: "0241234567", name: "Akua" },
});- The SDK auto-clears the persisted cart token after success.
- Pass your own
idempotencyKey(UUIDv4) to safely retry on network errors. Without one, the SDK generates a fresh UUID per call.
palinde.orders
| Method | Returns | Description |
| ---------------------------------------------------------------------- | ------------- | --------------------------------------------------------------------------------- |
| get(orderRef, customerPhone) | PublicOrder | Single fetch |
| waitForPayment(orderRef, customerPhone, { timeoutMs?, intervalMs? }) | PublicOrder | Polls until a terminal payment state (PAID, FAILED, EXPIRED, REFUNDED) or timeout |
palinde.delivery
| Method | Returns | Description |
| ------------------------------------------------------------ | ------------------------ | ---------------------------------------------------------- |
| quote({ branchId, toGpsCode?, toLat?, toLng?, weightKg? }) | DeliveryQuotesResponse | Price every delivery option from a branch to a destination |
Error handling
Every failure throws a PalindeError:
import { PalindeError } from "@palinde/sdk";
try {
await palinde.carts.addItem({ sku, quantity: 5 });
} catch (err) {
if (err instanceof PalindeError) {
if (err.isStockConflict()) showToast("Just sold out — try another size.");
else if (err.isAuthError()) showToast("API key invalid — contact the merchant.");
else if (err.isCartGone()) {
palinde.carts.clearCurrent();
await palinde.carts.create();
} else if (err.isRateLimited()) {
await new Promise((r) => setTimeout(r, 2000));
} else {
showToast(err.message);
}
} else throw err;
}Predicates: | Predicate | HTTP | Meaning | |---|---|---| | isStockConflict() | 409 | Stock changed — refresh and retry | | isAuthError() | 401 |
Key invalid / revoked / expired | | isForbidden() | 403 | Origin not allowed, or cart belongs to another merchant | | isCartGone() | 410 | Cart
checked out or abandoned — start a new one | | isRateLimited() | 429 | Back off |
Properties: statusCode, errorName, path, timestamp, raw (full API error envelope).
TypeScript
Everything's typed. Import types directly:
import type { StoreProfile, Product, ProductVariant, Cart, CartQuote, CheckoutResult, PublicOrder } from "@palinde/sdk";Framework guides
React (hooks)
Full example at /examples/react.tsx. Wrap your tree with the providers:
import { PalindeProvider, CartProvider } from "@palinde/sdk/react-example";
<PalindeProvider>
<CartProvider>
<YourApp />
</CartProvider>
</PalindeProvider>;Then use the hooks:
function CartDrawer() {
const { cart, quote, addItem, removeItem } = useCart();
// ...
}
function ThankYou() {
const { order, loading } = useOrder(orderRef, phone);
// ...
}Next.js (App Router)
Full example at /examples/nextjs/. Two SDK instances:
lib/palinde-server.ts—cartStorageKey: nullfor server componentslib/palinde-client.tsx— default storage, wrapped in"use client"
Catalog renders on the server (fast first paint, SEO). Cart + checkout are client components.
Framer
Framer Code Components run in the browser. Use the CDN import:
import { PalindeSDK } from "https://cdn.jsdelivr.net/npm/@palinde/sdk@1/dist/index.mjs";
const palinde = new PalindeSDK({ key: "pk_live_..." });
export default function ProductGrid() {
const [products, setProducts] = useState([]);
useEffect(() => {
palinde.products.list().then((r) => setProducts(r.data));
}, []);
return <div>...</div>;
}Webflow / Wordpress / vanilla JS
<script type="module">
import { PalindeSDK } from "https://cdn.jsdelivr.net/npm/@palinde/sdk@1/dist/index.mjs";
const palinde = new PalindeSDK({ key: "pk_live_..." });
document.querySelectorAll("[data-add-to-cart]").forEach((btn) => {
btn.addEventListener("click", async () => {
await palinde.carts.ensure();
await palinde.carts.addItem({ sku: btn.dataset.sku, quantity: 1 });
});
});
</script>Vue 3
import { PalindeSDK } from "@palinde/sdk";
import { ref, onMounted } from "vue";
export const palinde = new PalindeSDK({ key: import.meta.env.VITE_PALINDE_KEY });
export function useStore() {
const store = ref(null);
onMounted(async () => {
store.value = await palinde.store.get();
});
return { store };
}Production keys
All API keys are live. Use small order amounts during development and refund them — there is no separate sandbox environment.
Server-side usage
Set cartStorageKey: null to disable localStorage access. Manage cart tokens yourself (e.g. in your session store):
const palinde = new PalindeSDK({
key: process.env.PALINDE_KEY,
cartStorageKey: null,
});
// Pass cartToken explicitly:
await palinde.carts.addItem({ cartToken: session.cartToken, sku: "X", quantity: 1 });FAQ
Why doesn't addItem take the SKU directly like the example shows? It does — but as a named argument so it's hard to misuse:
addItem({ sku, quantity }). Avoids the addItem(token, opts) vs addItem(opts) ambiguity that bites users in similar SDKs.
Can I add multiple items at once? Not in one call. Loop and add them one by one — the API rate limits are high enough this isn't an issue in practice.
The cart drawer is empty after the user pays — is that right? Yes. After checkout.create() succeeds, the cart is CHECKED_OUT (immutable). The
SDK auto-clears the persisted token, so palinde.carts.ensure() will create a fresh cart on the user's next interaction.
Can the SDK call private merchant APIs? No. The publishable key only authorizes the public-API surface (catalog, cart, checkout, order tracking). It cannot list other merchants' orders, modify products, or trigger payouts — by design.
License
MIT.
