Integrating Qikink's Print-on-Demand API with Node.js — The Definitive Guide
Integrating Qikink's Print-on-Demand API with Node.js — The Definitive Guide
If you've ever tried wiring Qikink into a Node.js backend and hit inconsistent auth fields, sandbox-only SKU failures, or a mysterious 15-character order limit, this guide condenses the real integration path into one working reference.
Table of Contents
- What is Qikink?
- Prerequisites
- Understanding the Two Environments
- Step 1: Getting Your Sandbox Credentials
- Step 2: Authentication — Getting an Access Token
- Step 3: Understanding SKUs
- Step 4: Creating an Order — The Core API Call
- The Sandbox Trap:
search_from_my_products - Common Errors and How to Fix Them
- Step 5: Building a Production-Ready Service
- Step 6: Getting Live Credentials
- Step 7: Switching to Live
- Bonus: Retry Logic and Resilience
- Complete Working Code
- Conclusion
What is Qikink?
Qikink is an Indian Print-on-Demand (POD) and dropshipping platform. You design a product, a customer orders it on your website, and Qikink prints, packs, and ships it directly to the customer. You never touch inventory.
Their API lets you automate this entire flow, but their official documentation leaves a lot of gaps. This post fills in the practical details that matter when you are actually trying to ship code.
Prerequisites
Before you start, make sure you have:
- Node.js (v18+ recommended)
- TypeScript (optional but recommended)
- axios for HTTP requests (
npm install axios) - A Qikink account at dashboard.qikink.com
- At least one product created on your Qikink dashboard under My Products
Understanding the Two Environments
Qikink provides two completely separate API environments:
| Feature | Sandbox | Live |
|---|---|---|
| Base URL | https://sandbox.qikink.com | https://api.qikink.com |
| Purpose | Testing only | Real orders |
| "My Products" Database | ❌ Separate and empty by default | ✅ Your actual products |
| Orders Created | Not fulfilled | Printed and shipped |
| Wallet Charged | No | Yes |
Critical gotcha: Sandbox and Live do not share the same database. Products created on your dashboard exist only in the Live environment. The Sandbox has its own isolated database, which is why valid SKUs can still fail there.
Step 1: Getting Your Sandbox Credentials
- Log in to dashboard.qikink.com
- Go to Integration → Custom API
- You will see your Sandbox API Client ID and Client Secret
- Copy both, you will need them
# .env
QIKINK_CLIENT_ID=YOUR_CLIENT_ID
QIKINK_SANDBOX_SECRET=your_sandbox_secret_here
QIKINK_CLIENT_SECRET=
QIKINK_SANDBOX_MODE=true
Step 2: Authentication — Getting an Access Token
Every Qikink API call requires an access token. You get one by hitting the /api/token endpoint with your Client ID and Client Secret.
The Request
import axios from 'axios';
const getQikinkToken = async (): Promise<string> => {
const clientId = process.env.QIKINK_CLIENT_ID!;
const isSandbox = process.env.QIKINK_SANDBOX_MODE === 'true';
const clientSecret = isSandbox
? process.env.QIKINK_SANDBOX_SECRET!
: process.env.QIKINK_CLIENT_SECRET!;
const baseUrl = isSandbox
? 'https://sandbox.qikink.com'
: 'https://api.qikink.com';
const params = new URLSearchParams();
params.append('ClientId', clientId);
params.append('client_secret', clientSecret);
const response = await axios.post(`${baseUrl}/api/token`, params, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
const token = response.data?.Accesstoken;
if (!token) {
throw new Error('No token received from Qikink');
}
return token;
};
Key details the docs do not emphasize
- Content-Type must be
application/x-www-form-urlencoded, not JSON. - The field names are inconsistent: send
ClientIdandclient_secret. - The response token field is
Accesstoken. - Tokens expire in 1 hour (3600 seconds). Cache them and refresh before expiry.
Production-ready token caching
let cachedToken: string | null = null;
let tokenExpiry: number | null = null;
const getQikinkToken = async (forceRefresh = false): Promise<string> => {
if (!forceRefresh && cachedToken && tokenExpiry && Date.now() < tokenExpiry - 30000) {
return cachedToken;
}
const clientId = process.env.QIKINK_CLIENT_ID!;
const isSandbox = process.env.QIKINK_SANDBOX_MODE === 'true';
const clientSecret = isSandbox
? process.env.QIKINK_SANDBOX_SECRET!
: process.env.QIKINK_CLIENT_SECRET!;
const baseUrl = isSandbox ? 'https://sandbox.qikink.com' : 'https://api.qikink.com';
const params = new URLSearchParams();
params.append('ClientId', clientId);
params.append('client_secret', clientSecret);
const response = await axios.post(`${baseUrl}/api/token`, params, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
const token = response.data?.Accesstoken;
if (!token) {
throw new Error('No token received from Qikink');
}
cachedToken = token;
tokenExpiry = Date.now() + (55 * 60 * 1000);
return token;
};
Step 3: Understanding SKUs
SKUs are how Qikink identifies specific product variants. Every combination of product type, color, and size has a unique SKU.
Where to Find SKUs
- Go to dashboard.qikink.com
- Navigate to Products → SKU Descriptions
- You will see a table like this:
| Product | Color | Size | SKU |
|---|---|---|---|
| Unisex Sweatshirt | White | M | USs-Wh-M |
| Unisex Sweatshirt | Black | L | USs-Bk-L |
| Men's V-Neck Half Sleeve | White | S | MVnHs-Wh-S |
SKU format
The SKU follows a pattern: {ProductCode}-{ColorCode}-{SizeCode}
- USs = Unisex Sweatshirt
- Wh = White, Bk = Black
- M = Medium, L = Large, S = Small
Tip: Always copy SKUs exactly from the dashboard. They are case-sensitive.
USs-Wh-Mworks,uss-wh-mdoes not.
Step 4: Creating an Order — The Core API Call
This is the main API call that creates a print-on-demand order.
Endpoint
POST {baseUrl}/api/order/create
Headers
{
'ClientId': 'your_client_id',
'Accesstoken': 'your_access_token',
'Content-Type': 'application/json'
}
Critical gotcha: The header is
Accesstoken, notAuthorizationorBearer.ClientIdmust be sent as a header alongside it.
Request body structure
{
"order_number": "ORD-001",
"qikink_shipping": "1",
"gateway": "Prepaid",
"total_order_value": "799",
"line_items": [
{
"search_from_my_products": 1,
"sku": "USs-Wh-M",
"quantity": "1",
"price": "799"
}
],
"shipping_address": {
"first_name": "John",
"last_name": "Doe",
"address1": "123 Main Street",
"address2": "",
"phone": "9876543210",
"email": "john.doe@example.com",
"city": "Mumbai",
"zip": "400001",
"province": "Maharashtra",
"country_code": "IN"
}
}
Field-by-field breakdown
| Field | Type | Notes |
|---|---|---|
order_number | String | Max 15 characters. Longer values can fail. |
qikink_shipping | String | Always "1" to let Qikink handle shipping |
gateway | String | "Prepaid" or "COD" |
total_order_value | String | Must be a string, not a number |
quantity | String | Must be a string, not a number |
price | String | Must be a string, not a number |
search_from_my_products | Integer | 1 or 0 |
country_code | String | Must be "IN" for Indian orders |
Critical gotcha:
order_numberhas a hard limit of 15 characters. If you are using MongoDB ObjectIDs or UUIDs, truncate them.
const shortOrderNum = 'APP' + orderId.slice(-12);
The Sandbox Trap: search_from_my_products
This is the single most confusing part of the Qikink API.
What search_from_my_products does
search_from_my_products: 1means: look up this SKU in my dashboard's My Products section.search_from_my_products: 0means: I am providing the design details in the API call.
Why this matters for Sandbox
When you create products on your Qikink dashboard, those products exist only in the Live database. The Sandbox has a separate empty database.
So when you send search_from_my_products: 1 to Sandbox with a valid SKU like USs-Wh-M, it searches the Sandbox database, finds nothing, and returns:
{
"message": "Invalid SKU"
}
The SKU is valid. The Sandbox just cannot see your dashboard products.
The solution: use search_from_my_products: 0 for Sandbox
When testing in Sandbox, you must use search_from_my_products: 0 and provide all design details manually:
{
"search_from_my_products": 0,
"sku": "USs-Wh-M",
"quantity": "1",
"price": "799",
"print_type_id": 1,
"designs": [
{
"design_code": "TestDesign001",
"width_inches": "10",
"height_inches": "10",
"placement_sku": "fr",
"design_link": "https://example.com/your-design.png",
"mockup_link": "https://example.com/your-mockup.png"
}
]
}
The designs array
| Field | Required when search_from_my_products: 0 | Notes |
|---|---|---|
design_code | Yes | Cannot be empty |
width_inches | Recommended | Use "10" as a safe default |
height_inches | Recommended | Use "10" as a safe default |
placement_sku | Yes | "fr" = Front, "bk" = Back |
design_link | Recommended | Publicly accessible image URL |
mockup_link | Yes | Cannot be an empty string |
Critical gotcha: If
search_from_my_productsis0, thendesign_code,mockup_link, andplacement_skuare mandatory. If any are missing, Qikink returns a validation error.
Sandbox example data
designs: [{
design_code: 'iPhoneXR',
width_inches: '10',
height_inches: '10',
placement_sku: 'fr',
design_link: 'https://sgp1.digitaloceanspaces.com/cdn.qikink.com/erp2/assets/designs/83/1696668376.jpg',
mockup_link: 'https://sgp1.digitaloceanspaces.com/cdn.qikink.com/erp2/assets/designs/83/1696668376.jpg'
}]
Common Errors and How to Fix Them
Error 1: "Invalid SKU"
Cause: search_from_my_products: 1 in Sandbox mode.
Fix: Switch to search_from_my_products: 0 and provide design details.
Error 2: "Design code, Mockup Link and placement sku is mandatory if search_for_my_products is 0"
Cause: Missing or empty design fields.
Fix: Provide actual values for design_code, mockup_link, and placement_sku.
Error 3: "Order no. cannot exceed 15 chars"
Cause: order_number is longer than 15 characters.
Fix: Truncate the identifier.
Error 4: 401 Unauthorized
Cause: Token expired or credentials are wrong.
Fix: Force-refresh the token and retry.
Error 5: Empty or malformed response
Cause: Token request sent as JSON instead of form-urlencoded.
Fix: Use URLSearchParams.
Step 5: Building a Production-Ready Service
Here is a production-ready service that handles both Sandbox and Live:
// qikinkAuthService.ts
import axios from 'axios';
let cachedToken: string | null = null;
let tokenExpiry: number | null = null;
export const getQikinkBaseUrl = (): string => {
const isSandbox = process.env.QIKINK_SANDBOX_MODE === 'true';
return isSandbox ? 'https://sandbox.qikink.com' : 'https://api.qikink.com';
};
export const getQikinkToken = async (forceRefresh = false): Promise<string> => {
if (!forceRefresh && cachedToken && tokenExpiry && Date.now() < tokenExpiry - 30000) {
return cachedToken;
}
const clientId = process.env.QIKINK_CLIENT_ID!;
const isSandbox = process.env.QIKINK_SANDBOX_MODE === 'true';
const clientSecret = isSandbox
? process.env.QIKINK_SANDBOX_SECRET!
: process.env.QIKINK_CLIENT_SECRET!;
const baseUrl = getQikinkBaseUrl();
const params = new URLSearchParams();
params.append('ClientId', clientId);
params.append('client_secret', clientSecret);
const response = await axios.post(`${baseUrl}/api/token`, params, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
const token = response.data?.Accesstoken;
if (!token) {
throw new Error(`No token received. Response: ${JSON.stringify(response.data)}`);
}
cachedToken = token;
const expiresInSec = response.data?.expires_in || 3600;
tokenExpiry = Date.now() + ((expiresInSec - 300) * 1000);
return token;
};
// qikinkService.ts
import axios from 'axios';
import { getQikinkToken, getQikinkBaseUrl } from './qikinkAuthService';
interface OrderItem {
sku: string;
quantity: number;
price: number;
}
interface ShippingAddress {
first_name: string;
last_name: string;
address1: string;
phone: string;
email: string;
city: string;
zip: string;
state: string;
}
interface CreateOrderPayload {
orderId: string;
payment_method: 'online' | 'cod';
items: OrderItem[];
customer: ShippingAddress;
}
export const createQikinkOrder = async (payload: CreateOrderPayload) => {
let token = await getQikinkToken();
const baseUrl = getQikinkBaseUrl();
const clientId = process.env.QIKINK_CLIENT_ID!;
const isSandbox = process.env.QIKINK_SANDBOX_MODE === 'true';
const shortOrderNum = 'APP' + payload.orderId.slice(-12);
const totalOrderValue = payload.items.reduce(
(sum, item) => sum + (item.price * item.quantity), 0
);
const qikinkPayload = {
order_number: shortOrderNum,
qikink_shipping: '1',
gateway: payload.payment_method === 'cod' ? 'COD' : 'Prepaid',
total_order_value: String(totalOrderValue),
line_items: payload.items.map(item => {
if (isSandbox) {
return {
search_from_my_products: 0,
sku: String(item.sku),
quantity: String(item.quantity),
price: String(item.price),
print_type_id: 1,
designs: [{
design_code: 'TestDesign001',
width_inches: '10',
height_inches: '10',
placement_sku: 'fr',
design_link: 'https://sgp1.digitaloceanspaces.com/cdn.qikink.com/erp2/assets/designs/83/1696668376.jpg',
mockup_link: 'https://sgp1.digitaloceanspaces.com/cdn.qikink.com/erp2/assets/designs/83/1696668376.jpg'
}]
};
}
return {
search_from_my_products: 1,
sku: String(item.sku),
quantity: String(item.quantity),
price: String(item.price)
};
}),
shipping_address: {
first_name: payload.customer.first_name,
last_name: payload.customer.last_name || '',
address1: payload.customer.address1,
address2: '',
phone: payload.customer.phone,
email: payload.customer.email,
city: payload.customer.city,
zip: payload.customer.zip,
province: payload.customer.state,
country_code: 'IN'
}
};
try {
return await sendRequest(baseUrl, clientId, token, qikinkPayload);
} catch (error: any) {
if (error.response?.status === 401) {
token = await getQikinkToken(true);
return await sendRequest(baseUrl, clientId, token, qikinkPayload);
}
throw error;
}
};
const sendRequest = async (
baseUrl: string, clientId: string, token: string, payload: any
) => {
const response = await axios.post(`${baseUrl}/api/order/create`, payload, {
headers: {
'ClientId': clientId,
'Accesstoken': token,
'Content-Type': 'application/json'
}
});
if (response.data?.status_code === '200' || response.data?.order_id) {
return {
success: true,
orderId: String(response.data.order_id),
message: response.data.message
};
}
throw new Error(response.data?.message || 'Unknown error from Qikink');
};
Step 6: Getting Live Credentials
After your Sandbox integration is working, you need to request Live API credentials.
- Place 3 Sandbox orders.
- Request live access from Integration → Custom API.
- Email Qikink with the 3 order details and a screenshot of your admin panel.
- Receive the live client secret.
Step 7: Switching to Live
Update environment variables
# Before (Sandbox)
QIKINK_SANDBOX_MODE=true
QIKINK_CLIENT_ID=YOUR_CLIENT_ID
QIKINK_SANDBOX_SECRET=your_sandbox_secret
# After (Live)
QIKINK_SANDBOX_MODE=false
QIKINK_CLIENT_ID=YOUR_CLIENT_ID
QIKINK_CLIENT_SECRET=your_live_secret
What changes automatically
| Behavior | Sandbox (true) | Live (false) |
|---|---|---|
| API URL | sandbox.qikink.com | api.qikink.com |
| Secret Used | QIKINK_SANDBOX_SECRET | QIKINK_CLIENT_SECRET |
search_from_my_products | 0 | 1 |
| Orders | Test only | Real fulfillment |
Bonus: Retry Logic and Resilience
const createOrderWithRetry = async (
payload: CreateOrderPayload,
maxRetries = 3
): Promise<any> => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await createQikinkOrder(payload);
} catch (error: any) {
const isLastAttempt = attempt === maxRetries;
const status = error.response?.status;
if (status && status >= 400 && status < 500 && status !== 401) {
throw error;
}
if (isLastAttempt) {
throw error;
}
const delay = Math.pow(2, attempt - 1) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
};
Complete Working Code
// routes/orders.ts
import express from 'express';
import { createQikinkOrder } from '../services/qikinkService';
const router = express.Router();
router.post('/create', async (req, res) => {
try {
const { orderId, items, customer, paymentMethod } = req.body;
const result = await createQikinkOrder({
orderId,
payment_method: paymentMethod,
items: items.map((item: any) => ({
sku: item.sku,
quantity: item.qty,
price: item.price
})),
customer: {
first_name: customer.firstName,
last_name: customer.lastName,
address1: customer.address,
phone: customer.phone,
email: customer.email,
city: customer.city,
zip: customer.pincode,
state: customer.state
}
});
res.json({
success: true,
message: 'Order placed successfully',
qikinkOrderId: result.orderId
});
} catch (error: any) {
console.error('Order creation failed:', error.message);
res.status(500).json({
success: false,
message: error.message
});
}
});
export default router;
Required environment variables
QIKINK_CLIENT_ID=your_client_id
QIKINK_SANDBOX_SECRET=your_sandbox_secret
QIKINK_CLIENT_SECRET=your_live_secret
QIKINK_SANDBOX_MODE=true
Quick Reference Cheat Sheet
| Area | Key Details |
|---|---|
| Auth endpoint | POST /api/token |
| Auth content type | application/x-www-form-urlencoded |
| Auth fields | ClientId, client_secret |
| Auth response key | Accesstoken |
| Order endpoint | POST /api/order/create |
| Order content type | application/json |
| Order headers | ClientId, Accesstoken |
| Sandbox URL | https://sandbox.qikink.com |
| Live URL | https://api.qikink.com |
order_number limit | Max 15 characters |
quantity / price | Must be strings, not numbers |
country_code | Must be IN |
| Sandbox behavior | search_from_my_products = 0 |
| Live behavior | search_from_my_products = 1 |
| Go-live checklist | 3 sandbox orders, then email Qikink, then wait for approval |
Conclusion
Qikink's API is powerful but under-documented. The biggest pitfalls are:
- Sandbox and Live do not share the same database.
search_from_my_productsmust be0in Sandbox and1in Live.ClientId,client_secret, andAccesstokenare named inconsistently.- Quantity, price, and
total_order_valuemust be strings. order_numbermust stay within 15 characters.
Once you understand those details, the integration becomes straightforward. The code above reflects the practical path that avoids the common Qikink failures.
Written from real integration experience building a Node.js e-commerce backend powered by Qikink's Print-on-Demand API.