Back to Blog
2026-07-15
25 min read

Integrating Qikink's Print-on-Demand API with Node.js — The Definitive Guide

#Node.js#TypeScript#Qikink#Print on Demand#API Integration#E-commerce#Backend Engineering

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

  1. What is Qikink?
  2. Prerequisites
  3. Understanding the Two Environments
  4. Step 1: Getting Your Sandbox Credentials
  5. Step 2: Authentication — Getting an Access Token
  6. Step 3: Understanding SKUs
  7. Step 4: Creating an Order — The Core API Call
  8. The Sandbox Trap: search_from_my_products
  9. Common Errors and How to Fix Them
  10. Step 5: Building a Production-Ready Service
  11. Step 6: Getting Live Credentials
  12. Step 7: Switching to Live
  13. Bonus: Retry Logic and Resilience
  14. Complete Working Code
  15. 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:

FeatureSandboxLive
Base URLhttps://sandbox.qikink.comhttps://api.qikink.com
PurposeTesting onlyReal orders
"My Products" Database❌ Separate and empty by default✅ Your actual products
Orders CreatedNot fulfilledPrinted and shipped
Wallet ChargedNoYes

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

  1. Log in to dashboard.qikink.com
  2. Go to Integration → Custom API
  3. You will see your Sandbox API Client ID and Client Secret
  4. 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

  1. Content-Type must be application/x-www-form-urlencoded, not JSON.
  2. The field names are inconsistent: send ClientId and client_secret.
  3. The response token field is Accesstoken.
  4. 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

  1. Go to dashboard.qikink.com
  2. Navigate to Products → SKU Descriptions
  3. You will see a table like this:
ProductColorSizeSKU
Unisex SweatshirtWhiteMUSs-Wh-M
Unisex SweatshirtBlackLUSs-Bk-L
Men's V-Neck Half SleeveWhiteSMVnHs-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-M works, uss-wh-m does 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, not Authorization or Bearer. ClientId must 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

FieldTypeNotes
order_numberStringMax 15 characters. Longer values can fail.
qikink_shippingStringAlways "1" to let Qikink handle shipping
gatewayString"Prepaid" or "COD"
total_order_valueStringMust be a string, not a number
quantityStringMust be a string, not a number
priceStringMust be a string, not a number
search_from_my_productsInteger1 or 0
country_codeStringMust be "IN" for Indian orders

Critical gotcha: order_number has 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: 1 means: look up this SKU in my dashboard's My Products section.
  • search_from_my_products: 0 means: 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

FieldRequired when search_from_my_products: 0Notes
design_codeYesCannot be empty
width_inchesRecommendedUse "10" as a safe default
height_inchesRecommendedUse "10" as a safe default
placement_skuYes"fr" = Front, "bk" = Back
design_linkRecommendedPublicly accessible image URL
mockup_linkYesCannot be an empty string

Critical gotcha: If search_from_my_products is 0, then design_code, mockup_link, and placement_sku are 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.

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.

  1. Place 3 Sandbox orders.
  2. Request live access from Integration → Custom API.
  3. Email Qikink with the 3 order details and a screenshot of your admin panel.
  4. 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

BehaviorSandbox (true)Live (false)
API URLsandbox.qikink.comapi.qikink.com
Secret UsedQIKINK_SANDBOX_SECRETQIKINK_CLIENT_SECRET
search_from_my_products01
OrdersTest onlyReal 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

AreaKey Details
Auth endpointPOST /api/token
Auth content typeapplication/x-www-form-urlencoded
Auth fieldsClientId, client_secret
Auth response keyAccesstoken
Order endpointPOST /api/order/create
Order content typeapplication/json
Order headersClientId, Accesstoken
Sandbox URLhttps://sandbox.qikink.com
Live URLhttps://api.qikink.com
order_number limitMax 15 characters
quantity / priceMust be strings, not numbers
country_codeMust be IN
Sandbox behaviorsearch_from_my_products = 0
Live behaviorsearch_from_my_products = 1
Go-live checklist3 sandbox orders, then email Qikink, then wait for approval

Conclusion

Qikink's API is powerful but under-documented. The biggest pitfalls are:

  1. Sandbox and Live do not share the same database.
  2. search_from_my_products must be 0 in Sandbox and 1 in Live.
  3. ClientId, client_secret, and Accesstoken are named inconsistently.
  4. Quantity, price, and total_order_value must be strings.
  5. order_number must 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.