printify-sdk
v0.6.0
Published
TypeScript SDK for the Printify API
Readme
printify-sdk
TypeScript SDK for the Printify API. Fully typed, zero runtime dependencies, works from both ESM (import) and CommonJS (require).
Install
npm install printify-sdkRequires Node.js 18+.
Quick Start
import { PrintifyClient } from "printify-sdk";
const client = new PrintifyClient({
accessToken: "YOUR_API_TOKEN",
shopId: "YOUR_SHOP_ID", // optional, can be passed per-call
});
// List shops
const shops = await client.shops.list();
// Iterate every product (auto-pagination)
for await (const product of client.products.listAll()) {
console.log(product.title);
}
// Create a product
const product = await client.products.create({
title: "My T-Shirt",
description: "A great shirt",
blueprint_id: 6,
print_provider_id: 1,
variants: [{ id: 17390, price: 2000, is_enabled: true }],
print_areas: [
{
variant_ids: [17390],
placeholders: [
{
position: "front",
// An uploaded image id plus its placement; Printify fills in the
// file name, type and pixel size from the upload.
images: [{ id: "image-id", x: 0.5, y: 0.5, scale: 1, angle: 0 }],
},
],
},
],
});Full method/type reference: API.md.
API
new PrintifyClient(options)
| Option | Type | Required | Description |
| ------------- | ----------------------- | -------- | -------------------------------------------------------------------- |
| accessToken | string | Yes | Printify API token |
| shopId | string | No | Default shop ID for shop-scoped calls |
| baseUrl | string | No | API base URL (defaults to https://api.printify.com/v1) |
| timeoutMs | number | No | Request timeout in milliseconds (default: 30 000) |
| retry | RetryOptions \| false | No | Retry policy for transient failures (see Retries) |
| userAgent | string | No | User-Agent header (default: printify-sdk; Printify requires one) |
Shops
client.shops.list(): Promise<Shop[]>
client.shops.disconnect(shopId?): Promise<void>Blueprints (Catalog)
client.blueprints.list(): Promise<Blueprint[]>
client.blueprints.get(blueprintId): Promise<Blueprint>
client.blueprints.getPrintProviders(blueprintId): Promise<PrintProvider[]>
client.blueprints.getVariants(blueprintId, printProviderId): Promise<VariantsResponse>
client.blueprints.getShippingInfo(blueprintId, printProviderId): Promise<BlueprintShippingInfo>Print Providers
client.printProviders.list(): Promise<PrintProvider[]>
client.printProviders.get(printProviderId): Promise<PrintProviderWithBlueprints>Products
All methods accept an optional shopId parameter to override the default.
client.products.list(shopId?, page?, limit?): Promise<PaginatedResponse<Product>>
client.products.listAll(shopId?, limit?): AsyncGenerator<Product>
client.products.get(productId, shopId?): Promise<Product>
client.products.create(data, shopId?): Promise<Product>
client.products.update(productId, data, shopId?): Promise<Product>
client.products.delete(productId, shopId?): Promise<void>
client.products.publish(productId, publishData, shopId?): Promise<void>
client.products.publishingSucceeded(productId, { external: { id, handle } }, shopId?): Promise<void>
client.products.publishingFailed(productId, reason, shopId?): Promise<void>
client.products.getImages(productId, shopId?): Promise<ProductImage[]>For custom sales channels, publish() locks the product and fires a
product:publish:started webhook; complete the handshake with
publishingSucceeded / publishingFailed to unlock it. See
API.md.
Orders
client.orders.list(shopId?, params?): Promise<PaginatedResponse<PrintifyOrder>> // page, limit (max 10), status, sku
client.orders.listAll(shopId?, params?): AsyncGenerator<PrintifyOrder>
client.orders.get(orderId, shopId?): Promise<PrintifyOrder>
client.orders.create(data, shopId?): Promise<PrintifyOrder>
client.orders.sendToProduction(orderId, shopId?): Promise<void>
client.orders.cancel(orderId, shopId?): Promise<void>
client.orders.calculateShipping(data, shopId?): Promise<ShippingRate> // one rate for the whole basketA line_items entry names what to print in one of three mutually exclusive
ways — CreateOrderLineItem. The third needs no Printify product at all:
line_items: [
{ product_id: "5bfd0b66a342bcc9b5563216", variant_id: 17887, quantity: 1 },
{ sku: "18000-XL-BLACK", quantity: 1 },
{
blueprint_id: 9,
print_provider_id: 5,
variant_id: 17887,
quantity: 1,
print_areas: {
front: [{ src: "https://you.example/art.png", x: 0.5, y: 0.5, scale: 1 }],
},
},
];calculateShipping accepts the same three forms, which makes it a free
pre-flight check on a blueprint / provider / variant combination.
Notes on the blueprint form:
print_areastakes a URL Printify fetches — not an upload-library id, as a product's print areas do. Give it a window that outlives Printify's own retry schedule.- Either
{ front: "url" }(auto-centred) or{ front: [{ src, x, y, scale, angle }] }, never a mix of the two in one object. - The union rejects a line item naming two identifiers, and rejects
print_areason a product line — Printify ignores that field there rather than erroring, so the order would ship blank.
Uploads
client.uploads.uploadImage(data): Promise<UploadedImage>
client.uploads.getImage(imageId): Promise<UploadedImage>
client.uploads.listImages(page?, limit?): Promise<PaginatedResponse<UploadedImage>> // limit max 100
client.uploads.listAllImages(limit?): AsyncGenerator<UploadedImage>
client.uploads.archiveImage(imageId): Promise<void>Webhooks
client.webhooks.list(shopId?): Promise<Webhook[]>
client.webhooks.create(data, shopId?): Promise<Webhook>
client.webhooks.update(webhookId, data, shopId?): Promise<Webhook>
client.webhooks.delete(webhookId, shopId?, host): Promise<void>Verifying webhook deliveries
Printify signs deliveries with HMAC-SHA256 in the x-pfy-signature header.
verifyWebhook checks the signature with a constant-time comparison and
returns a typed, discriminated union of events:
import { verifyWebhook, WEBHOOK_SIGNATURE_HEADER } from "printify-sdk";
const event = verifyWebhook(
webhookSecret,
rawBody, // exact request body — do not re-serialize parsed JSON
request.headers[WEBHOOK_SIGNATURE_HEADER],
);
switch (event.type) {
case "order:shipment:created":
console.log(event.resource.data.carrier.tracking_number);
break;
case "product:publish:started":
// complete the publish handshake
break;
}Retries
429 and 5xx responses (and network errors) are retried automatically with
exponential backoff + jitter, honoring Retry-After. Only idempotent
requests are replayed — order/product creation and publishing are never
auto-retried. Printify's publish endpoint is rate-limited separately
(200 req/30 min); Retry-After hints longer than maxRetryAfterMs
(default 30 s) abort immediately with PrintifyRateLimitError instead of
stalling.
const client = new PrintifyClient({
accessToken: "...",
retry: { maxAttempts: 5 }, // or retry: false to disable
});See API.md for all RetryOptions.
Input Validation
All resource IDs are validated before use — path traversal attempts (../, /, special characters) throw immediately. Upload URLs must use HTTPS. Pagination page/limit must be positive integers; products.list enforces the documented limit cap of 50.
Error Handling
import { PrintifyApiError, PrintifyRateLimitError } from "printify-sdk";
try {
await client.products.get("non-existent");
} catch (err) {
if (err instanceof PrintifyRateLimitError) {
// err.retryAfter — seconds to wait (or null)
console.log(`Rate limited. Retry after ${err.retryAfter}s`);
} else if (err instanceof PrintifyApiError) {
// err.statusCode, err.message, err.errors
console.log(`API error ${err.statusCode}: ${err.message}`);
}
}Development
npm run lint # eslint
npm run format:check # prettier
npm run typecheck # tsc --noEmit
npm run test # vitest
npm run build # tsup → dist/ (ESM + CJS + types)Pre-commit hooks (via husky) run lint, typecheck, and tests automatically. CI validates lint, formatting, types, tests, and the build on every push and PR; publishing to npm is manual.
License
MIT
