๐Ÿ‡ต๐Ÿ‡ฐ Now shipping to 38 countries โ€” white-label, no minimums

Documentation

llms.txt llms-full.txt openapi.yaml

API

CeePrinto public API v2 for integrators

API ยท Start here

API overview

What the CeePrinto v2 API does, the base URL, who it is for and how versioning works.

The CeePrinto v2 API lets your store or app publish CeePrinto designs as products, send in orders that CeePrinto prints, ships and collects cash on delivery for, and track payouts.

What it is and who it is for

CeePrinto is a white-label print-on-demand hub. Merchants design on blank products in the Design Configurator, then sell those designs in their own stores. The API is the single protocol every store connector speaks: the Shopify app, the CeePrinto Connect plugin for WooCommerce and any custom integration you build all call the same routes with the same API key format.

Use it if you are:

  • a developer connecting a store that is not Shopify or WooCommerce (a custom site, a marketplace, an ERP);
  • a merchant automating publishing, order submission or payout reporting;
  • building or debugging a connector and need the exact contract.

Base URLs

Environment Base URL
Production https://ceeprinto.com/wp-json/ceeprinto/v2

Every route in these docs is relative to the base URL. Examples read it from the CP_BASE environment variable.

Authentication in one line

Send Authorization: Bearer cp_live_<key_id>.<secret> on every request, using a key from My Account โ†’ Store Connections โ†’ API Keys. Details in Authentication and scopes.

Three ways to integrate

Path Best for What you do
Shopify app Shopify stores Install the app, paste the cp1. connection code from My Account โ†’ Store Connections โ†’ Get Started. No code.
CeePrinto Connect plugin WooCommerce stores Install the plugin on your WordPress site, paste the cp1. connection code. No code.
Raw API Custom stores and tools Create an API key with the scopes you need and call the routes in this reference.

The two connectors are clients of this same API, so anything they do you can do with the raw API.

Versioning

Item Value
Namespace ceeprinto/v2
Current version 2.2.0
Version header X-CP-Api-Version: 2.2.0 on every response
Compatibility Additive changes (new fields, new routes) ship within v2. Branch on error.code, never on the message text.

Legacy namespaces are deprecated. integration/v1, app/v1, internal/v1 and ceeprinto/v1 still answer, but every response carries Deprecation: true and a Link: <โ€ฆ/openapi.yaml>; rel="deprecation" header. No sunset date is set yet. Build new work on v2 only.

Using these docs with an AI assistant

These pages are written so an AI assistant can produce correct integration code from them.

Resource What it gives you
Copy as prompt button On every section and every endpoint card. Copies that part as Markdown with a short preamble (base URL, Bearer auth, "use only these facts"). Paste it into your assistant.
llms.txt An index of every section with a one-line summary and a link, in the llmstxt.org format.
llms-full.txt The whole documentation as one Markdown file. Best for "read this, then write my integration".
openapi.yaml The OpenAPI 3.0 spec: every operation with operationId, x-scope, examples and webhook payload schemas. Feed it to code generators.
"For AI assistants" boxes The invariants at the end of each section. An assistant should never break them.

A good prompt: "Using the CeePrinto docs below, write a Node 18 script that publishes design 11 to shop 5 and polls until the publish item completes." Then paste llms-full.txt or the relevant copied sections.

API ยท Start here

Getting started

Create an API key in My Account and make your first authenticated call to /me.

In about five minutes you will have an API key, your environment set up, and a successful call to GET /me that shows which account and scopes the key has.

Prerequisites

  • A CeePrinto account (log in at ceeprinto.com).
  • curl, or one of PHP 7.4+ with the cURL extension, Node 18+ or Python 3.8+ with requests.
  1. Create an API key

    Go to My Account โ†’ Store Connections โ†’ API Keys. Give the key a name, tick the scopes it needs and create it. The full key (cp_live_<key_id>.<secret>) is shown once. Copy it now; CeePrinto stores only a hash and cannot show it again.

    Not sure which scopes? For a store integration tick all eight. See Authentication and scopes.

  2. Set environment variables

    Every example in these docs reads the base URL and key from the environment, so nothing secret ends up in your code.

    Shell
    export CP_BASE="https://ceeprinto.com/wp-json/ceeprinto/v2"
    export CP_KEY="cp_live_xxxxxxxxxxxxxxxxxxxxxxxx.yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
  3. Call GET /me

    /me needs no scope, so it is the right smoke test for any key.

    GET /me
    curl
    curl -s "$CP_BASE/me" \
      -H "Authorization: Bearer $CP_KEY"

    PHP inside WordPress? The CeePrinto Connect plugin's includes/Client.php is a complete WordPress-native client (wp_remote_request, Bearer header, JSON envelope) you can copy. The PHP examples in these docs use plain cURL so they run anywhere.

  4. Read the response

    Response 200
    {
      "data": {
        "id": 214,
        "email": "[email protected]",
        "name": "Ayesha Khan",
        "first_name": "Ayesha",
        "last_name": "Khan",
        "company": "Threadline Karachi",
        "api_key": {
          "key_id": "9f3a1c7eK2pQ8mZx4LbN6tRw",
          "name": "WooCommerce Connect",
          "is_legacy": false,
          "last_used_at": "2026-09-29 10:14:52"
        },
        "scopes": [
          "designs:read",
          "products:read",
          "listings:read",
          "listings:write",
          "orders:write",
          "shops:write",
          "webhooks:write"
        ],
        "fulfillment_profile": {
          "complete": true,
          "missing": [],
          "fields": {
            "billing_phone": true,
            "billing_address_1": true,
            "billing_city": true,
            "billing_country": true,
            "billing_company": true,
            "billing_address_2": false,
            "billing_state": true,
            "billing_postcode": true,
            "first_name": true,
            "last_name": true
          }
        }
      }
    }
    Field Meaning
    data.id The merchant id. Store it; it never changes.
    data.api_key.name The key's name. Shopify App and WooCommerce Connect are connection-code keys.
    data.api_key.is_legacy true for an old 24-character key.
    data.scopes What this key may do. Compare with the scopes table.
    data.fulfillment_profile.complete false means billing phone, address, city or country is missing in My Account; missing lists them.
  5. Check what scopes you got

    If scopes lacks orders:read (as in the example above, a connection-code key), GET /orders, GET /payouts and POST /shipping/quote will answer 403. Create a key on the API Keys tab with orders:read ticked if you need them.

If it fails

Status error.code Fix
401 unauthorized Key missing, mistyped, revoked or not prefixed with Bearer . Recopy it or create a new one.
404 rest_no_route Wrong CP_BASE or path. The base must end in /wp-json/ceeprinto/v2.
429 rate_limited Wait Retry-After seconds.

API ยท Start here

Authentication and scopes

Bearer API keys, the scopes each key carries, connection codes, rotation and revocation.

Every v2 request is authenticated with an API key sent as a Bearer token, and each key only reaches the routes its scopes allow.

Headers

Accepted credential headers, in the order they are checked
Header Value Use
Authorization Bearer cp_live_<key_id>.<secret> Preferred. Checked first.
X-CP-Key cp_live_<key_id>.<secret> For clients that cannot set Authorization.
auth <24-character legacy key> Legacy only. What old Shopify app versions send.
Both forms work
curl -s "$CP_BASE/me" -H "Authorization: Bearer $CP_KEY"
curl -s "$CP_BASE/me" -H "X-CP-Key: $CP_KEY"

Key format

Part Format Notes
Prefix cp_live_ Marks a v2 key.
key_id 24 letters and digits Public identifier. Shown in My Account and in GET /me.
Separator .
secret 40 letters and digits Shown once at creation. Stored only as a hash.
Legacy key 24 characters, no prefix Still accepted. See below.

Scopes

Each route requires one scope. A key without it gets 403 forbidden with details.required_scope and details.granted_scopes.

Scope Label in My Account Routes
(none) GET /me
designs:read Read designs GET /designs, GET /designs/{id}, GET|POST /designs/{id}/variation-mockups
products:read Read the blank-product catalog GET /catalog/products, GET /catalog/products/{id}
listings:read Read store listings GET /products, GET /products/{id}, GET /shops, GET /shops/{id}, GET /shops/{id}/listings, GET /publish-jobs/{id}, GET /shops/{id}/publish-jobs/pending
listings:write Create and remove store listings POST /products, PATCH /products/{id}, PATCH /products/{id}/variants/{variation_id}, listing POST/PATCH/DELETE, POST /publish-jobs, publish item complete/fail
orders:read Read orders GET /orders, GET /orders/{id}, GET /orders/{id}/events, GET /payouts, GET /payouts/summary, POST /shipping/quote
orders:write Submit orders POST /orders
shops:write Connect and disconnect stores POST /shops, DELETE /shops/{id}
webhooks:write Manage webhook subscriptions GET /webhooks, POST /webhooks, DELETE /webhooks/{id}
Response 403
{
  "error": {
    "code": "forbidden",
    "message": "This API key does not have the \"orders:read\" scope.",
    "details": {
      "required_scope": "orders:read",
      "granted_scopes": [
        "designs:read",
        "products:read",
        "listings:read",
        "listings:write",
        "orders:write",
        "shops:write",
        "webhooks:write"
      ]
    }
  }
}

Connection codes (cp1.)

The Shopify app and the CeePrinto Connect plugin do not ask the merchant for a raw key. Instead My Account โ†’ Store Connections โ†’ Get Started generates a one-time connection code that bundles the hub URL and a freshly minted key.

Property Value
Format cp1. + base64url (no padding) of JSON {"v":1,"base":"https://ceeprinto.com/","key":"cp_live_โ€ฆ"}
Visible for 2 minutes, once. Reloading the page after that shows nothing.
Key name WooCommerce Connect or Shopify App
Rotation Generating a new code revokes the previous key of the same name.
Scopes granted designs:read, products:read, listings:read, listings:write, orders:write, shops:write, webhooks:write
Scope omitted orders:read

Connection-code keys cannot read orders. With a "Shopify App" or "WooCommerce Connect" key, GET /orders, GET /orders/{id}, GET /orders/{id}/events, GET /payouts, GET /payouts/summary and POST /shipping/quote return 403. They can still submit orders. For reporting, create a separate key with orders:read on the API Keys tab.

Decoding a connection code
import base64, json

code = "cp1.eyJ2IjoxLCJiYXNlIjoiaHR0cHM6Ly9jZWVwcmludG8uY29tLyIsImtleSI6ImNwX2xpdmVfLi4uIn0"
raw = code[len("cp1."):]
payload = json.loads(base64.urlsafe_b64decode(raw + "=" * (-len(raw) % 4)))
base_url = payload["base"].rstrip("/") + "/wp-json/ceeprinto/v2"
api_key = payload["key"]

Rotation and revocation

Action How Effect
Rotate Create a new key on the API Keys tab, deploy it, then revoke the old one. Zero downtime.
Revoke API Keys tab โ†’ Revoke. Immediate: the next request with that key is 401 "This API key has been revoked."
Regenerate a connection code Get Started tab. Revokes the previous key with the same name.
Last used Shown on the API Keys tab and in GET /me as api_key.last_used_at. Updated at most once a minute.

Legacy keys

Accounts created before v2 have a 24-character key without the cp_live_ prefix. It still works in any of the three headers and holds every scope. GET /me reports is_legacy: true and masks key_id to its last 4 characters. Move to a scoped v2 key when you can.

Security tips

  • Keep keys server-side. Never ship one in browser JavaScript or a mobile app.
  • Give each integration its own key with the fewest scopes it needs, so you can revoke one without breaking the others.
  • Store keys in environment variables or a secrets manager, not in source control.
  • Revoke immediately if a key leaks, then check the API Keys tab for unexpected "last used" times.
  • Always call HTTPS in production.

The response envelope, error codes, pagination headers, rate limits, request ids and idempotency.

Every v2 route shares one response envelope, one error format, one pagination scheme and one rate limit, so a single client wrapper handles them all.

Response envelope

Success bodies wrap the payload in data. meta appears only when there is something to say (pagination, replay flags, cache flags). Error bodies have a single error object and no data.

Success
{
  "data": [
    {
      "id": 5,
      "channel": "custom",
      "external_shop_id": "my-store-01",
      "name": "My Store",
      "webhook_url": null,
      "status": "active",
      "connected_at": "2026-09-15T07:30:00+00:00"
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 20,
    "total": 1,
    "total_pages": 1
  }
}
Error
{
  "error": {
    "code": "unprocessable_entity",
    "message": "The order could not be accepted.",
    "details": {
      "issues": [
        "line_items[0].customer_price is required and drives COD collection."
      ]
    }
  }
}

error.details is present only when there are details; otherwise the key is absent. error.code is the contract; error.message is for humans and may change. A 204 response has no body at all.

Error codes

HTTP error.code Meaning details
400 invalid_request A required field or header is missing or malformed (e.g. no Idempotency-Key). Sometimes valid_topics
401 unauthorized No credential, malformed, unknown or revoked key. None
403 forbidden The key lacks the route's scope. required_scope, granted_scopes
404 not_found The resource does not exist or belongs to another account. None
409 conflict The store is already connected to another account. None
422 unprocessable_entity Well-formed but invalid (bad mode, missing customer_price). issues[] on orders
429 rate_limited Over 120 requests in the current minute. retry_after
500 server_error Something failed on the hub. None

WordPress's own validation errors are passed through in the same envelope with their native codes: rest_no_route (404, wrong path or method) and rest_missing_callback_param / rest_invalid_param (400, with details.params). Treat any unknown 4xx code as a client error. Full list with fixes in Errors and troubleshooting.

Pagination

Item Value
Query parameters page (1-based), per_page
Default per_page 20
Max per_page 100 (larger values are clamped; values below 1 fall back to 20)
Body meta.page, meta.per_page, meta.total, meta.total_pages
Headers X-CP-Total, X-CP-Total-Pages, Link
Link relations prev and first when page > 1; next and last when more pages exist; absent when there is one page
Not paginated GET /shops/{id}/publish-jobs/pending, GET /webhooks, GET /orders/{id}/events: bare array in data, no meta
GET /catalog/products?page=2&per_page=100
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
X-CP-Request-Id: req_3f9a1c7e5b2d8a40
X-CP-Api-Version: 2.2.0
X-CP-RateLimit-Limit: 120
X-CP-RateLimit-Remaining: 117
X-CP-RateLimit-Reset: 1790676060
X-CP-Total: 230
X-CP-Total-Pages: 3
Link: <https://ceeprinto.com/wp-json/ceeprinto/v2/catalog/products?page=1&per_page=100>; rel="prev", <https://ceeprinto.com/wp-json/ceeprinto/v2/catalog/products?page=1&per_page=100>; rel="first", <https://ceeprinto.com/wp-json/ceeprinto/v2/catalog/products?page=3&per_page=100>; rel="next", <https://ceeprinto.com/wp-json/ceeprinto/v2/catalog/products?page=3&per_page=100>; rel="last"

Loop until meta.page >= meta.total_pages, or until the Link header has no rel="next". A complete example is in Recipes (catalog sync).

Rate limiting

Item Value
Limit 120 requests per key per fixed 60-second window
Scope Per API key; one merchant never limits another
X-CP-RateLimit-Limit Requests allowed per window (120)
X-CP-RateLimit-Remaining Requests left in this window
X-CP-RateLimit-Reset Unix time when the window resets
Over the limit 429 rate_limited, Retry-After header (seconds) and details.retry_after

Honour Retry-After on 429, and back off exponentially on 5xx and network errors. A reusable wrapper:

Retry wrapper
curl
#!/usr/bin/env bash
# cp_call METHOD PATH [JSON_BODY]  - retries 429 (Retry-After) and 5xx (1s, 2s, 4s, 8s)
cp_call() {
  local method="$1" path="$2" body="${3:-}" attempt=0 status headers
  while :; do
    headers=$(mktemp)
    if [ -n "$body" ]; then
      status=$(curl -s -o /tmp/cp_body -D "$headers" -w "%{http_code}" -X "$method" "$CP_BASE$path" \
        -H "Authorization: Bearer $CP_KEY" -H "Content-Type: application/json" -d "$body")
    else
      status=$(curl -s -o /tmp/cp_body -D "$headers" -w "%{http_code}" -X "$method" "$CP_BASE$path" \
        -H "Authorization: Bearer $CP_KEY")
    fi
    if [ "$status" = "429" ] || [ "$status" -ge 500 ] 2>/dev/null; then
      attempt=$((attempt + 1))
      [ "$attempt" -gt 4 ] && { cat /tmp/cp_body; rm -f "$headers"; return 1; }
      wait=$(grep -i '^retry-after:' "$headers" | tr -dc '0-9')
      sleep "${wait:-$((2 ** (attempt - 1)))}"
      rm -f "$headers"
      continue
    fi
    rm -f "$headers"
    cat /tmp/cp_body
    [ "$status" -lt 400 ]
    return
  done
}

cp_call GET /me

The limit is enforced only when the hub runs a persistent object cache. The headers are always sent, so write your client as if the limit is always on.

Request id and version headers

Header Example Use
X-CP-Request-Id req_3f9a1c7e5b2d8a40 On every response, success or error. Log it; quote it in support tickets.
X-CP-Api-Version 2.2.0 On every response. The hub version that served the request.

Timestamps and money

Item Format
Timestamps RFC 3339 in UTC, e.g. 2026-09-29T10:15:00+00:00. Nullable ones are null, never an empty string.
Exception api_key.last_used_at on GET /me is YYYY-MM-DD HH:MM:SS UTC.
Dates in filters YYYY-MM-DD (date_from, date_to on /payouts)
Currency PKR unless stated. customer_price is a decimal string, e.g. "2500.00".
Webhook sent_at ISO 8601 UTC, e.g. 2026-09-29T10:15:00+00:00

Idempotency

Route Behaviour on repeat
POST /orders Requires Idempotency-Key (max 191 chars). Same key returns 200 with the original intake and meta.idempotent_replay: true. Missing key is 400.
POST /shops Upserts on (channel, external_shop_id). Always 201.
POST /shops/{id}/listings and /bulk Upserts on external_variant_id. Rows without one are inserted again.
POST /publish-jobs Not idempotent. In create mode designs already listed on the shop are skipped; if nothing is left it is 400.
POST /webhooks Not idempotent: each call creates a new subscription.

API ยท Start here

Core concepts

Glossary of blanks, designs, hub products, variants, listings, shops, publish jobs, mockups and payouts.

This glossary defines the objects the API moves around, so the rest of the docs can use each term in exactly one sense.

Glossary

Term What it is Where in the API
Blank A plain WooCommerce product CeePrinto prints on (a tee, a hoodie). Has variations (Size ร— Color) and print sides. GET /catalog/products
Variation One Size/Color combination of a blank. variation_id is its WooCommerce id; 0 means a simple blank with no variations. variations[] on a catalog product
Stage (print side) A printable side of a blank: Front, Back, Sleeveโ€ฆ stage_index 0 is always the front. print_config.stages[], mockup cells
Design Artwork a merchant saved in the Design Configurator on a specific blank. Owned by one account. GET /designs
Hub product The merchant's sellable product on CeePrinto: one blank + a default design + optional per-variant overrides. Channel-independent; publishable to any store. /products
Variant (inherit / override) One row per blank variation inside a hub product. design_id: null inherits the default; a number overrides it. effective_design_id is the design actually printed. variants[], PATCH /products/{id}/variants/{variation_id}
Shop A connected storefront. channel is a free-form label (shopify, woocommerce, custom); external_shop_id is your id for it. /shops
Listing A link from a store product/variant (external_product_id, external_variant_id, external_sku) to a design and blank variation. How an incoming order line finds its design. /shops/{id}/listings
Publish job A request to put designs (or a hub product) into a shop. Has one item per design. POST /publish-jobs
Publish item One design to publish. The connector does the store work and closes it with complete or fail. /publish-jobs/items/{item_id}/โ€ฆ
Mode create a new store product, link to an existing one, or update one already live. mode on publish jobs
Price mode blank uses the blank's price; flat uses flat_price for every variant. price_mode, flat_price
Mockup matrix Product photos of a design: one cell per variation ร— stage. Each cell has a 240px thumb_url (hub UI) and a 2048px image_url (stores). GET /designs/{id}/variation-mockups
Intake An order as received by the API, before it becomes a WooCommerce order. Statuses: received, processing, created, failed, ignored. POST /orders, GET /orders
WooCommerce order What an intake becomes. Created as Pending payment in the merchant's My Account โ†’ Orders. Its id is wc_order_id. order on GET /orders/{id}
customer_price What the end buyer pays for a line, as a decimal string. Required. Drives how much is collected on delivery. line_items[].customer_price
COD From Customer The merchant's election, on the pending order, to have CeePrinto collect cash from the buyer on delivery and pay it out. additional_cod_from_customer is an extra amount (e.g. delivery fee) added to the collection. cod_from_customer, cod_collection_total
Payout Money CeePrinto owes the merchant for a delivered COD order: sum of customer_price plus additional_cod_from_customer. Held 7 days after courier-confirmed delivery. GET /payouts
Fulfillment profile The merchant's billing phone, address, city and country, used as the sender on every order. fulfillment_profile on GET /me
Webhook subscription A target_url that receives signed POSTs for one topic. /webhooks

Payout statuses

Resolved in this order; the first match wins.

payout_status Meaning
processed Paid out (label "Paid out"). processed_at is set.
missing_bank The merchant has no bank code or account number in My Account โ†’ Pay Out (label "Missing bank details"). Checked before delivery state.
awaiting_delivery Delivery not confirmed yet (label "Awaiting delivery").
in_hold Delivered; inside the 7-day hold (label "7-day hold"). release_at says when it ends.
ready Hold over; due in the next payout (label "Ready for payout").

How the pieces fit

  1. The merchant saves a design on a blank in the Design Configurator.
  2. Optionally they compose a hub product: default design plus per-variant overrides.
  3. A shop is connected (POST /shops).
  4. A publish job is created. The connector polls pending items, fetches the mockup matrix, creates the store product, registers one listing per store variant (POST โ€ฆ/listings/bulk) and marks the item complete.
  5. A buyer orders in the store. The connector submits an intake (POST /orders) with customer_price per line.
  6. The hub resolves each line through the listings, creates a Pending payment WooCommerce order, and the merchant elects COD From Customer.
  7. CeePrinto prints and ships; order.shipped carries tracking.
  8. After delivery and the 7-day hold, the amount appears as a payout.

API ยท Start here

Quickstart (curl)

A step-by-step curl walkthrough from first key to a published listing and a test order.

With nothing but curl and an API key you will connect a store, publish a design, submit a COD order and subscribe to events, the same protocol the Shopify app and CeePrinto Connect use.

A channel: "custom" integration gets exactly the same behaviour as the official connectors, including cash on delivery. Use a key with all eight scopes (created on My Account โ†’ Store Connections โ†’ API Keys) so every step below works.

  1. Set your base URL and key

    bash
    export CP_BASE="https://ceeprinto.com/wp-json/ceeprinto/v2"
    export CP_KEY="cp_live_xxxxxxxxxxxxxxxxxxxxxxxx.yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
    alias cp='curl -s -H "Authorization: Bearer $CP_KEY" -H "Content-Type: application/json"'
  2. Who am I (200)

    bash
    cp "$CP_BASE/me"

    Returns your numeric account id (data.id), granted scopes, and whether your billing details are complete (fulfillment_profile.complete).

  3. Connect a store (201)

    bash
    cp -X POST "$CP_BASE/shops" \
      -d '{"channel":"custom","external_shop_id":"my-store-01","name":"My Store"}'
    # โ†’ 201 { "data": { "id": 5, "channel": "custom", "status": "active", ... } }
    export CP_SHOP=5

    channel is a free-form label: custom here, woocommerce for a Woo plugin, shopify for the app. Re-posting the same external_shop_id refreshes the shop instead of duplicating it. 409 means another CeePrinto account already owns it.

  4. Browse designs and the catalog (200)

    bash
    cp "$CP_BASE/designs?per_page=20"
    cp "$CP_BASE/designs/11"
    cp "$CP_BASE/catalog/products?per_page=100"
    cp "$CP_BASE/catalog/products/37"

    A design's product_id is the blank it was made on. The catalog product lists that blank's variations and print stages.

  5. Get the design's product images (200)

    bash
    cp "$CP_BASE/designs/11/variation-mockups"
    # โ†’ { "data": { "status": "ready", "total": 24, "store_status": "ready", "store_ready": 24, "queued": 0,
    #              "items": [ { "variation_id": 101, "stage_index": 0, "stage_name": "Front",
    #                           "store_ready": true, "thumb_url": "...", "image_url": "..." }, ... ] } }

    Mockups render on request at the URLs returned, so there is nothing to queue and usually nothing to wait for. Still check store_status is "ready" (or "partial", skipping cells with failed: true) before uploading. POST to the same URL is kept for older clients and answers 202 with queued: 0.

    Rule Why
    Upload image_url (2048px), never thumb_url (240px) Thumbs are for the hub UI and look blurry in a store.
    stage_index 0 is the front Use it as the main product image.
    Key cells by variation_id + stage_index There is one cell per variation per side. A plain variation_id โ†’ url map lets the back overwrite the front.
  6. Link the design to your store product (201)

    For one variant:

    bash
    cp -X POST "$CP_BASE/shops/$CP_SHOP/listings" \
      -d '{"design_id":11,"product_id":37,"variation_id":101,"external_product_id":"9001","external_variant_id":"9001-1","external_sku":"TEE-L"}'

    For a full Size ร— Color matrix use bulk (200 with a per-row summary), one row per store variant, each with its own design_id:

    bash
    cp -X POST "$CP_BASE/shops/$CP_SHOP/listings/bulk" \
      -d '{"listings":[
        {"design_id":11,"product_id":37,"variation_id":101,"external_product_id":"9001","external_variant_id":"9001-1"},
        {"design_id":11,"product_id":37,"variation_id":102,"external_product_id":"9001","external_variant_id":"9001-2"}
      ]}'

    external_variant_id / external_sku are how your store identifies the item; CeePrinto resolves incoming orders back to the design through these listings.

  7. Or publish through a publish job (201) and poll it

    bash
    # Create a brand-new product in the store:
    cp -X POST "$CP_BASE/publish-jobs" \
      -d '{"shop_id":'"$CP_SHOP"',"design_ids":[11],"price_mode":"blank","mode":"create"}'
    
    # Or attach the design to a product you already sell:
    cp -X POST "$CP_BASE/publish-jobs" \
      -d '{"shop_id":'"$CP_SHOP"',"design_ids":[11],"mode":"link","external_product_id":"9001"}'
    
    # Or publish a whole hub product (default design + per-variant overrides):
    cp -X POST "$CP_BASE/publish-jobs" \
      -d '{"shop_id":'"$CP_SHOP"',"product_id":42,"price_mode":"blank","mode":"create"}'
    
    # Then poll. Do not rely on the design.publish_requested webhook alone.
    cp "$CP_BASE/shops/$CP_SHOP/publish-jobs/pending"
    # โ†’ { "data": [ { "id": 7, "job_id": 3, "design_id": 11, "mode": "create",
    #                 "external_product_id": null, "variant_count": 12, "status": "pending", ... } ] }
    
    # Do the store work, then close the item (200). Always one or the other:
    cp -X POST "$CP_BASE/publish-jobs/items/7/complete" -d '{"external_product_id":"9001"}'
    cp -X POST "$CP_BASE/publish-jobs/items/7/fail"     -d '{"error":"variant matrix rejected"}'

    mode defaults to create. An unknown mode is 422, so a typo never creates a duplicate product. link and update require external_product_id (422 without). Full walkthrough: Publish a design to a store.

  8. Submit an order (202)

    bash
    cp -X POST "$CP_BASE/orders" \
      -H "Idempotency-Key: my-store-01:1001" \
      -d '{
        "shop_id": '"$CP_SHOP"',
        "external_order_id": "1001",
        "currency": "PKR",
        "shipping_address": {
          "first_name": "Bilal", "address_1": "Flat 4B, Block 5, Clifton",
          "city": "Karachi", "country": "PK", "phone": "03001234567"
        },
        "line_items": [
          { "external_variant_id": "9001-1", "quantity": 1,
            "customer_price": "2500.00", "customer_variant": "L / Black" }
        ]
      }'
    # โ†’ 202 { "data": { "id": 1, "status": "received", "wc_order_id": null, ... } }
    Rule Result if broken
    Idempotency-Key header is required 400 invalid_request
    customer_price on every line (what the buyer pays, collected on COD) 422 with details.issues[]; never defaulted
    shop_id must be yours 400 (not 404)
    Same Idempotency-Key again 200 with the original intake and meta.idempotent_replay: true; nothing new is created, so retrying a timeout is safe
  9. Watch it become an order (200)

    bash
    cp "$CP_BASE/orders/1"
    cp "$CP_BASE/orders/1/events"

    The order is created as Pending payment in the merchant's My Account โ†’ Orders, where they elect COD From Customer. If a line item cannot be matched the intake becomes failed with a specific last_error: create the missing listing and retry from My Account โ†’ Store Connections โ†’ Order Activity.

  10. Receive events (201)

    bash
    cp -X POST "$CP_BASE/webhooks" \
      -d '{"topic":"order.status_changed","target_url":"https://my-store-01.pk/webhooks/ceeprinto"}'
    # โ†’ 201 { "data": { "id": 9, "secret": "whsec_...", ... } }

    Each delivery is signed X-CP-Signature: t=<ts>,v1=<hex>, an HMAC-SHA256 of <ts>.<raw body> with the secret. See Webhooks for verification code.

API ยท Guides

Connect a store

Register your shop with CeePrinto so designs can be published to it and orders can flow back.

Registering your storefront once gives you a shop id that every publish job, listing and order refers to.

Prerequisites

  • An API key with shops:write (to connect) and listings:read (to list shops).
  • A stable id for your store that you control (external_shop_id): a domain, a Shopify shop handle, a tenant id.

Which path?

Your store Do this channel
Shopify Install the CeePrinto Shopify app and paste the cp1. code from My Account โ†’ Store Connections โ†’ Get Started. The app calls POST /shops for you. shopify
WooCommerce Install CeePrinto Connect and paste the cp1. code. The plugin calls POST /shops for you. woocommerce
Anything else Call POST /shops yourself, as below. custom (or any label up to 32 characters)
  1. Register the shop

    POST /shops upserts on (channel, external_shop_id), so it is safe to call on every connector start-up.

    POST /shops
    curl
    curl -s -X POST "$CP_BASE/shops" \
      -H "Authorization: Bearer $CP_KEY" \
      -H "Content-Type: application/json" \
      -d '{"channel":"custom","external_shop_id":"my-store-01","name":"My Store"}'
    Response 201
    {
      "data": {
        "id": 5,
        "channel": "custom",
        "external_shop_id": "my-store-01",
        "name": "My Store",
        "webhook_url": null,
        "status": "active",
        "connected_at": "2026-09-29T10:15:00+00:00"
      }
    }
  2. Store the shop id

    Save data.id (here 5). You need it for /shops/{id}/listings, /shops/{id}/publish-jobs/pending, POST /publish-jobs and POST /orders.

  3. Check the connection

    bash
    curl -s "$CP_BASE/shops" -H "Authorization: Bearer $CP_KEY"
    curl -s "$CP_BASE/shops/5" -H "Authorization: Bearer $CP_KEY"

    The shop now appears in My Account โ†’ Store Connections โ†’ Stores.

  4. Subscribe to events (optional)

    Subscribe to design.publish_requested and order.shipped so your store reacts quickly. See Webhooks. Always poll as well; webhooks are best-effort.

  5. Disconnect or reconnect

    bash
    # Disconnect (soft): 200 with "status": "disconnected"
    curl -s -X DELETE "$CP_BASE/shops/5" -H "Authorization: Bearer $CP_KEY"
    
    # Reconnect: POST the same channel + external_shop_id again (201, status back to "active")
    curl -s -X POST "$CP_BASE/shops" -H "Authorization: Bearer $CP_KEY" -H "Content-Type: application/json" \
      -d '{"channel":"custom","external_shop_id":"my-store-01","name":"My Store"}'

Outcomes

Call Status Meaning
POST /shops, new store 201 Created, status: "active"
POST /shops, same store again 201 Refreshed (name overwritten, disconnected shop reactivated), same id. Always send name: omitting it clears it.
POST /shops, store owned by another account 409 conflict. Ask the other account to disconnect it.
POST /shops, missing or > 32-character channel 400 invalid_request or rest_missing_callback_param
DELETE /shops/{id} 200 Body is the shop with status: "disconnected". Listings and orders are kept.
Any route with another account's shop id 404 not_found

API ยท Guides

Publish a design to a store

Generate mockups, wait until the store is ready, create a publish job and poll it to completion.

A publish job turns a design or hub product into a real product in your store, with correct images per variant and a listing per variant so orders route back to the right design.

Prerequisites

  • A connected shop id (Connect a store).
  • A key with listings:write, listings:read and designs:read.
  • A design id (GET /designs) or a hub product id (GET /products).

Who does what

Step Done by
Create the publish job The merchant (Products or Get Started tab in My Account) or you via POST /publish-jobs
Find pending items Your connector, by polling GET /shops/{id}/publish-jobs/pending (and optionally the design.publish_requested webhook)
Create or update the store product, upload images Your connector
Register listings Your connector, POST /shops/{id}/listings/bulk
Close the item Your connector, complete or fail

Choosing mode

mode What the connector does external_product_id Already-listed designs
create (default) Creates a new store product. Not needed Skipped. If every design is already listed: 400.
link Attaches the design to an existing store product (adds listings, images). Required (422 without) Included
update Non-destructively edits a product already live in the store. Required (422 without) Included
anything else Nothing: rejected so a typo never creates a duplicate. 422 unprocessable_entity

Choosing price_mode

price_mode Store price
blank (default) The blank's price (from /catalog/products). For hub products, a variant's own price, when set, is passed to the connector in variants[] to apply.
flat flat_price for every variant
  1. Create the publish job

    Send design_ids for one or more designs, or product_id for a hub product (designs and per-variant composition are derived from it). Answers 201 and fires design.publish_requested.

    POST /publish-jobs
    curl
    curl -s -X POST "$CP_BASE/publish-jobs" \
      -H "Authorization: Bearer $CP_KEY" \
      -H "Content-Type: application/json" \
      -d '{"shop_id":5,"design_ids":[11],"price_mode":"blank","mode":"create"}'
    Other shapes
    # Publish a hub product at a flat price
    curl -s -X POST "$CP_BASE/publish-jobs" -H "Authorization: Bearer $CP_KEY" -H "Content-Type: application/json" \
      -d '{"shop_id":5,"product_id":42,"price_mode":"flat","flat_price":2500,"mode":"create"}'
    
    # Attach design 11 to store product 9001
    curl -s -X POST "$CP_BASE/publish-jobs" -H "Authorization: Bearer $CP_KEY" -H "Content-Type: application/json" \
      -d '{"shop_id":5,"design_ids":[11],"mode":"link","external_product_id":"9001"}'
  2. Poll for pending items

    Poll every 30 to 60 seconds. The webhook is only a hint to poll sooner: a missed delivery would otherwise lose the publish silently. The list is not paginated.

    bash
    curl -s "$CP_BASE/shops/5/publish-jobs/pending" -H "Authorization: Bearer $CP_KEY"
    Response 200
    {
      "data": [
        {
          "id": 7,
          "job_id": 3,
          "design_id": 11,
          "product_id": 37,
          "variant_count": 12,
          "status": "pending",
          "mode": "create",
          "external_product_id": null,
          "product_id_hub": 42,
          "error_text": null,
          "created_at": "2026-09-29T10:15:00+00:00",
          "updated_at": "2026-09-29T10:15:00+00:00",
          "completed_at": null,
          "shop_id": 5,
          "price_mode": "blank",
          "flat_price": null
        }
      ]
    }

    Handle webhook-delivered and polled items with the same code, and de-duplicate by item id.

  3. Work out each variant's design

    Item has Read variants from
    product_id_hub set GET /products/{product_id_hub}: use each variant's effective_design_id, skip enabled: false, use its price/sku when set
    product_id_hub: null GET /catalog/products/{product_id}: every variation uses the item's design_id
  4. Fetch the images

    bash
    curl -s "$CP_BASE/designs/11/variation-mockups" -H "Authorization: Bearer $CP_KEY"

    For each distinct design, read the matrix. Mockups render on request at the URLs returned; nothing is queued (queued is 0).

    Check Rule
    store_status Upload only when "ready". "partial" means some cells failed: upload the rest, skip failed: true. "generating": wait 5 seconds and GET again. "unavailable": the renderer is off; fail the item.
    Which URL image_url (2048px). Never thumb_url (240px).
    Which side stage_index 0 is the front: make it the main image. Other stages are extra gallery images.
    Keying By variation_id + stage_index. One cell per variation per stage.
    Fetching Download each image_url and upload the bytes to your store. Retry a failed download once after a few seconds.
  5. Create the product and register listings

    Create (or link, or update) the store product with one store variant per enabled blank variation. Then register one listing per store variant in a single bulk call:

    bash
    curl -s -X POST "$CP_BASE/shops/5/listings/bulk" \
      -H "Authorization: Bearer $CP_KEY" -H "Content-Type: application/json" \
      -d '{"listings":[
        {"design_id":11,"product_id":37,"variation_id":101,"external_product_id":"9001","external_variant_id":"9001-1"},
        {"design_id":12,"product_id":37,"variation_id":102,"external_product_id":"9001","external_variant_id":"9001-2"}
      ]}'
    # โ†’ 200 { "data": [ ... ], "meta": { "created": 2, "failed": 0, "errors": [] } }

    Always send external_variant_id: listings upsert on it, so a retry updates instead of duplicating. Check meta.failed; rows fail independently.

  6. Close the item

    bash
    # Success: send the store product id
    curl -s -X POST "$CP_BASE/publish-jobs/items/7/complete" \
      -H "Authorization: Bearer $CP_KEY" -H "Content-Type: application/json" \
      -d '{"external_product_id":"9001"}'
    
    # Failure: the message is shown to the merchant
    curl -s -X POST "$CP_BASE/publish-jobs/items/7/fail" \
      -H "Authorization: Bearer $CP_KEY" -H "Content-Type: application/json" \
      -d '{"error":"Store rejected the variant matrix: too many options"}'

    Every item must end in one of the two, or it stays pending and the merchant sees "publishing" forever. For hub-product items, complete also back-fills product_id_hub on the matching listings.

After publishing

Change Call What your connector gets
Swap a hub variant's design PATCH /products/{id}/variants/{variation_id} {"design_id": 12} product.updated with the new per-variant designs and shop_ids; listings already updated on the hub
Change the default design PATCH /products/{id} {"default_design_id": 12} product.updated
Override one listing only PATCH /shops/{id}/listings/{listing_id} Nothing (no webhook)
Store product deleted DELETE /shops/{id}/listings?external_product_id=9001 204

Errors when creating a job

Check (in order) Status
Unknown mode 422 unprocessable_entity
shop_id not yours 404 not_found
product_id not yours 404 not_found
No design_ids and no product_id (or product has no designs) 400 invalid_request
link/update without external_product_id 422 unprocessable_entity
Every design already published to this shop, or unloadable 400 invalid_request

API ยท Guides

Submit and track orders

Send orders with an Idempotency-Key, understand line-item resolution and follow order status.

Submitting each store order once, with an Idempotency-Key and the buyer's price per line, gets it printed, shipped and collected on delivery, and lets you follow it to tracking.

Prerequisites

  • A key with orders:write. Reading orders back needs orders:read, which connection-code keys ("Shopify App", "WooCommerce Connect") do not have.
  • A connected shop id, and listings for the products you sell (Publish a design), or the design id per line.
  • The merchant's billing details complete (GET /me โ†’ fulfillment_profile.complete).

How a line item finds its design

Each line must carry at least one reference. The hub tries them in this order and uses the first match:

Order Reference Matched against
1 listing_id Your listing with that id on this shop
2 external_variant_id A listing on this shop with that external_variant_id
3 external_sku A listing on this shop with that external_sku
4 external_variant_id / external_sku Legacy v1 integration products (older accounts only)
5 design_id A design you own (prints on the blank variation it was made on)

If nothing matches, no WooCommerce order is created for any line and the intake is marked failed.

Required fields

Field Rule If missing
Idempotency-Key header Unique per order, max 191 characters. Scoped to the shop. Use <external_shop_id>:<external_order_id>. 400
shop_id A shop you own 400 (not 404)
shipping_address.first_name, address_1, city Non-empty. country is ISO 3166-1 alpha-2 (PK). 422
line_items At least one 422
line_items[].quantity At least 1 422
line_items[].customer_price Numeric string, what the buyer pays. Never defaulted: 0 would collect nothing. 422
A reference per line One of listing_id, external_variant_id, external_sku, design_id 422
currency Optional, default PKR

customer_price and quantity. The hub adds up customer_price across line items to get customer_price_total and cod_collection_total; it does not multiply by quantity. For lines with quantity above 1, confirm the collection amount on GET /orders/{id} (order.cod_collection_total) or in My Account โ†’ Orders before the merchant elects COD.

  1. Submit the order

    A new key answers 202 with status: "received". A background worker then builds the WooCommerce order, usually within seconds.

    POST /orders
    curl
    curl -s -X POST "$CP_BASE/orders" \
      -H "Authorization: Bearer $CP_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: my-store-01:1001" \
      -d '{
        "shop_id": 5,
        "external_order_id": "1001",
        "currency": "PKR",
        "shipping_address": {
          "first_name": "Bilal", "last_name": "Ahmed", "phone": "03001234567",
          "address_1": "Flat 4B, Block 5, Clifton", "city": "Karachi", "country": "PK"
        },
        "line_items": [
          { "external_variant_id": "9001-1", "quantity": 1,
            "customer_price": "2500.00", "customer_variant": "L / Black" }
        ]
      }'
    Response 202
    {
      "data": {
        "id": 1,
        "shop_id": 5,
        "external_order_id": "1001",
        "status": "received",
        "wc_order_id": null,
        "attempts": 0,
        "last_error": null,
        "received_at": "2026-09-29T10:15:00+00:00",
        "processed_at": null
      }
    }
  2. Retry safely

    You got Do
    202 Store data.id. Done.
    200 with meta.idempotent_replay: true This key was already accepted. data is the original intake (it may already be created). Nothing new was made.
    Timeout / network error / 5xx / 429 Resend the same body with the same Idempotency-Key. Never mint a new key for a retry.
    400 / 403 / 422 Fix the request. Do not retry unchanged.

    A replay returns the first intake even if the body changed; use a new key only for a genuinely new order.

  3. Follow the intake

    Needs orders:read
    curl -s "$CP_BASE/orders/1" -H "Authorization: Bearer $CP_KEY"
    curl -s "$CP_BASE/orders/1/events" -H "Authorization: Bearer $CP_KEY"
    Intake status Meaning
    received Accepted and queued.
    processing The worker is building the order. attempts counts tries.
    created WooCommerce order exists: wc_order_id set, order block present on GET /orders/{id}.
    failed No order was created. last_error says why.
    ignored Reserved; not set by the current API.
  4. Handle a failed intake

    last_error starts with Cause Fix
    No listing, design or legacy product matched this line item (โ€ฆ) No reference on a line resolved. The parentheses list what you sent. Create the listing (POST /shops/{id}/listings) or send design_id, then the merchant clicks Retry in My Account โ†’ Store Connections โ†’ Order Activity.
    WooCommerce product N no longer exists. The blank or variation was removed from the catalog. Re-publish on a current blank and update the listing.
    Order has no line items. Stored payload had no lines. Submit a new order with a new key.
    A line item could not be added to the order. WooCommerce refused the product (e.g. not purchasable). Contact support with the X-CP-Request-Id.

    Retrying the same Idempotency-Key through the API does not reprocess a failed intake; it returns it. Reprocessing is the Retry button in Order Activity.

  5. Merchant elects COD, CeePrinto ships

    The order lands as Pending payment in the merchant's My Account โ†’ Orders, where they elect COD From Customer (and any additional amount). Then subscribe to order.status_changed and order.shipped (Webhooks). order.shipped carries tracking_number, tracking_company and tracking_url; if you miss it, GET /orders/{id} โ†’ order.tracking has the same data.

Totals on GET /orders/{id}

Field Is
order.total What the merchant owes CeePrinto: blank price per line plus shipping (default 200 PKR Karachi, 250 PKR elsewhere).
order.customer_price_total Sum of line customer_price values.
order.additional_cod_from_customer Extra amount the merchant added to the collection.
order.cod_collection_total customer_price_total + additional_cod_from_customer: what the courier collects.

API ยท Guides

Webhooks

Subscribe to event topics, verify the X-CP-Signature header and handle retries.

Webhooks push signed events to your server within seconds of an order, design, stock or publish change, so your store reacts without constant polling.

Prerequisites

  • A key with webhooks:write.
  • A public HTTPS endpoint that answers 2xx within 10 seconds.

Topics

Topic Fires when Typical reaction
order.status_changed A WooCommerce order placed through the API changes status Update the order status in your store
order.shipped An order moves to completed with a live courier booking Mark fulfilled, send tracking to the buyer
design.updated A saved design changes Refresh images if you show that design
design.publish_requested A publish job is created Poll pending items now
product.stock_changed A blank or variation you have listed changes stock Update availability in your store
product.updated A published hub product's default or variant design changes Swap images per variant

Payload schemas and full examples: Webhook events reference.

Delivery

Property Value
Method POST, Content-Type: application/json
Body {"topic": "โ€ฆ", "data": {โ€ฆ}, "sent_at": "2026-09-29T10:15:00+00:00"}
Headers X-CP-Topic: <topic>, X-CP-Signature: t=<unix ts>,v1=<hex>
Signature Hex HMAC-SHA256 of <t>.<raw body> with the subscription's whsec_โ€ฆ secret
Timeout 10 seconds
Success Any 2xx status
Attempts 5 in total
Back-off 1, 2, 4 and 8 minutes after attempts 1 to 4
Order Not guaranteed. Use sent_at and re-read state with GET when order matters.
Guarantee Best-effort. Also poll GET /shops/{id}/publish-jobs/pending and GET /orders/{id}.
  1. Subscribe

    One subscription per topic. The response contains the signing secret; store it with the subscription id.

    bash
    for topic in order.status_changed order.shipped design.publish_requested product.stock_changed product.updated; do
      curl -s -X POST "$CP_BASE/webhooks" \
        -H "Authorization: Bearer $CP_KEY" -H "Content-Type: application/json" \
        -d '{"topic":"'"$topic"'","target_url":"https://my-store-01.pk/webhooks/ceeprinto"}'
    done
    # โ†’ 201 { "data": { "id": 9, "topic": "order.status_changed", "secret": "whsec_Xk2p9QvT4mLr7sWn1bYc8dHf3gJa6eZu", "status": "active", ... } }

    GET /webhooks lists your subscriptions, each with its secret (treat the response as sensitive). DELETE /webhooks/{id} removes one (204).

  2. Verify the signature

    Compute the HMAC over the raw request bytes, before any JSON parsing. The hub's JSON escapes slashes (https:\/\/), so re-serialising the parsed body gives a different string and a false mismatch. Compare in constant time and reject timestamps more than 5 minutes from now (replay protection).

    Verify X-CP-Signature
    curl
    #!/usr/bin/env bash
    # verify.sh SIGNATURE_HEADER BODY_FILE   (secret in CP_WEBHOOK_SECRET)
    sig_header="$1"; body_file="$2"
    t=$(printf '%s' "$sig_header" | sed -n 's/.*t=\([0-9]*\).*/\1/p')
    v1=$(printf '%s' "$sig_header" | sed -n 's/.*v1=\([0-9a-f]*\).*/\1/p')
    expected=$( { printf '%s.' "$t"; cat "$body_file"; } | openssl dgst -sha256 -hmac "$CP_WEBHOOK_SECRET" | awk '{print $NF}')
    now=$(date +%s)
    if [ "$expected" != "$v1" ]; then echo "bad signature"; exit 1; fi
    if [ $(( now > t ? now - t : t - now )) -gt 300 ]; then echo "stale timestamp"; exit 1; fi
    echo "ok"

    Complete runnable servers are in Recipes.

  3. Answer fast, work later

    Return 2xx as soon as the signature checks out, then process asynchronously (a queue, a background job). Slow handlers hit the 10-second timeout and are retried, which causes duplicates.

  4. Be idempotent

    Retries and the reduced duplicate below mean the same event can arrive more than once. De-duplicate on the natural key: wc_order_id + status for orders, job_id / item_id for publishes, product_id + variation_id + stock_status for stock.

  5. Keep polling

    After 5 failed attempts an event is dropped. Poll GET /shops/{id}/publish-jobs/pending every 30 to 60 seconds, and re-read GET /orders/{id} for open orders, so nothing is lost.

Quirks to handle

Situation What you receive
Order completes with a courier booking order.shipped (full payload with tracking) and a reduced order.status_changed with only wc_order_id and status
Publish job created from the Get Started tab design.publish_requested without mode, product_id_hub, variants or per-item mode; treat as create
Stock change on a blank you have not listed Nothing: product.stock_changed goes only to merchants with a listing for that product or variation
Subscription deleted or inactive Queued retries stop

API ยท Guides

Payouts and shipping quotes

Read your payout history and request shipping quotes before placing an order.

Read the same COD payout ledger the merchant sees in My Account โ†’ Pay Out, and quote shipping for a city before you take an order.

Prerequisites

All three routes need orders:read. Connection-code keys ("Shopify App", "WooCommerce Connect") do not have it and get 403. Create a separate key with orders:read on the API Keys tab for reporting.

Routes

Route Returns Paginated
GET /payouts One row per COD order: amount, bucket, hold end, tracking Yes (page, per_page max 100)
GET /payouts/summary Totals and counts per bucket (the five My Account cards) No; cached 15 minutes (meta.cached)
POST /shipping/quote Flat rate for a city in PKR No
  1. List payouts still owed

    status takes all (default), pending (everything not yet paid out) or one bucket. date_from / date_to (YYYY-MM-DD, inclusive) filter on order creation date.

    GET /payouts
    curl
    curl -s "$CP_BASE/payouts?status=pending&date_from=2026-09-01&per_page=100" \
      -H "Authorization: Bearer $CP_KEY"
  2. Show the totals

    bash
    curl -s "$CP_BASE/payouts/summary" -H "Authorization: Bearer $CP_KEY"
    Response 200
    {
      "data": {
        "total": 184500,
        "processed": 120000,
        "pending": 64500,
        "ready": 22000,
        "in_hold": 30500,
        "awaiting_delivery": 12000,
        "missing_bank": 0,
        "count_total": 41,
        "count_processed": 27,
        "count_pending": 14,
        "count_ready": 5,
        "count_in_hold": 6,
        "count_awaiting_delivery": 3,
        "count_missing_bank": 0,
        "currency": "PKR"
      },
      "meta": {
        "cached": false
      }
    }

    The summary is always the whole ledger (filters are ignored) and is cached per merchant for 15 minutes; the cache is cleared when an order changes status or a payout is processed.

  3. Quote shipping

    bash
    curl -s -X POST "$CP_BASE/shipping/quote" \
      -H "Authorization: Bearer $CP_KEY" -H "Content-Type: application/json" \
      -d '{"city":"Karachi"}'
    # โ†’ 200 { "data": { "city": "Karachi", "total": 200, "currency": "PKR", "method_id": "flat_rate", "method_title": "Flat Rate" } }

    Send city or a shipping_address object (only its city is read). Missing city is 400. The same rate is added as the shipping line when POST /orders creates the WooCommerce order.

    Defaults; CeePrinto can change the table
    City (case-insensitive) Default rate
    Karachi, KHI 200 PKR
    Anywhere else 250 PKR

Payout buckets

payout_status Label Meaning
processed Paid out Paid; processed_at set
missing_bank Missing bank details No bank code or account number on file (checked before delivery state)
awaiting_delivery Awaiting delivery Not yet delivered
in_hold 7-day hold Delivered; hold ends at release_at
ready Ready for payout Due in the next payout
Row field Rule
amount Owed to the merchant: sum of line customer_price plus additional_cod_from_customer
payout_status_label Display as given; do not derive your own
release_at, days_until_ready null until delivery is confirmed
payment_url Non-null only when needs_payment is true. Show a "Pay now" action only then.

API ยท Guides

Recipes: full scripts

Complete scripts in curl, PHP, Node and Python for publishing, webhooks, orders and catalog sync.

Four complete, runnable scripts in bash, PHP, Node and Python that you can copy, set environment variables for, and run as the starting point of a real integration.

Recipe Env variables Scopes
Publish and poll CP_BASE, CP_KEY, CP_SHOP_ID, CP_DESIGN_ID listings:write, listings:read, designs:read, products:read
Verify a webhook CP_WEBHOOK_SECRET (whsec_โ€ฆ) None (receiver)
Submit an order with idempotent retry CP_BASE, CP_KEY, CP_SHOP_ID, CP_EXTERNAL_SHOP_ID orders:write (+ orders:read for the status check)
Sync catalog and stock CP_BASE, CP_KEY products:read

Runtimes: bash scripts need curl, jq and openssl; PHP 7.4+ with ext-curl (CLI); Node 18+ (save as .mjs); Python 3.8+ with pip install requests.

Publish and poll

Creates a publish job for one design, then works the pending queue for that job: waits for store-ready mockups, creates the store product (a stub you replace), registers one listing per variation, and closes each item with complete, or fail on any error.

Publish and poll
curl
#!/usr/bin/env bash
# publish-and-poll.sh   Requires curl, jq.
# Env: CP_BASE, CP_KEY, CP_SHOP_ID, CP_DESIGN_ID
set -euo pipefail
: "${CP_BASE:?}" "${CP_KEY:?}" "${CP_SHOP_ID:?}" "${CP_DESIGN_ID:?}"

# api METHOD PATH [JSON]: prints the body; returns 1 on HTTP >= 400
api() {
  local method="$1" path="$2" body="${3:-}" out status
  out=$(mktemp)
  local args=(-s -o "$out" -w "%{http_code}" -X "$method" "$CP_BASE$path" -H "Authorization: Bearer $CP_KEY")
  [ -n "$body" ] && args+=(-H "Content-Type: application/json" -d "$body")
  status=$(curl "${args[@]}")
  if [ "$status" -ge 400 ]; then
    echo "HTTP $status $method $path: $(cat "$out")" >&2
    rm -f "$out"
    return 1
  fi
  cat "$out"
  rm -f "$out"
}

# Replace with your store's API. Must print the store product id.
create_store_product() {
  local design_id="$1" images_tsv="$2"
  echo "Would create a product for design $design_id with $(wc -l < "$images_tsv") images" >&2
  echo "demo-$design_id-$(date +%s)"
}

process_item() {
  local item="$1" item_id design_id product_id mode ext m status i
  item_id=$(jq -r '.id' <<<"$item")
  design_id=$(jq -r '.design_id' <<<"$item")
  product_id=$(jq -r '.product_id' <<<"$item")
  mode=$(jq -r '.mode' <<<"$item")

  # Mockups render on request; wait for store_status ready or partial.
  for i in $(seq 1 24); do
    m=$(api GET "/designs/$design_id/variation-mockups") || return 1
    status=$(jq -r '.data.store_status' <<<"$m")
    case "$status" in
      ready|partial) break ;;
      unavailable) echo "Mockup renderer unavailable" >&2; return 1 ;;
    esac
    [ "$i" -eq 24 ] && { echo "Mockups not ready after 2 minutes" >&2; return 1; }
    sleep 5
  done

  # One row per variation x stage. Key by variation_id + stage_index; 0 is the front.
  local images
  images=$(mktemp)
  jq -r '.data.items[] | select(.failed | not) | [.variation_id, .stage_index, .image_url] | @tsv' <<<"$m" > "$images"

  if [ "$mode" = "create" ]; then
    ext=$(create_store_product "$design_id" "$images") || return 1
  else
    ext=$(jq -r '.external_product_id' <<<"$item")
  fi
  rm -f "$images"

  local catalog listings
  catalog=$(api GET "/catalog/products/$product_id") || return 1
  listings=$(jq -c --arg ext "$ext" --argjson d "$design_id" --argjson p "$product_id" '
    {listings: (if (.data.variations | length) == 0
      then [{design_id: $d, product_id: $p, variation_id: 0, external_product_id: $ext, external_variant_id: $ext}]
      else [.data.variations[] | {design_id: $d, product_id: $p, variation_id: .id,
            external_product_id: $ext, external_variant_id: ($ext + "-" + (.id | tostring))}]
      end)}' <<<"$catalog")
  api POST "/shops/$CP_SHOP_ID/listings/bulk" "$listings" | jq -e '.meta.failed == 0' >/dev/null || return 1

  api POST "/publish-jobs/items/$item_id/complete" "$(jq -nc --arg e "$ext" '{external_product_id: $e}')" >/dev/null
  echo "Item $item_id complete -> store product $ext"
}

job=$(api POST /publish-jobs "$(jq -nc --argjson s "$CP_SHOP_ID" --argjson d "$CP_DESIGN_ID" \
  '{shop_id: $s, design_ids: [$d], mode: "create", price_mode: "blank"}')")
job_id=$(jq -r '.data.id' <<<"$job")
echo "Created publish job $job_id"

for round in $(seq 1 60); do
  pending=$(api GET "/shops/$CP_SHOP_ID/publish-jobs/pending" | jq -c --argjson j "$job_id" '[.data[] | select(.job_id == $j)]')
  if [ "$(jq length <<<"$pending")" -eq 0 ]; then
    echo "Job $job_id has no pending items. Done."
    exit 0
  fi
  while read -r item; do
    item_id=$(jq -r '.id' <<<"$item")
    if ! process_item "$item"; then
      api POST "/publish-jobs/items/$item_id/fail" '{"error":"Connector could not publish this design. See connector logs."}' >/dev/null || true
      echo "Item $item_id failed" >&2
    fi
  done < <(jq -c '.[]' <<<"$pending")
  sleep 5
done
echo "Gave up waiting on job $job_id" >&2
exit 1

Verify a webhook

A minimal HTTP receiver: reads the raw body, checks X-CP-Signature with a constant-time compare and a 5-minute tolerance, answers 200 immediately and then handles the event. The bash tab is a test sender that signs a sample event and posts it to your receiver.

Verify a webhook
curl
#!/usr/bin/env bash
# send-test-webhook.sh   Signs a sample event and POSTs it to your receiver.
# Env: CP_WEBHOOK_SECRET. Optional: TARGET (default https://your-store.example/webhooks/ceeprinto)
set -euo pipefail
: "${CP_WEBHOOK_SECRET:?}"
TARGET="${TARGET:-https://your-store.example/webhooks/ceeprinto}"

now=$(date +%s)
sent_at=$(date -u +%Y-%m-%dT%H:%M:%S+00:00)
body='{"topic":"order.status_changed","data":{"wc_order_id":50231,"status":"processing","previous_status":"pending","external_order_id":"1001"},"sent_at":"'"$sent_at"'"}'
sig=$(printf '%s.%s' "$now" "$body" | openssl dgst -sha256 -hmac "$CP_WEBHOOK_SECRET" | awk '{print $NF}')

echo "Valid signature:"
curl -s -w " (HTTP %{http_code})\n" -X POST "$TARGET" \
  -H "Content-Type: application/json" \
  -H "X-CP-Topic: order.status_changed" \
  -H "X-CP-Signature: t=$now,v1=$sig" \
  --data-binary "$body"

echo "Tampered body (must be rejected):"
curl -s -w " (HTTP %{http_code})\n" -X POST "$TARGET" \
  -H "Content-Type: application/json" \
  -H "X-CP-Topic: order.status_changed" \
  -H "X-CP-Signature: t=$now,v1=$sig" \
  --data-binary "${body/processing/cancelled}"

Submit an order with idempotent retry

Sends one order with an Idempotency-Key derived from your shop and order number, retries timeouts, network errors, 429 and 5xx with the same key, stops on 4xx, recognises a replay, then follows the intake until it is created or failed. Pass your order number as the first argument.

Submit an order with idempotent retry
curl
#!/usr/bin/env bash
# submit-order.sh ORDER_NUMBER   Requires curl, jq.
# Env: CP_BASE, CP_KEY, CP_SHOP_ID, CP_EXTERNAL_SHOP_ID
set -euo pipefail
: "${CP_BASE:?}" "${CP_KEY:?}" "${CP_SHOP_ID:?}" "${CP_EXTERNAL_SHOP_ID:?}"
order_no="${1:?usage: $0 ORDER_NUMBER}"
idem="$CP_EXTERNAL_SHOP_ID:$order_no"   # same key on every retry

body=$(jq -nc --argjson shop "$CP_SHOP_ID" --arg no "$order_no" '{
  shop_id: $shop, external_order_id: $no, currency: "PKR",
  shipping_address: {first_name: "Bilal", last_name: "Ahmed", phone: "03001234567",
    address_1: "Flat 4B, Block 5, Clifton", city: "Karachi", country: "PK"},
  line_items: [{external_variant_id: "9001-1", quantity: 1, customer_price: "2500.00", customer_variant: "L / Black"}]
}')

out=$(mktemp); hdr=$(mktemp); trap 'rm -f "$out" "$hdr"' EXIT
delay=1
for attempt in 1 2 3 4 5; do
  status=$(curl -s --max-time 20 -o "$out" -D "$hdr" -w "%{http_code}" -X POST "$CP_BASE/orders" \
    -H "Authorization: Bearer $CP_KEY" -H "Content-Type: application/json" \
    -H "Idempotency-Key: $idem" -d "$body") || status=000
  case "$status" in
    202) echo "Accepted: $(jq -c '.data' "$out")"; break ;;
    200) echo "Already accepted earlier (replay): $(jq -c '.data' "$out")"; break ;;
    000|429|5??)
      wait=$(grep -i '^retry-after:' "$hdr" 2>/dev/null | tr -dc '0-9' || true)
      echo "Attempt $attempt got $status; retrying with the same key" >&2
      sleep "${wait:-$delay}"; delay=$((delay * 2)) ;;
    *) echo "Rejected ($status): $(cat "$out")" >&2; exit 1 ;;
  esac
  [ "$attempt" -eq 5 ] && { echo "Gave up; safe to rerun later with the same order number" >&2; exit 1; }
done

intake_id=$(jq -r '.data.id' "$out")
for i in $(seq 1 30); do
  status=$(curl -s -o "$out" -w "%{http_code}" "$CP_BASE/orders/$intake_id" -H "Authorization: Bearer $CP_KEY")
  [ "$status" = "403" ] && { echo "Key lacks orders:read; check My Account -> Store Connections -> Order Activity"; exit 0; }
  s=$(jq -r '.data.status' "$out")
  case "$s" in
    created) echo "WooCommerce order $(jq -r '.data.wc_order_id' "$out") created (Pending payment)"; exit 0 ;;
    failed)  echo "Intake failed: $(jq -r '.data.last_error' "$out")" >&2; exit 1 ;;
  esac
  sleep 2
done
echo "Still $s after 60 s; check again later" >&2

Sync catalog and stock

Walks GET /catalog/products 100 at a time by following the Link header's rel="next", fetches each variable blank for per-variation stock, honours 429 Retry-After, and writes catalog.json with one row per sellable variant. Subscribe to product.stock_changed afterwards to stay in sync without re-running it.

Sync catalog and stock
curl
#!/usr/bin/env bash
# sync-catalog.sh   Requires curl, jq. Env: CP_BASE, CP_KEY. Writes catalog.json
set -euo pipefail
: "${CP_BASE:?}" "${CP_KEY:?}"

hdr=$(mktemp); out=$(mktemp); rows=$(mktemp); trap 'rm -f "$hdr" "$out" "$rows"' EXIT

# get URL: fetch into $out/$hdr, retrying 429 after Retry-After
get() {
  local url="$1" status wait
  for attempt in 1 2 3 4 5; do
    status=$(curl -s -o "$out" -D "$hdr" -w "%{http_code}" "$url" -H "Authorization: Bearer $CP_KEY")
    [ "$status" = "200" ] && return 0
    if [ "$status" = "429" ] || [ "$status" -ge 500 ]; then
      wait=$(grep -i '^retry-after:' "$hdr" | tr -dc '0-9' || true)
      sleep "${wait:-$attempt}"; continue
    fi
    echo "HTTP $status for $url: $(cat "$out")" >&2; return 1
  done
  return 1
}

url="$CP_BASE/catalog/products?per_page=100"
while [ -n "$url" ]; do
  get "$url"
  # Save the next-page URL now; the detail requests below overwrite $hdr.
  next=$(grep -i '^link:' "$hdr" | tr ',' '\n' | grep 'rel="next"' | sed -E 's/.*<([^>]+)>.*/\1/' || true)
  jq -c '.data[]' "$out" > "$rows.page"
  while read -r p; do
    id=$(jq -r '.id' <<<"$p")
    if [ "$(jq -r '.has_variations' <<<"$p")" = "true" ]; then
      get "$CP_BASE/catalog/products/$id"
      jq -c '.data as $p | $p.variations[] | {product_id: $p.id, variation_id: .id, title: .title, sku, stock_status, stock_quantity, in_stock}' "$out" >> "$rows"
    else
      jq -c '{product_id: .id, variation_id: 0, title, sku, stock_status, stock_quantity, in_stock}' <<<"$p" >> "$rows"
    fi
  done < "$rows.page"
  rm -f "$rows.page"
  url="$next"
done

jq -s '.' "$rows" > catalog.json
echo "Wrote $(jq length catalog.json) variants to catalog.json"

API ยท Reference

Endpoint reference

Every ceeprinto/v2 route with its scope, parameters, request, response and notes.

Every ceeprinto/v2 route with its scope, parameters, request body, full response and runnable examples.

Base URL https://ceeprinto.com/wp-json/ceeprinto/v2. Every request sends Authorization: Bearer $CP_KEY. Examples read CP_BASE and CP_KEY from the environment; Node examples use top-level await, so save them as .mjs (Node 18+). The machine-readable spec is openapi.yaml.

Routes per group
Group Routes Scopes
Me 1 none
Catalog 2 products:read
Products 5 listings:read, listings:write
Designs 4 designs:read
Shops 4 listings:read, shops:write
Listings 6 listings:read, listings:write
Publish jobs 5 listings:write, listings:read
Orders 4 orders:read, orders:write
Payouts 2 orders:read
Shipping 1 orders:read
Webhooks 3 webhooks:write

Me

The account and API key behind the credential.

GET /me

  • Scope: none
  • Status: 200

The account behind the key.

Response 200
{
  "data": {
    "id": 214,
    "email": "[email protected]",
    "name": "Ayesha Khan",
    "first_name": "Ayesha",
    "last_name": "Khan",
    "company": "Threadline Karachi",
    "api_key": {
      "key_id": "9f3a1c7eK2pQ8mZx4LbN6tRw",
      "name": "WooCommerce Connect",
      "is_legacy": false,
      "last_used_at": "2026-09-29 10:14:52"
    },
    "scopes": [
      "designs:read",
      "products:read",
      "listings:read",
      "listings:write",
      "orders:write",
      "shops:write",
      "webhooks:write"
    ],
    "fulfillment_profile": {
      "complete": true,
      "missing": [],
      "fields": {
        "billing_phone": true,
        "billing_address_1": true,
        "billing_city": true,
        "billing_country": true,
        "billing_company": true,
        "billing_address_2": false,
        "billing_state": true,
        "billing_postcode": true,
        "first_name": true,
        "last_name": true
      }
    }
  }
}
Example
curl
curl -s "$CP_BASE/me" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • Any valid key can call it; no scope is required.
  • Store data.id as the merchant id. It is the numeric WordPress user id and never changes.
  • api_key.key_id is the public 24-character id (the part between cp_live_ and the dot). For a legacy key it is masked except for the last 4 characters.
  • fulfillment_profile.complete is false until billing phone, address line 1, city and country are set in My Account. Orders still intake, but fulfilment needs them.
  • Error statuses: 401, 404, 429. See Errors.

Catalog

Blank WooCommerce products that designs are printed on, with stock.

GET /catalog/products

  • Scope: products:read
  • Status: 200

Blank products designs are built on.

Parameters

Name In Type Required Description
search query string No Free-text search on product title.
page query integer No 1-based page number.
per_page query integer No Items per page. Values below 1 fall back to 20; values above 100 are clamped to 100.
Response 200
{
  "data": [
    {
      "id": 37,
      "title": "Classic Crew Tee",
      "type": "variable",
      "has_variations": true,
      "sku": "CP-TEE",
      "price": "1200",
      "image_url": "https://ceeprinto.com/wp-content/uploads/2026/05/classic-tee.jpg",
      "stock_status": "instock",
      "stock_quantity": null,
      "in_stock": true
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 20,
    "total": 1,
    "total_pages": 1
  }
}
Example
curl
curl -s "$CP_BASE/catalog/products?search=tee&per_page=50" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • Published WooCommerce products ordered by title. These are blanks, not the merchant's own products.
  • Paginated: read meta.total_pages or follow the Link header.
  • stock_quantity is null when WooCommerce is not managing stock; use in_stock.
  • Error statuses: 401, 403, 429. See Errors.

GET /catalog/products/{id}

  • Scope: products:read
  • Status: 200

One blank with attributes, variations, stock and print config.

Parameters

Name In Type Required Description
id path integer Yes Catalog (blank) product id.
Response 200
{
  "data": {
    "id": 37,
    "title": "Classic Crew Tee",
    "type": "variable",
    "has_variations": true,
    "sku": "CP-TEE",
    "price": "1200",
    "image_url": "https://ceeprinto.com/wp-content/uploads/2026/05/classic-tee.jpg",
    "stock_status": "instock",
    "stock_quantity": null,
    "in_stock": true,
    "description": "180 GSM combed cotton crew neck.",
    "attributes": [
      {
        "name": "Size",
        "slug": "pa_size",
        "options": [
          "S",
          "M",
          "L"
        ]
      },
      {
        "name": "Color",
        "slug": "pa_color",
        "options": [
          "Black",
          "White",
          "Navy",
          "Maroon"
        ]
      }
    ],
    "variations": [
      {
        "id": 101,
        "title": "Classic Crew Tee - L, Black",
        "attributes": {
          "attribute_pa_size": "l",
          "attribute_pa_color": "black"
        },
        "sku": "CP-TEE-L-BLK",
        "price": "1200",
        "stock_status": "instock",
        "stock_quantity": 40,
        "in_stock": true
      }
    ],
    "print_config": {
      "stages": [
        {
          "id": 1,
          "index": 0,
          "name": "Front"
        },
        {
          "id": 2,
          "index": 1,
          "name": "Back"
        }
      ]
    }
  }
}
Example
curl
curl -s "$CP_BASE/catalog/products/37" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • print_config.stages[].index matches stage_index in the mockup matrix. Index 0 is the front.
  • Subscribe to product.stock_changed to keep stock in sync instead of polling this.
  • Error statuses: 401, 403, 404, 429. See Errors.

Products

Hub products: a blank plus a default design and per-variant overrides, publishable to any connected store.

GET /products

  • Scope: listings:read
  • Status: 200

The merchant's hub products.

Parameters

Name In Type Required Description
page query integer No 1-based page number.
per_page query integer No Items per page. Values below 1 fall back to 20; values above 100 are clamped to 100.
Response 200
{
  "data": [
    {
      "id": 42,
      "user_id": 214,
      "blank_product_id": 37,
      "title": "Karachi Skyline Tee",
      "description": "",
      "price_mode": "blank",
      "flat_price": null,
      "default_design_id": 11,
      "status": "active",
      "created_at": "2026-09-20T09:12:00+00:00",
      "updated_at": "2026-09-28T16:40:05+00:00",
      "variants": [
        {
          "id": 501,
          "product_id": 42,
          "variation_id": 101,
          "design_id": null,
          "price": null,
          "sku": null,
          "enabled": true,
          "effective_design_id": 11,
          "inherits_default": true
        },
        {
          "id": 502,
          "product_id": 42,
          "variation_id": 102,
          "design_id": 12,
          "price": 2800,
          "sku": "TEE-BLK-L",
          "enabled": true,
          "effective_design_id": 12,
          "inherits_default": false
        }
      ]
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 20,
    "total": 1,
    "total_pages": 1
  }
}
Example
curl
curl -s "$CP_BASE/products" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • Hub products (a blank plus a default design and per-variant overrides), not the blank catalog. Newest first.
  • per_page is honoured since 2.2.0.
  • Error statuses: 401, 403, 429. See Errors.

POST /products

  • Scope: listings:write
  • Status: 201

Create a hub product from a blank.

Parameters

Name In Type Required Description
blank_product_id body integer Yes
title body string No Defaults to the blank's name.
description body string No Product description.
price_mode body string No One of blank, flat. Default blank.
flat_price body number, nullable No
default_design_id body integer No
status body string No Status label. One of draft, active, archived. Default draft.
Request body
{
  "blank_product_id": 37,
  "title": "Karachi Skyline Tee",
  "default_design_id": 11,
  "price_mode": "flat",
  "flat_price": 2500
}
Response 201
{
  "data": {
    "id": 42,
    "user_id": 214,
    "blank_product_id": 37,
    "title": "Karachi Skyline Tee",
    "description": "",
    "price_mode": "flat",
    "flat_price": 2500,
    "default_design_id": 11,
    "status": "draft",
    "created_at": "2026-09-29T10:15:00+00:00",
    "updated_at": "2026-09-29T10:15:00+00:00",
    "variants": [
      {
        "id": 501,
        "product_id": 42,
        "variation_id": 101,
        "design_id": null,
        "price": null,
        "sku": null,
        "enabled": true,
        "effective_design_id": 11,
        "inherits_default": true
      },
      {
        "id": 502,
        "product_id": 42,
        "variation_id": 102,
        "design_id": null,
        "price": null,
        "sku": null,
        "enabled": true,
        "effective_design_id": 11,
        "inherits_default": true
      }
    ]
  }
}
Example
curl
curl -s -X POST "$CP_BASE/products" \
  -H "Authorization: Bearer $CP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"blank_product_id":37,"title":"Karachi Skyline Tee","default_design_id":11,"price_mode":"flat","flat_price":2500}'

Notes

  • Seeds one variant per blank variation with design_id: null (inherit the default). A simple blank gets one variant with variation_id: 0.
  • An unknown or missing blank_product_id is 400 invalid_request, not 422.
  • Error statuses: 400, 401, 403, 429, 500. See Errors.

GET /products/{id}

  • Scope: listings:read
  • Status: 200

One hub product's full composition.

Parameters

Name In Type Required Description
id path integer Yes Hub product id.
Response 200
{
  "data": {
    "id": 42,
    "user_id": 214,
    "blank_product_id": 37,
    "title": "Karachi Skyline Tee",
    "description": "",
    "price_mode": "blank",
    "flat_price": null,
    "default_design_id": 11,
    "status": "active",
    "created_at": "2026-09-20T09:12:00+00:00",
    "updated_at": "2026-09-28T16:40:05+00:00",
    "variants": [
      {
        "id": 501,
        "product_id": 42,
        "variation_id": 101,
        "design_id": null,
        "price": null,
        "sku": null,
        "enabled": true,
        "effective_design_id": 11,
        "inherits_default": true
      },
      {
        "id": 502,
        "product_id": 42,
        "variation_id": 102,
        "design_id": 12,
        "price": 2800,
        "sku": "TEE-BLK-L",
        "enabled": true,
        "effective_design_id": 12,
        "inherits_default": false
      }
    ]
  }
}
Example
curl
curl -s "$CP_BASE/products/42" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • Pending publish items carry only product_id_hub; fetch the per-variant designs here.
  • Use effective_design_id, not design_id: design_id: null means the variant inherits default_design_id.
  • Error statuses: 401, 403, 404, 429. See Errors.

PATCH /products/{id}

  • Scope: listings:write
  • Status: 200

Update product-level fields.

Parameters

Name In Type Required Description
id path integer Yes Hub product id.
title body string No Product title.
description body string No Product description.
price_mode body string No One of blank, flat.
flat_price body number, nullable No
default_design_id body integer No
status body string No Status label. One of draft, active, archived.
Request body
{
  "default_design_id": 12,
  "status": "active"
}
Response 200
{
  "data": {
    "id": 42,
    "user_id": 214,
    "blank_product_id": 37,
    "title": "Karachi Skyline Tee",
    "description": "",
    "price_mode": "blank",
    "flat_price": null,
    "default_design_id": 12,
    "status": "active",
    "created_at": "2026-09-20T09:12:00+00:00",
    "updated_at": "2026-09-29T10:15:00+00:00",
    "variants": [
      {
        "id": 501,
        "product_id": 42,
        "variation_id": 101,
        "design_id": null,
        "price": null,
        "sku": null,
        "enabled": true,
        "effective_design_id": 12,
        "inherits_default": true
      }
    ]
  }
}
Example
curl
curl -s -X PATCH "$CP_BASE/products/42" \
  -H "Authorization: Bearer $CP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"default_design_id":12,"status":"active"}'

Notes

  • Changing default_design_id re-flows to every variant with no override of its own.
  • If the product is already published, the change is projected onto its listings in every store and product.updated fires.
  • Error statuses: 401, 403, 404, 429. See Errors.

PATCH /products/{id}/variants/{variation_id}

  • Scope: listings:write
  • Status: 200

Override (or restore) one variant's design, price, sku or enabled flag.

Parameters

Name In Type Required Description
id path integer Yes Hub product id.
variation_id path integer Yes Blank variation id (0 for a simple blank).
design_id body integer No 0 = inherit the product default.
price body number, nullable No
sku body string No
enabled body boolean No
Request body
{
  "design_id": 12,
  "price": 2800,
  "sku": "TEE-BLK-L"
}
Response 200
{
  "data": {
    "id": 502,
    "product_id": 42,
    "variation_id": 102,
    "design_id": 12,
    "price": 2800,
    "sku": "TEE-BLK-L",
    "enabled": true,
    "effective_design_id": 12,
    "inherits_default": false
  }
}
Example
curl
curl -s -X PATCH "$CP_BASE/products/42/variants/102" \
  -H "Authorization: Bearer $CP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"design_id":12,"price":2800,"sku":"TEE-BLK-L"}'

Notes

  • Send design_id: 0 to clear an override and inherit the product default again.
  • Projects onto published stores and fires product.updated, like PATCH /products/{id}.
  • Error statuses: 401, 403, 404, 429. See Errors.

Designs

The merchant's saved designs and their per-variation, per-side mockups.

GET /designs

  • Scope: designs:read
  • Status: 200

The merchant's designs.

Parameters

Name In Type Required Description
status query string No Filter by design status, e.g. saved.
product_id query integer No Filter by blank product id.
page query integer No 1-based page number.
per_page query integer No Items per page. Values below 1 fall back to 20; values above 100 are clamped to 100.
Response 200
{
  "data": [
    {
      "id": 11,
      "name": "Karachi Skyline",
      "status": "saved",
      "product_id": 37,
      "variation_id": 101,
      "preview_url": "https://ceeprinto.com/wp-content/uploads/ceeprinto-designs/previews/design-11.png",
      "total_price": 1200,
      "order_id": null,
      "created_at": "2026-09-18T11:02:44+00:00",
      "updated_at": "2026-09-18T11:05:10+00:00"
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 20,
    "total": 1,
    "total_pages": 1
  }
}
Example
curl
curl -s "$CP_BASE/designs?product_id=37&per_page=20" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • product_id is the blank the design was made on.
  • Error statuses: 401, 403, 429. See Errors.

GET /designs/{id}

  • Scope: designs:read
  • Status: 200

One design owned by the key's account.

Parameters

Name In Type Required Description
id path integer Yes Design id.
Response 200
{
  "data": {
    "id": 11,
    "name": "Karachi Skyline",
    "status": "saved",
    "product_id": 37,
    "variation_id": 101,
    "preview_url": "https://ceeprinto.com/wp-content/uploads/ceeprinto-designs/previews/design-11.png",
    "total_price": 1200,
    "order_id": null,
    "created_at": "2026-09-18T11:02:44+00:00",
    "updated_at": "2026-09-18T11:05:10+00:00",
    "hires_png_url": "https://ceeprinto.com/wp-content/uploads/ceeprinto-designs/hires/design-11.png",
    "pdf_url": null,
    "config_id": 6,
    "base_product": {
      "id": 37,
      "title": "Classic Crew Tee",
      "sku": "CP-TEE",
      "price": "1200"
    },
    "design_data": {
      "stages": [
        {
          "index": 0,
          "name": "Front",
          "objects": []
        }
      ]
    }
  }
}
Example
curl
curl -s "$CP_BASE/designs/11" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • Another account's design is 404, never 403.
  • hires_png_url is the print file; preview_url is a small preview.
  • Error statuses: 401, 403, 404, 429. See Errors.

GET /designs/{id}/variation-mockups

  • Scope: designs:read
  • Status: 200

Per-variation, per-side mockup matrix and its render status.

Parameters

Name In Type Required Description
id path integer Yes Design id.
ensure_store query integer No Accepted for compatibility. Mockups now render on request, so this changes nothing.
batch query integer No Echoed back as batch (clamped 1 to 25). Nothing is queued.
Response 200
{
  "data": {
    "status": "ready",
    "total": 24,
    "ready": 24,
    "pending": 0,
    "failed": 0,
    "store_status": "ready",
    "store_ready": 24,
    "store_failed": 0,
    "store_pending": 0,
    "batch": 5,
    "queued": 0,
    "items": [
      {
        "variation_id": 101,
        "stage_index": 0,
        "stage_id": 1,
        "stage_name": "Front",
        "ready": true,
        "store_ready": true,
        "failed": false,
        "error": null,
        "thumb_url": "https://ceeprinto.com/wp-json/ceeprinto/v1/mockup/11/3fa9c2e1b7d4.jpg",
        "image_url": "https://ceeprinto.com/wp-json/ceeprinto/v1/mockup/11/8be0d41c92aa.jpg"
      },
      {
        "variation_id": 101,
        "stage_index": 1,
        "stage_id": 2,
        "stage_name": "Back",
        "ready": true,
        "store_ready": true,
        "failed": false,
        "error": null,
        "thumb_url": "https://ceeprinto.com/wp-json/ceeprinto/v1/mockup/11/c71f03d5e2b9.jpg",
        "image_url": "https://ceeprinto.com/wp-json/ceeprinto/v1/mockup/11/0d4e6a91f3c7.jpg"
      }
    ]
  }
}
Example
curl
curl -s "$CP_BASE/designs/11/variation-mockups" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • One cell per blank variation per stage: key cells by variation_id + stage_index. stage_index 0 is the front.
  • Cells render on request at the returned URLs; nothing is queued, so queued is always 0.
  • Wait for store_status: "ready" (or "partial", skipping cells with failed: true) before uploading.
  • Upload image_url (2048px) to stores. Never upload thumb_url (240px).
  • If the design configurator is inactive this answers 200 with status: "unavailable" and empty items.
  • Error statuses: 401, 403, 404, 429. See Errors.

POST /designs/{id}/variation-mockups

  • Scope: designs:read
  • Status: 202

Compatibility route: returns the mockup matrix with 202. Nothing is queued; cells render on request.

Parameters

Name In Type Required Description
id path integer Yes Design id.
ensure_store query integer No Accepted for compatibility. Mockups now render on request, so this changes nothing.
batch query integer No Echoed back as batch (clamped 1 to 25). Nothing is queued.
Response 202
{
  "data": {
    "status": "ready",
    "total": 24,
    "ready": 24,
    "pending": 0,
    "failed": 0,
    "store_status": "ready",
    "store_ready": 24,
    "store_failed": 0,
    "store_pending": 0,
    "batch": 5,
    "queued": 0,
    "items": [
      {
        "variation_id": 101,
        "stage_index": 0,
        "stage_id": 1,
        "stage_name": "Front",
        "ready": true,
        "store_ready": true,
        "failed": false,
        "error": null,
        "thumb_url": "https://ceeprinto.com/wp-json/ceeprinto/v1/mockup/11/3fa9c2e1b7d4.jpg",
        "image_url": "https://ceeprinto.com/wp-json/ceeprinto/v1/mockup/11/8be0d41c92aa.jpg"
      },
      {
        "variation_id": 101,
        "stage_index": 1,
        "stage_id": 2,
        "stage_name": "Back",
        "ready": true,
        "store_ready": true,
        "failed": false,
        "error": null,
        "thumb_url": "https://ceeprinto.com/wp-json/ceeprinto/v1/mockup/11/c71f03d5e2b9.jpg",
        "image_url": "https://ceeprinto.com/wp-json/ceeprinto/v1/mockup/11/0d4e6a91f3c7.jpg"
      }
    ]
  }
}
Example
curl
curl -s -X POST "$CP_BASE/designs/11/variation-mockups" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • Kept for compatibility. Returns 202 with the same envelope as the GET and queued: 0, because cells render on request.
  • Returns 500 server_error when the mockup renderer (design configurator) is not active.
  • You never need to call this before a GET.
  • Error statuses: 401, 403, 404, 429, 500. See Errors.

Shops

Connected storefronts (Shopify, WooCommerce, custom).

GET /shops

  • Scope: listings:read
  • Status: 200

Connected storefronts.

Parameters

Name In Type Required Description
page query integer No 1-based page number.
per_page query integer No Items per page. Values below 1 fall back to 20; values above 100 are clamped to 100.
Response 200
{
  "data": [
    {
      "id": 5,
      "channel": "custom",
      "external_shop_id": "my-store-01",
      "name": "My Store",
      "webhook_url": null,
      "status": "active",
      "connected_at": "2026-09-15T07:30:00+00:00"
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 20,
    "total": 1,
    "total_pages": 1
  }
}
Example
curl
curl -s "$CP_BASE/shops" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • Includes disconnected shops (status: "disconnected").
  • Error statuses: 401, 403, 429. See Errors.

POST /shops

  • Scope: shops:write
  • Status: 201

Register or refresh a storefront (idempotent).

Parameters

Name In Type Required Description
channel body string Yes Free-form label, 1 to 32 characters: custom, woocommerce, shopify. Max length 32.
external_shop_id body string Yes Your own stable id for the store. Upsert key together with channel.
name body string No Display name shown in My Account.
Request body
{
  "channel": "custom",
  "external_shop_id": "my-store-01",
  "name": "My Store"
}
Response 201
{
  "data": {
    "id": 5,
    "channel": "custom",
    "external_shop_id": "my-store-01",
    "name": "My Store",
    "webhook_url": null,
    "status": "active",
    "connected_at": "2026-09-29T10:15:00+00:00"
  }
}
Example
curl
curl -s -X POST "$CP_BASE/shops" \
  -H "Authorization: Bearer $CP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"channel":"custom","external_shop_id":"my-store-01","name":"My Store"}'

Notes

  • Upserts on (channel, external_shop_id): re-posting refreshes the row and reactivates a disconnected shop. Always 201.
  • 409 conflict when another CeePrinto account already owns that store.
  • Store the returned data.id; every other shop route uses it.
  • Error statuses: 400, 401, 403, 409, 429. See Errors.

GET /shops/{id}

  • Scope: listings:read
  • Status: 200

One connected storefront.

Parameters

Name In Type Required Description
id path integer Yes Shop id (from POST /shops).
Response 200
{
  "data": {
    "id": 5,
    "channel": "custom",
    "external_shop_id": "my-store-01",
    "name": "My Store",
    "webhook_url": null,
    "status": "active",
    "connected_at": "2026-09-15T07:30:00+00:00"
  }
}
Example
curl
curl -s "$CP_BASE/shops/5" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • Another account's shop is 404.
  • Error statuses: 401, 403, 404, 429. See Errors.

DELETE /shops/{id}

  • Scope: shops:write
  • Status: 200

Disconnect a storefront (soft).

Parameters

Name In Type Required Description
id path integer Yes Shop id (from POST /shops).
Response 200
{
  "data": {
    "id": 5,
    "channel": "custom",
    "external_shop_id": "my-store-01",
    "name": "My Store",
    "webhook_url": null,
    "status": "disconnected",
    "connected_at": "2026-09-15T07:30:00+00:00"
  }
}
Example
curl
curl -s -X DELETE "$CP_BASE/shops/5" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • Soft delete: answers 200 with the shop and status: "disconnected" (not 204).
  • Listings and order history are kept. POST /shops with the same external_shop_id reconnects.
  • Error statuses: 401, 403, 404, 429. See Errors.

Listings

Links between a design and a store product/variant, used to resolve incoming orders.

GET /shops/{id}/listings

  • Scope: listings:read
  • Status: 200

Listings on one shop.

Parameters

Name In Type Required Description
id path integer Yes Shop id.
page query integer No 1-based page number.
per_page query integer No Items per page. Values below 1 fall back to 20; values above 100 are clamped to 100.
Response 200
{
  "data": [
    {
      "id": 88,
      "shop_id": 5,
      "design_id": 11,
      "product_id": 37,
      "variation_id": 101,
      "external_product_id": "9001",
      "external_variant_id": "9001-1",
      "external_sku": "TEE-L",
      "product_id_hub": 42,
      "status": "active",
      "created_at": "2026-09-20T09:30:00+00:00",
      "updated_at": "2026-09-20T09:30:00+00:00"
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 20,
    "total": 1,
    "total_pages": 1
  }
}
Example
curl
curl -s "$CP_BASE/shops/5/listings" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • Listings are how an incoming order line is resolved back to a design.
  • Error statuses: 401, 403, 404, 429. See Errors.

POST /shops/{id}/listings

  • Scope: listings:write
  • Status: 201

Link a design to an external product/variant.

Parameters

Name In Type Required Description
id path integer Yes Shop id (from POST /shops).
design_id body integer No Design id.
product_id body integer No Catalog (blank) product id.
variation_id body integer No Blank variation id.
external_product_id body string No Your store's product id.
external_variant_id body string No Your store's variant id. Listings upsert on this value.
external_sku body string No Your store's SKU.
status body string No Status label.
Request body
{
  "design_id": 11,
  "product_id": 37,
  "variation_id": 101,
  "external_product_id": "9001",
  "external_variant_id": "9001-1",
  "external_sku": "TEE-L"
}
Response 201
{
  "data": {
    "id": 88,
    "shop_id": 5,
    "design_id": 11,
    "product_id": 37,
    "variation_id": 101,
    "external_product_id": "9001",
    "external_variant_id": "9001-1",
    "external_sku": "TEE-L",
    "product_id_hub": null,
    "status": "active",
    "created_at": "2026-09-29T10:15:00+00:00",
    "updated_at": "2026-09-29T10:15:00+00:00"
  }
}
Example
curl
curl -s -X POST "$CP_BASE/shops/5/listings" \
  -H "Authorization: Bearer $CP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"design_id":11,"product_id":37,"variation_id":101,"external_product_id":"9001","external_variant_id":"9001-1","external_sku":"TEE-L"}'

Notes

  • Needs at least one of external_variant_id, external_product_id, external_sku, and one of design_id, product_id (else 400).
  • Upserts on external_variant_id only. Re-posting a row without one creates a new listing every time.
  • Error statuses: 400, 401, 403, 404, 429, 500. See Errors.

DELETE /shops/{id}/listings

  • Scope: listings:write
  • Status: 204

Delete every listing for one external (store) product.

Parameters

Name In Type Required Description
id path integer Yes Shop id (from POST /shops).
external_product_id query string Yes The store product whose listings should all be removed.
Example
curl
curl -s -o /dev/null -w "%{http_code}\n" -X DELETE "$CP_BASE/shops/5/listings?external_product_id=9001" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • Call this when a store product is deleted or unlinked, so orders cannot resolve to it.
  • Missing external_product_id is 400.
  • Error statuses: 400, 401, 403, 404, 429. See Errors.

POST /shops/{id}/listings/bulk

  • Scope: listings:write
  • Status: 200

Create or update many listings (one per variant).

Parameters

Name In Type Required Description
id path integer Yes Shop id (from POST /shops).
listings body array of object Yes
listings[].design_id body integer No Design id.
listings[].product_id body integer No Catalog (blank) product id.
listings[].variation_id body integer No Blank variation id.
listings[].external_product_id body string No Your store's product id.
listings[].external_variant_id body string No Your store's variant id. Listings upsert on this value.
listings[].external_sku body string No Your store's SKU.
listings[].status body string No Status label.
Request body
{
  "listings": [
    {
      "design_id": 11,
      "product_id": 37,
      "variation_id": 101,
      "external_product_id": "9001",
      "external_variant_id": "9001-1"
    },
    {
      "design_id": 12,
      "product_id": 37,
      "variation_id": 102,
      "external_product_id": "9001",
      "external_variant_id": "9001-2"
    }
  ]
}
Response 200
{
  "data": [
    {
      "id": 88,
      "shop_id": 5,
      "design_id": 11,
      "product_id": 37,
      "variation_id": 101,
      "external_product_id": "9001",
      "external_variant_id": "9001-1",
      "external_sku": null,
      "product_id_hub": null,
      "status": "active",
      "created_at": "2026-09-29T10:15:00+00:00",
      "updated_at": "2026-09-29T10:15:00+00:00"
    },
    {
      "id": 89,
      "shop_id": 5,
      "design_id": 12,
      "product_id": 37,
      "variation_id": 102,
      "external_product_id": "9001",
      "external_variant_id": "9001-2",
      "external_sku": null,
      "product_id_hub": null,
      "status": "active",
      "created_at": "2026-09-29T10:15:00+00:00",
      "updated_at": "2026-09-29T10:15:00+00:00"
    }
  ],
  "meta": {
    "created": 2,
    "failed": 0,
    "errors": []
  }
}
Example
curl
curl -s -X POST "$CP_BASE/shops/5/listings/bulk" \
  -H "Authorization: Bearer $CP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"listings":[{"design_id":11,"product_id":37,"variation_id":101,"external_product_id":"9001","external_variant_id":"9001-1"},{"design_id":12,"product_id":37,"variation_id":102,"external_product_id":"9001","external_variant_id":"9001-2"}]}'

Notes

  • Rows are independent: answers 200 even if some fail. Check meta.failed and meta.errors[].index.
  • One row per store variant, each with its own design_id.
  • Error statuses: 400, 401, 403, 404, 429. See Errors.

PATCH /shops/{id}/listings/{listing_id}

  • Scope: listings:write
  • Status: 200

Override the design on one listing cell.

Parameters

Name In Type Required Description
id path integer Yes Shop id (from POST /shops).
listing_id path integer Yes Listing id.
design_id body integer Yes The design to print for this listing cell.
Request body
{
  "design_id": 12
}
Response 200
{
  "data": {
    "id": 89,
    "shop_id": 5,
    "design_id": 12,
    "product_id": 37,
    "variation_id": 102,
    "external_product_id": "9001",
    "external_variant_id": "9001-2",
    "external_sku": null,
    "product_id_hub": 42,
    "status": "active",
    "created_at": "2026-09-20T09:30:00+00:00",
    "updated_at": "2026-09-29T10:15:00+00:00"
  }
}
Example
curl
curl -s -X PATCH "$CP_BASE/shops/5/listings/89" \
  -H "Authorization: Bearer $CP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"design_id":12}'

Notes

  • Changes one cell only and notifies nothing. If you own a hub product, use PATCH /products/{id}/variants/{variation_id} instead so every store is updated and product.updated fires.
  • A design_id you do not own is 404.
  • Error statuses: 400, 401, 403, 404, 429. See Errors.

DELETE /shops/{id}/listings/{listing_id}

  • Scope: listings:write
  • Status: 204

Delete one listing.

Parameters

Name In Type Required Description
id path integer Yes Shop id (from POST /shops).
listing_id path integer Yes Listing id.
Example
curl
curl -s -o /dev/null -w "%{http_code}\n" -X DELETE "$CP_BASE/shops/5/listings/89" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • Answers 204 with an empty body.
  • Error statuses: 401, 403, 404, 429. See Errors.

Publish jobs

Hub-driven publishing. Connectors poll pending items and close them out.

POST /publish-jobs

  • Scope: listings:write
  • Status: 201

Create a hub publish job (one item per design).

Parameters

Name In Type Required Description
shop_id body integer Yes Shop id from POST /shops.
design_ids body array of integer No Designs to publish, one item each. Required unless product_id is sent.
product_id body integer No A hub product id, instead of design_ids. Designs and variants[] come from its composition.
mode body string No What the connector should do in the store. One of create, link, update. Default create.
external_product_id body string No Required when mode is "link" or "update".
price_mode body string No blank uses the blank's price; flat uses flat_price. One of blank, flat. Default blank.
flat_price body number, nullable No Used only when price_mode is flat.
Request body
{
  "shop_id": 5,
  "design_ids": [
    11
  ],
  "price_mode": "blank",
  "mode": "create"
}
Response 201
{
  "data": {
    "id": 3,
    "user_id": 214,
    "shop_id": 5,
    "status": "pending",
    "mode": "create",
    "price_mode": "blank",
    "flat_price": null,
    "created_at": "2026-09-29T10:15:00+00:00",
    "updated_at": "2026-09-29T10:15:00+00:00",
    "items": [
      {
        "id": 7,
        "job_id": 3,
        "design_id": 11,
        "product_id": 37,
        "variant_count": 12,
        "status": "pending",
        "mode": "create",
        "external_product_id": null,
        "product_id_hub": null,
        "error_text": null,
        "created_at": "2026-09-29T10:15:00+00:00",
        "updated_at": "2026-09-29T10:15:00+00:00",
        "completed_at": null
      }
    ]
  }
}
Example
curl
curl -s -X POST "$CP_BASE/publish-jobs" \
  -H "Authorization: Bearer $CP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"shop_id":5,"design_ids":[11],"price_mode":"blank","mode":"create"}'

Notes

  • Send design_ids or product_id (a hub product). Fires design.publish_requested.
  • Status order: unknown mode 422; shop not yours 404; product not yours 404; nothing to publish 400; link/update without external_product_id 422; every design already published 400.
  • In create mode, designs already listed on the shop are skipped.
  • The webhook is best-effort. Connectors must poll GET /shops/{id}/publish-jobs/pending.
  • Error statuses: 400, 401, 403, 404, 422, 429, 500. See Errors.

GET /publish-jobs/{id}

  • Scope: listings:read
  • Status: 200

One publish job with its items.

Parameters

Name In Type Required Description
id path integer Yes Publish job id.
Response 200
{
  "data": {
    "id": 3,
    "user_id": 214,
    "shop_id": 5,
    "status": "completed",
    "mode": "create",
    "price_mode": "blank",
    "flat_price": null,
    "created_at": "2026-09-29T10:15:00+00:00",
    "updated_at": "2026-09-29T10:18:40+00:00",
    "items": [
      {
        "id": 7,
        "job_id": 3,
        "design_id": 11,
        "product_id": 37,
        "variant_count": 12,
        "status": "completed",
        "mode": "create",
        "external_product_id": "9001",
        "product_id_hub": null,
        "error_text": null,
        "created_at": "2026-09-29T10:15:00+00:00",
        "updated_at": "2026-09-29T10:18:40+00:00",
        "completed_at": "2026-09-29T10:18:40+00:00"
      }
    ]
  }
}
Example
curl
curl -s "$CP_BASE/publish-jobs/3" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • Job status follows its items: pending, processing, completed, failed.
  • Error statuses: 401, 403, 404, 429. See Errors.

GET /shops/{id}/publish-jobs/pending

  • Scope: listings:read
  • Status: 200

Publish items the hub is still waiting on for this store.

Parameters

Name In Type Required Description
id path integer Yes Shop id (from POST /shops).
Response 200
{
  "data": [
    {
      "id": 7,
      "job_id": 3,
      "design_id": 11,
      "product_id": 37,
      "variant_count": 12,
      "status": "pending",
      "mode": "create",
      "external_product_id": null,
      "product_id_hub": 42,
      "error_text": null,
      "created_at": "2026-09-29T10:15:00+00:00",
      "updated_at": "2026-09-29T10:15:00+00:00",
      "completed_at": null,
      "shop_id": 5,
      "price_mode": "blank",
      "flat_price": null
    }
  ]
}
Example
curl
curl -s "$CP_BASE/shops/5/publish-jobs/pending" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • Not paginated: data is a bare array and there is no meta.
  • Connectors must poll this. Items carry mode, external_product_id, shop_id, price_mode and flat_price, so a polled item is handled exactly like a webhook one.
  • For hub-product items (product_id_hub set), read the per-variant designs from GET /products/{id}.
  • Close every item with complete or fail, or it stays pending forever.
  • Error statuses: 401, 403, 404, 429. See Errors.

POST /publish-jobs/items/{item_id}/complete

  • Scope: listings:write
  • Status: 200

Mark a publish item done.

Parameters

Name In Type Required Description
item_id path integer Yes Publish job item id.
external_product_id body string No The store product the item landed on.
Request body
{
  "external_product_id": "9001"
}
Response 200
{
  "data": {
    "id": 7,
    "job_id": 3,
    "design_id": 11,
    "product_id": 37,
    "variant_count": 12,
    "status": "completed",
    "mode": "create",
    "external_product_id": "9001",
    "product_id_hub": 42,
    "error_text": null,
    "created_at": "2026-09-29T10:15:00+00:00",
    "updated_at": "2026-09-29T10:18:40+00:00",
    "completed_at": "2026-09-29T10:18:40+00:00"
  }
}
Example
curl
curl -s -X POST "$CP_BASE/publish-jobs/items/7/complete" \
  -H "Authorization: Bearer $CP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"external_product_id":"9001"}'

Notes

  • Send the store product id the item landed on.
  • For hub-product items the matching listings get product_id_hub back-filled.
  • Error statuses: 401, 403, 404, 429. See Errors.

POST /publish-jobs/items/{item_id}/fail

  • Scope: listings:write
  • Status: 200

Mark a publish item failed.

Parameters

Name In Type Required Description
item_id path integer Yes Publish job item id.
error body string No Shown to the merchant. Defaults to "Publish failed."
Request body
{
  "error": "variant matrix rejected"
}
Response 200
{
  "data": {
    "id": 7,
    "job_id": 3,
    "design_id": 11,
    "product_id": 37,
    "variant_count": 12,
    "status": "failed",
    "mode": "create",
    "external_product_id": null,
    "product_id_hub": null,
    "error_text": "variant matrix rejected",
    "created_at": "2026-09-29T10:15:00+00:00",
    "updated_at": "2026-09-29T10:17:02+00:00",
    "completed_at": null
  }
}
Example
curl
curl -s -X POST "$CP_BASE/publish-jobs/items/7/fail" \
  -H "Authorization: Bearer $CP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"error":"variant matrix rejected"}'

Notes

  • error is shown to the merchant in My Account. Defaults to Publish failed.
  • Error statuses: 401, 403, 404, 429. See Errors.

Orders

Order intake (idempotent, queued) and the resulting WooCommerce order with COD fields.

GET /orders

  • Scope: orders:read
  • Status: 200

The merchant's order intakes.

Parameters

Name In Type Required Description
page query integer No 1-based page number.
per_page query integer No Items per page. Values below 1 fall back to 20; values above 100 are clamped to 100.
Response 200
{
  "data": [
    {
      "id": 1,
      "shop_id": 5,
      "external_order_id": "1001",
      "status": "created",
      "wc_order_id": 50231,
      "attempts": 1,
      "last_error": null,
      "received_at": "2026-09-29T10:15:00+00:00",
      "processed_at": "2026-09-29T10:15:04+00:00"
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 20,
    "total": 1,
    "total_pages": 1
  }
}
Example
curl
curl -s "$CP_BASE/orders?per_page=20" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • Requires orders:read. Connection-code keys ("Shopify App", "WooCommerce Connect") get 403.
  • Rows are intakes; wc_order_id is set once the WooCommerce order exists.
  • Error statuses: 401, 403, 429. See Errors.

POST /orders

  • Scope: orders:write
  • Status: 202 (200 on replay)

Submit an order (intake).

Parameters

Name In Type Required Description
Idempotency-Key header string Yes Required on POST /orders. Any unique string per order (e.g. <external_shop_id>:<external_order_id>). Replaying a key returns the original intake with 200 and meta.idempotent_replay: true.
shop_id body integer Yes A shop you own. An unknown shop is 400, not 404.
external_order_id body string No Your store's order number. Echoed in webhooks.
currency body string No Default PKR.
shipping_address body object Yes Buyer address. first_name, address_1 and city are required; country is ISO 3166-1 alpha-2.
line_items body array of object Yes At least one line item.
line_items[].listing_id body integer No Preferred reference: resolves directly.
line_items[].design_id body integer No Fallback reference: a design you own.
line_items[].external_variant_id body string No Resolved through your listings.
line_items[].external_sku body string No Resolved through your listings.
line_items[].quantity body integer Yes At least 1.
line_items[].customer_price body string Yes Required. What the buyer pays; drives COD collection. Never defaulted. See the note on quantity.
line_items[].customer_price_currency body string No Defaults to the order currency.
line_items[].customer_name body string No Line name shown on the order. Defaults to the product name.
line_items[].customer_variant body string No Variant label shown to the merchant, e.g. L / Black.
Request body
{
  "shop_id": 5,
  "external_order_id": "1001",
  "currency": "PKR",
  "shipping_address": {
    "first_name": "Bilal",
    "last_name": "Ahmed",
    "phone": "03001234567",
    "email": "[email protected]",
    "address_1": "Flat 4B, Block 5, Clifton",
    "city": "Karachi",
    "state": "Sindh",
    "postcode": "75600",
    "country": "PK"
  },
  "line_items": [
    {
      "external_variant_id": "9001-1",
      "quantity": 1,
      "customer_price": "2500.00",
      "customer_price_currency": "PKR",
      "customer_variant": "L / Black"
    }
  ]
}
Response 202
{
  "data": {
    "id": 1,
    "shop_id": 5,
    "external_order_id": "1001",
    "status": "received",
    "wc_order_id": null,
    "attempts": 0,
    "last_error": null,
    "received_at": "2026-09-29T10:15:00+00:00",
    "processed_at": null
  }
}
Example
curl
curl -s -X POST "$CP_BASE/orders" \
  -H "Authorization: Bearer $CP_KEY" \
  -H "Idempotency-Key: my-store-01:1001" \
  -H "Content-Type: application/json" \
  -d '{"shop_id":5,"external_order_id":"1001","currency":"PKR","shipping_address":{"first_name":"Bilal","last_name":"Ahmed","phone":"03001234567","email":"[email protected]","address_1":"Flat 4B, Block 5, Clifton","city":"Karachi","state":"Sindh","postcode":"75600","country":"PK"},"line_items":[{"external_variant_id":"9001-1","quantity":1,"customer_price":"2500.00","customer_price_currency":"PKR","customer_variant":"L / Black"}]}'

Notes

  • Idempotency-Key header is required (400 without). Use <external_shop_id>:<external_order_id>.
  • New key: 202 with status: "received". Same key again: 200 with the original intake and meta.idempotent_replay: true; nothing new is created.
  • Line items resolve in this order: listing_id, then external_variant_id, then external_sku (through your listings), then design_id.
  • customer_price is required on every line (422 with details.issues[] if missing). The hub sums customer_price across lines and does not multiply it by quantity; check cod_collection_total on GET /orders/{id}.
  • The order is created as Pending payment in the merchant's My Account โ†’ Orders, where they elect COD From Customer.
  • Error statuses: 400, 401, 403, 422, 429. See Errors.

GET /orders/{id}

  • Scope: orders:read
  • Status: 200

One intake plus its WooCommerce order, COD fields and tracking.

Parameters

Name In Type Required Description
id path integer Yes Intake id returned by POST /orders.
Response 200
{
  "data": {
    "id": 1,
    "shop_id": 5,
    "external_order_id": "1001",
    "status": "created",
    "wc_order_id": 50231,
    "attempts": 1,
    "last_error": null,
    "received_at": "2026-09-29T10:15:00+00:00",
    "processed_at": "2026-09-29T10:15:04+00:00",
    "order": {
      "wc_order_id": 50231,
      "status": "completed",
      "total": "1400.00",
      "currency": "PKR",
      "cod_from_customer": true,
      "additional_cod_from_customer": 200,
      "customer_price_total": 2500,
      "cod_collection_total": 2700,
      "tracking": {
        "number": "KHI1234567",
        "company": "M&P Courier",
        "url": "https://mulphilog.com/tracking/KHI1234567"
      },
      "items": [
        {
          "name": "Karachi Skyline Tee - L",
          "quantity": 1,
          "line_total": 1200,
          "customer_price": "2500.00",
          "customer_variant": "L / Black",
          "design_id": 11
        }
      ]
    }
  }
}
Example
curl
curl -s "$CP_BASE/orders/1" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • order is present only once the WooCommerce order exists (status: "created").
  • tracking is null until a courier booking exists. Use it to reconcile a missed order.shipped.
  • cod_collection_total = customer_price_total + additional_cod_from_customer.
  • Error statuses: 401, 403, 404, 429. See Errors.

GET /orders/{id}/events

  • Scope: orders:read
  • Status: 200

Intake event history.

Parameters

Name In Type Required Description
id path integer Yes Intake id returned by POST /orders.
Response 200
{
  "data": [
    {
      "event": "received",
      "at": "2026-09-29T10:15:00+00:00"
    },
    {
      "event": "processing",
      "attempts": 1
    },
    {
      "event": "created",
      "wc_order_id": 50231,
      "at": "2026-09-29T10:15:04+00:00"
    }
  ]
}
Example
curl
curl -s "$CP_BASE/orders/1/events" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • Oldest first, not paginated. A failed event carries reason.
  • The processing event has attempts and no at.
  • Error statuses: 401, 403, 404, 429. See Errors.

Payouts

Read-only COD payout ledger (money CeePrinto owes the brand).

GET /payouts

  • Scope: orders:read
  • Status: 200

The merchant's COD payout ledger.

Parameters

Name In Type Required Description
status query string No all (default), pending (everything not yet paid out), or one exact bucket. Unrecognised values fall back to all.
date_from query string (date) No YYYY-MM-DD, on order creation date.
date_to query string (date) No YYYY-MM-DD, inclusive.
page query integer No 1-based page number.
per_page query integer No Items per page. Values below 1 fall back to 20; values above 100 are clamped to 100.
Response 200
{
  "data": [
    {
      "wc_order_id": 50231,
      "number": "50231",
      "date_created": "2026-09-29T10:15:04+00:00",
      "order_status": "completed",
      "currency": "PKR",
      "amount": 2700,
      "payout_status": "in_hold",
      "payout_status_label": "7-day hold",
      "release_at": "2026-10-08T08:02:11+00:00",
      "days_until_ready": 7,
      "processed_at": null,
      "tracking": {
        "number": "KHI1234567",
        "company": "M&P Courier",
        "url": "https://mulphilog.com/tracking/KHI1234567"
      },
      "needs_payment": false,
      "payment_url": null,
      "items": [
        {
          "name": "Karachi Skyline Tee - L",
          "quantity": 1,
          "customer_price": "2500.00"
        }
      ]
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 20,
    "total": 1,
    "total_pages": 1
  }
}
Example
curl
curl -s "$CP_BASE/payouts?status=pending&per_page=50" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • Requires orders:read: connection-code keys get 403.
  • Display payout_status_label; do not derive it yourself. The hold is 7 days after courier-confirmed delivery (release_at).
  • payment_url is non-null only when needs_payment is true. Show a "Pay now" action only then.
  • Returns 500 when the CeePrinto theme is not active on the hub.
  • Error statuses: 401, 403, 429, 500. See Errors.

GET /payouts/summary

  • Scope: orders:read
  • Status: 200

Payout totals for the merchant.

Response 200
{
  "data": {
    "total": 184500,
    "processed": 120000,
    "pending": 64500,
    "ready": 22000,
    "in_hold": 30500,
    "awaiting_delivery": 12000,
    "missing_bank": 0,
    "count_total": 41,
    "count_processed": 27,
    "count_pending": 14,
    "count_ready": 5,
    "count_in_hold": 6,
    "count_awaiting_delivery": 3,
    "count_missing_bank": 0,
    "currency": "PKR"
  },
  "meta": {
    "cached": false
  }
}
Example
curl
curl -s "$CP_BASE/payouts/summary" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • Cached for 15 minutes per user; meta.cached says which you got. Always the whole ledger (no filters).
  • Requires orders:read.
  • Error statuses: 401, 403, 429. See Errors.

Shipping

Shipping rate quotes (PKR).

POST /shipping/quote

  • Scope: orders:read
  • Status: 200

Shipping rate for a destination city (PKR).

Parameters

Name In Type Required Description
city body string No Destination city. Required unless shipping_address.city is sent.
shipping_address body object No Alternative to city; only city is read.
Request body
{
  "city": "Karachi"
}
Response 200
{
  "data": {
    "city": "Karachi",
    "total": 200,
    "currency": "PKR",
    "method_id": "flat_rate",
    "method_title": "Flat Rate"
  }
}
Example
curl
curl -s -X POST "$CP_BASE/shipping/quote" \
  -H "Authorization: Bearer $CP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"city":"Karachi"}'

Notes

  • Requires orders:read: connection-code keys get 403.
  • Missing city is 400. Amounts are PKR.
  • Error statuses: 400, 401, 403, 429. See Errors.

Webhooks

Signed outbound event subscriptions.

GET /webhooks

  • Scope: webhooks:write
  • Status: 200

The account's webhook subscriptions.

Response 200
{
  "data": [
    {
      "id": 9,
      "topic": "order.status_changed",
      "target_url": "https://my-store-01.pk/webhooks/ceeprinto",
      "secret": "whsec_Xk2p9QvT4mLr7sWn1bYc8dHf3gJa6eZu",
      "status": "active",
      "created_at": "2026-09-15T07:31:00+00:00"
    }
  ]
}
Example
curl
curl -s "$CP_BASE/webhooks" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • Not paginated: bare array, no meta.
  • Each item includes its signing secret. Treat the response as sensitive.
  • Error statuses: 401, 403, 429. See Errors.

POST /webhooks

  • Scope: webhooks:write
  • Status: 201

Subscribe a URL to one topic.

Parameters

Name In Type Required Description
topic body string Yes The event to subscribe to. One of order.status_changed, order.shipped, design.updated, design.publish_requested, product.stock_changed, product.updated.
target_url body string (URL) Yes HTTPS endpoint that receives signed POSTs.
Request body
{
  "topic": "order.status_changed",
  "target_url": "https://my-store-01.pk/webhooks/ceeprinto"
}
Response 201
{
  "data": {
    "id": 9,
    "topic": "order.status_changed",
    "target_url": "https://my-store-01.pk/webhooks/ceeprinto",
    "secret": "whsec_Xk2p9QvT4mLr7sWn1bYc8dHf3gJa6eZu",
    "status": "active",
    "created_at": "2026-09-29T10:15:00+00:00"
  }
}
Example
curl
curl -s -X POST "$CP_BASE/webhooks" \
  -H "Authorization: Bearer $CP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"topic":"order.status_changed","target_url":"https://my-store-01.pk/webhooks/ceeprinto"}'

Notes

  • One topic per subscription. Unknown topic is 400 with details.valid_topics; bad URL is 400.
  • Save secret (whsec_โ€ฆ) to verify X-CP-Signature.
  • Error statuses: 400, 401, 403, 429. See Errors.

DELETE /webhooks/{id}

  • Scope: webhooks:write
  • Status: 204

Remove a subscription.

Parameters

Name In Type Required Description
id path integer Yes Webhook subscription id.
Example
curl
curl -s -o /dev/null -w "%{http_code}\n" -X DELETE "$CP_BASE/webhooks/9" \
  -H "Authorization: Bearer $CP_KEY"

Notes

  • Answers 204. Pending retries for this subscription stop.
  • Error statuses: 401, 403, 404, 429. See Errors.

API ยท Reference

Webhook events reference

Every webhook topic with when it fires and a full example payload.

Each webhook topic below lists when it fires, every payload field, a full example body and a real signed delivery you can test your verifier against.

Envelope and headers (all topics)

Part Value
Method POST to your target_url
Content-Type application/json
X-CP-Topic The topic, e.g. order.shipped
X-CP-Signature t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
Body {"topic": string, "data": object, "sent_at": RFC 3339 UTC string}, compact JSON with escaped slashes (https:\/\/)

All signatures on this page were computed with the example secret whsec_Xk2p9QvT4mLr7sWn1bYc8dHf3gJa6eZu over the exact raw body shown. Feed a raw body and its header to your verifier (with the timestamp tolerance disabled) and it must accept it; change one byte and it must reject it.

order.status_changed

A WooCommerce order placed through the API changes status (for example pending โ†’ processing). Sent to the merchant who owns the order. When the same change also fires order.shipped, a second, reduced order.status_changed is sent carrying only wc_order_id and status.

Field (in data) Type Present Description
wc_order_id integer Always WooCommerce order id (wc_order_id on the intake)
status string Always New WooCommerce status without the wc- prefix
previous_status string Full event only Status before the change
external_order_id string Full event only The external_order_id you sent; empty string if none
Body (pretty-printed)
{
    "topic": "order.status_changed",
    "data": {
        "wc_order_id": 50231,
        "status": "processing",
        "previous_status": "pending",
        "external_order_id": "1001"
    },
    "sent_at": "2026-09-29T10:15:00+00:00"
}
Signed delivery (raw bytes)
POST /webhooks/ceeprinto HTTP/1.1
Content-Type: application/json
X-CP-Topic: order.status_changed
X-CP-Signature: t=1790676900,v1=d5dd3d9012f146631ada820fcc0c3b433f7d726bdf037cba4c64ae2cda33fca8

{"topic":"order.status_changed","data":{"wc_order_id":50231,"status":"processing","previous_status":"pending","external_order_id":"1001"},"sent_at":"2026-09-29T10:15:00+00:00"}

The reduced event sent alongside order.shipped:

Reduced order.status_changed
POST /webhooks/ceeprinto HTTP/1.1
Content-Type: application/json
X-CP-Topic: order.status_changed
X-CP-Signature: t=1790841731,v1=8c31ab481cce00bfbbf0b35f56335296cbee66697e0b8cf4d438200fc7a97d4d

{"topic":"order.status_changed","data":{"wc_order_id":50231,"status":"completed"},"sent_at":"2026-10-01T08:02:11+00:00"}

order.shipped

An API order transitions to completed and has a live (non-cancelled) courier booking. Carries the tracking details, which are also on GET /orders/{id} โ†’ order.tracking.

Field (in data) Type Present Description
wc_order_id integer Always WooCommerce order id
status string Always Always completed
previous_status string Always Status before completion
external_order_id string Always Your order number
tracking_number string Always Courier consignment number
tracking_company string Always Courier name, e.g. M&P Courier
tracking_url string Always Public tracking page
Body (pretty-printed)
{
    "topic": "order.shipped",
    "data": {
        "wc_order_id": 50231,
        "status": "completed",
        "previous_status": "processing",
        "external_order_id": "1001",
        "tracking_number": "KHI1234567",
        "tracking_company": "M&P Courier",
        "tracking_url": "https://mulphilog.com/tracking/KHI1234567"
    },
    "sent_at": "2026-10-01T08:02:11+00:00"
}
Signed delivery (raw bytes)
POST /webhooks/ceeprinto HTTP/1.1
Content-Type: application/json
X-CP-Topic: order.shipped
X-CP-Signature: t=1790841731,v1=cee1b4edfd917ad0906dee86730f60489237461410d49cfd4db21356843ef201

{"topic":"order.shipped","data":{"wc_order_id":50231,"status":"completed","previous_status":"processing","external_order_id":"1001","tracking_number":"KHI1234567","tracking_company":"M&P Courier","tracking_url":"https:\/\/mulphilog.com\/tracking\/KHI1234567"},"sent_at":"2026-10-01T08:02:11+00:00"}

design.updated

A saved design owned by the subscriber changes.

Field (in data) Type Present Description
design_id integer Always The design that changed
status string Always Design status, e.g. saved
Body (pretty-printed)
{
    "topic": "design.updated",
    "data": {
        "design_id": 11,
        "status": "saved"
    },
    "sent_at": "2026-09-29T10:15:00+00:00"
}
Signed delivery (raw bytes)
POST /webhooks/ceeprinto HTTP/1.1
Content-Type: application/json
X-CP-Topic: design.updated
X-CP-Signature: t=1790676900,v1=7b4759b08da6b57e7de5dfe5726b8a7e58a122618511d85a0ff47798b6251541

{"topic":"design.updated","data":{"design_id":11,"status":"saved"},"sent_at":"2026-09-29T10:15:00+00:00"}

design.publish_requested

A publish job is created (API, Products tab or Get Started tab). IDs only: fetch design, blank and hub product detail yourself, then POST /shops/{id}/listings/bulk. Jobs from the Get Started tab omit mode, product_id_hub, variants and per-item mode / external_product_id: treat them as create. Best-effort: poll pending items as well.

Field (in data) Type Present Description
job_id integer Always Publish job id
shop_id integer Always Target shop
mode string Not from Get Started create, link or update
product_id_hub integer or null Not from Get Started Hub product id when the job publishes a hub product
items[].item_id integer Always Close this with complete / fail
items[].design_id integer Always Design to publish
items[].product_id integer or null Always Blank product id
items[].variant_count integer Always Expected number of store variants
items[].mode string Not from Get Started Per-item mode
items[].external_product_id string or null Not from Get Started Store product for link / update
variants[] array Not from Get Started Per-variant composition; empty unless the job came from a hub product
variants[].variation_id integer Blank variation
variants[].design_id integer or null Effective design for that variation
variants[].price number or null Variant price override
variants[].sku string or null Variant SKU override
variants[].enabled boolean Only enabled variants are included
price_mode string Always blank or flat
flat_price number or null Always Set when price_mode is flat
Body (pretty-printed)
{
    "topic": "design.publish_requested",
    "data": {
        "job_id": 3,
        "shop_id": 5,
        "mode": "create",
        "product_id_hub": 42,
        "items": [
            {
                "item_id": 7,
                "design_id": 11,
                "product_id": 37,
                "variant_count": 12,
                "mode": "create",
                "external_product_id": null
            }
        ],
        "variants": [
            {
                "variation_id": 101,
                "design_id": 11,
                "price": null,
                "sku": null,
                "enabled": true
            },
            {
                "variation_id": 102,
                "design_id": 12,
                "price": 2800,
                "sku": "TEE-BLK-L",
                "enabled": true
            }
        ],
        "price_mode": "blank",
        "flat_price": null
    },
    "sent_at": "2026-09-29T10:15:00+00:00"
}
Signed delivery (raw bytes)
POST /webhooks/ceeprinto HTTP/1.1
Content-Type: application/json
X-CP-Topic: design.publish_requested
X-CP-Signature: t=1790676900,v1=ecac396f02a7139327ea77d1c162cabd11a69ae6416ae0492a40f299f71b1d9e

{"topic":"design.publish_requested","data":{"job_id":3,"shop_id":5,"mode":"create","product_id_hub":42,"items":[{"item_id":7,"design_id":11,"product_id":37,"variant_count":12,"mode":"create","external_product_id":null}],"variants":[{"variation_id":101,"design_id":11,"price":null,"sku":null,"enabled":true},{"variation_id":102,"design_id":12,"price":2800,"sku":"TEE-BLK-L","enabled":true}],"price_mode":"blank","flat_price":null},"sent_at":"2026-09-29T10:15:00+00:00"}

product.stock_changed

A blank product or variation changes stock status or quantity (quantity-only changes included). Sent only to merchants who have a listing referencing that product_id / variation_id.

Field (in data) Type Present Description
product_id integer Always Blank (parent) product id
variation_id integer or null Always Variation id; null for a simple product
stock_status string Always instock, outofstock or onbackorder
stock_quantity integer or null Always null when WooCommerce does not manage stock
in_stock boolean Always Use this to toggle availability
Body (pretty-printed)
{
    "topic": "product.stock_changed",
    "data": {
        "product_id": 37,
        "variation_id": 101,
        "stock_status": "outofstock",
        "stock_quantity": 0,
        "in_stock": false
    },
    "sent_at": "2026-09-29T10:15:00+00:00"
}
Signed delivery (raw bytes)
POST /webhooks/ceeprinto HTTP/1.1
Content-Type: application/json
X-CP-Topic: product.stock_changed
X-CP-Signature: t=1790676900,v1=3def3e51f29dd957977a630b4434fa06f557c406bebd180e580edec58eeae3e8

{"topic":"product.stock_changed","data":{"product_id":37,"variation_id":101,"stock_status":"outofstock","stock_quantity":0,"in_stock":false},"sent_at":"2026-09-29T10:15:00+00:00"}

product.updated

A hub product's default design or a variant override changes after it was published (PATCH /products/{id}, PATCH /products/{id}/variants/{variation_id} or the Products tab). The hub has already updated its listings; update your store images for each variant.

Field (in data) Type Present Description
product_id_hub integer Always Hub product id
shop_ids array of integer Always Shops the product is published to
variants[].variation_id integer Always Blank variation
variants[].design_id integer or null Always New effective design
Body (pretty-printed)
{
    "topic": "product.updated",
    "data": {
        "product_id_hub": 42,
        "shop_ids": [
            5
        ],
        "variants": [
            {
                "variation_id": 101,
                "design_id": 11
            },
            {
                "variation_id": 102,
                "design_id": 12
            }
        ]
    },
    "sent_at": "2026-09-29T10:15:00+00:00"
}
Signed delivery (raw bytes)
POST /webhooks/ceeprinto HTTP/1.1
Content-Type: application/json
X-CP-Topic: product.updated
X-CP-Signature: t=1790676900,v1=3109a19bca2da19790b920802b474111c7a423be628337ae57a6b1c67a9cc5cd

{"topic":"product.updated","data":{"product_id_hub":42,"shop_ids":[5],"variants":[{"variation_id":101,"design_id":11},{"variation_id":102,"design_id":12}]},"sent_at":"2026-09-29T10:15:00+00:00"}

Retry schedule

A non-2xx status or a timeout counts as a failure
Attempt Sent Timeout
1 Immediately (background job) 10 s
2 1 minute after attempt 1 fails 10 s
3 2 minutes after attempt 2 fails 10 s
4 4 minutes after attempt 3 fails 10 s
5 8 minutes after attempt 4 fails; then dropped 10 s

API ยท Errors

Errors and troubleshooting

Error codes, what causes them and how to fix the most common integration problems.

Match the status and error.code you got to the table below to find the likely cause and the fix, and include the X-CP-Request-Id when you need help.

Every error code

HTTP error.code Likely cause Fix
400 invalid_request Missing Idempotency-Key on POST /orders; shop_id missing or not yours on POST /orders; no design_ids/product_id or nothing left to publish; unknown webhook topic (details.valid_topics); bad target_url; listing without an external reference or design; missing city on shipping quote; unknown blank_product_id. Read error.message; add or correct the field. Do not retry unchanged.
400 rest_missing_callback_param, rest_invalid_param A required argument WordPress validates (e.g. channel on POST /shops, topic on POST /webhooks) is missing or the wrong type. details.params names it. Send the parameter with the right type.
401 unauthorized No credential; Bearer prefix missing; key mistyped, unknown or revoked; account deleted. Check Authorization: Bearer cp_live_โ€ฆ. Create a new key if revoked or lost.
403 forbidden The key lacks the route's scope. details.required_scope says which; details.granted_scopes lists what it has. Use a key with that scope. Connection-code keys never have orders:read.
404 not_found Wrong id, or the resource belongs to another account (never 403 for that). Re-list the collection to get valid ids.
404 rest_no_route Wrong path or HTTP method, or CP_BASE is missing /wp-json/ceeprinto/v2. Compare with the endpoint reference.
409 conflict POST /shops for a store another CeePrinto account already connected. The other account must disconnect it first; contact support if you own the store.
422 unprocessable_entity Unknown publish mode; link/update without external_product_id; order validation failed (details.issues[]: missing customer_price, quantity below 1, no line reference, missing address fields). Fix every item in details.issues, then resend with the same Idempotency-Key.
429 rate_limited More than 120 requests from this key in the current minute. Sleep Retry-After seconds (also in details.retry_after), then retry.
500 server_error Hub-side failure: mockup renderer inactive (POST โ€ฆ/variation-mockups), payouts unavailable, a row could not be saved. Retry with back-off. If it persists, open a ticket with the request id.

Connection-code keys and 403

Keys named "Shopify App" or "WooCommerce Connect" come from cp1. connection codes and deliberately omit orders:read.

Route Connection-code key
POST /orders Allowed (orders:write)
GET /orders, GET /orders/{id}, GET /orders/{id}/events 403
GET /payouts, GET /payouts/summary 403
POST /shipping/quote 403

Fix: create a key on My Account โ†’ Store Connections โ†’ API Keys with orders:read ticked, and use it for those reads.

Order intake failed

POST /orders answered 202 but GET /orders/{id} shows status: "failed". No WooCommerce order was created. last_error (and the failed event's reason) says why:

last_error Cause Fix
No listing, design or legacy product matched this line item (external_variant_id=โ€ฆ). Create a listing for it, then retry. None of the line's references resolved: no listing on this shop with that listing_id, external_variant_id or external_sku, and no design_id you own. Create the listing (POST /shops/{id}/listings), or send design_id. Then Retry in My Account โ†’ Store Connections โ†’ Order Activity.
WooCommerce product N no longer exists. The listing points at a blank or variation that was removed. Re-publish on a current blank; update or recreate the listing; Retry.
Order has no line items. The stored payload is empty. Submit a new order with a new Idempotency-Key.
A line item could not be added to the order. WooCommerce refused the product. Contact support with the request id.
Anything else Unexpected hub error. Retry in Order Activity; contact support if it repeats.

Resending the same Idempotency-Key returns the failed intake (200, idempotent_replay); it does not reprocess it. Reprocess with the Retry button in Order Activity after fixing the cause.

Common problems

Symptom Cause Fix
Store product shows the garment's back on every variant Mockup cells keyed by variation_id only; the back (stage_index 1) overwrote the front. Key by variation_id + stage_index; use stage_index 0 as the main image.
Blurry store images Uploaded thumb_url (240px). Upload image_url (2048px).
Product published with no images Uploaded before store_status was "ready". Poll GET /designs/{id}/variation-mockups until store_status is "ready" or "partial".
Merchant sees "publishing" forever Publish item never closed, or webhook missed and no polling. Poll GET /shops/{id}/publish-jobs/pending; always call complete or fail.
Duplicate orders New Idempotency-Key per retry. Derive the key from your order: <external_shop_id>:<external_order_id>.
COD collects less than expected on multi-quantity lines customer_price values are summed per line, not multiplied by quantity. Check order.cod_collection_total on GET /orders/{id} before COD is elected.
Webhook signature never matches Signed the re-serialised JSON instead of the raw body, or used the wrong subscription's secret. HMAC the raw bytes; each subscription has its own whsec_ secret (GET /webhooks).
Duplicate listings Rows posted without external_variant_id. Always send external_variant_id; listings upsert on it.
POST /publish-jobs is 400 "already published" create mode skips designs already listed on the shop. Use mode: "update" or "link" with external_product_id.
Rate-limit headers present but 429 never happens The hub enforces the limit only with a persistent object cache. Still honour the headers; the limit may be enforced at any time.

Handling 429

Node
async function withRateLimit(doRequest) {
  for (let attempt = 1; attempt <= 5; attempt++) {
    const res = await doRequest();
    if (res.status !== 429) return res;
    const wait = Number(res.headers.get('retry-after')) || 2 ** attempt;
    await new Promise((r) => setTimeout(r, wait * 1000));
  }
  throw new Error('Still rate limited after 5 attempts');
}

Spread bulk work: at 120 requests per minute, sync with per_page=100 and use POST /shops/{id}/listings/bulk instead of one call per listing.

What to include in a support ticket

Include Where to find it
X-CP-Request-Id Response header on every response, e.g. req_3f9a1c7e5b2d8a40. The most useful single item.
Time (UTC) of the request Your logs
Method and path e.g. POST /orders
Status and full error body Response
Key id (never the secret) GET /me โ†’ api_key.key_id
Resource ids Shop, design, job, item, intake or wc_order_id
X-CP-Api-Version Response header

API ยท Errors

Changelog

Dated list of changes to the v2 API and deprecations of older namespaces.

Every change to the ceeprinto/v2 API that affects integrators, newest first, so you can tell at a glance whether your client needs an update.

2.2.0 (2026-09-29)

Type Change Action needed
Fixed GET /products now honours per_page (default 20, max 100) like every other collection. It was fixed at 20. None. Remove any workaround that paged by 20.
Changed POST /designs/{id}/variation-mockups answers 202 (was 200) with the status envelope. Mockups render on request at the returned URLs, so queued is 0. Accept 202. You can drop the POST and just GET the matrix.
Docs openapi.yaml corrected: operationId, tags and x-scope on every operation, full request/response examples, response headers, and x-webhook-topics with per-topic payload schemas. Regenerate clients from openapi.yaml if you use a generator.
Docs Status codes documented as implemented: POST /products with an unknown blank and POST /publish-jobs without designs are 400 (not 422); DELETE /shops/{id} is 200 with the shop; POST /orders replay is 200 with meta.idempotent_replay. Check your status handling.
Docs GET /webhooks returns each subscription's secret; connection-code keys lack orders:read; GET /shops/{id}/publish-jobs/pending and GET /webhooks are unpaginated. None.
New This documentation page, with Copy as prompt, llms.txt, llms-full.txt and a public openapi.yaml. None.

Deprecations

Namespace Status Sunset
integration/v1 Deprecated: Deprecation: true and Link: โ€ฆ; rel="deprecation" headers Not set yet
app/v1 Deprecated Not set yet
internal/v1 Deprecated Not set yet
ceeprinto/v1 Deprecated. Mockup image URLs returned by v2 (โ€ฆ/ceeprinto/v1/mockup/{design_id}/{signature}.jpg) live here; use them exactly as returned. Not set yet

A Sunset header will be added to legacy responses once a date is chosen, and the date will be listed here first.

Editor

How to design products in the CeePrinto design editor

Editor ยท Basics

Editor overview

What the design editor is, where to open it and a tour of its screen.

The CeePrinto design editor lets you put your own images and text on a product, preview every print side, and add the finished design to your cart or save it for later.

Who uses the editor

  • Buyers customise a single product (a T-shirt, a hoodie, a mug) and order it.
  • Merchants create designs, save them to their account, and publish them to their own online store so CeePrinto can print and ship each order.

Where the editor opens

On any product that can be customised, the product page shows a "Customize" button next to the normal cart button. Clicking it opens the editor on its own page at /product/{product-name}/design/. If the product comes in options (for example sizes or colours), pick an option first; the button stays greyed out until you do.

Some pages open the editor in a pop-up window instead. That window is titled "Customize Your Design" and has a "Close" button and a "Save & Add to Cart" button at the bottom.

The shop owner can rename the "Customize" button, so on some stores it may carry a different label.

The screen at a glance

Main parts of the editor
Area Where What it is for
Top bar Top of the screen Shows where you are (product name / "Design Tools" / current side), the Undo and Redo buttons, the tools for the selected layer, "My Designs" (signed-in customers) and the "Add to Cart" button with its menu.
Design Tools sidebar Left Three tabs: "Product" (product details and your layers), "Images" (upload and reuse pictures) and "Text" (add text). A "Design Area" picker appears below when a side has more than one printable area.
Canvas Centre The product photo with a dashed box. Only what sits inside the dashed box is printed. The line under the canvas shows the print size of the side and any placement notes.
Print Sides panel Right One card per printable side (for example "Front" and a back side). Each card shows the print size, any extra price, and how many layers are on it.
Layers Left, "Product" tab Every image and text you add, top layer first, with "Move up", "Move down" and "Delete layer" buttons.

On a phone, the sidebar and the Print Sides panel slide in from the edges. Use the menu button at the top left ("Open design tools") and the grid button at the top right ("Open print sides"). The "Add to Cart" button sits in a bar at the bottom of the screen.

Design guidelines and pricing

Under the cart button on every product page you will find a short note asking you to make sure your design follows the CeePrinto guideline. Read the design guidelines before you order; CeePrinto is not responsible for designs that do not follow them. The same note links to the product's "Pricing Chart" tab.

You pay for each side you actually print on. A side you leave empty costs nothing extra. See Print sides and design areas.

What happens after "Add to Cart"

  1. The editor checks that nothing sticks out of the dashed print area and warns you if it does.
  2. It creates a preview and a high-resolution print file (300 DPI) for every side you used, and uploads them.
  3. It saves your design to the store and adds the product to your cart.
  4. The cart shows the item with a "Custom design" line and, when sides carry a price, a "Customization fee" line.

Editor ยท Basics

Your first design

Open a product, add text and an image, and add the finished design to your cart.

Follow these ten steps to go from a product page to a custom product in your cart in a few minutes.

  1. Open the product and choose an option

    Go to the product you want to customise. If it has options such as size or colour, choose one first. The "Customize" button stays greyed out until an option is selected.

  2. Click "Customize"

    The editor opens on its own page (/product/{product-name}/design/). Wait for "Setting up your designerโ€ฆ" to finish. The top bar shows the product name, then "Design Tools", then the side you are on, for example "Front".

  3. Pick the side you want to print on

    In the "Print Sides" panel on the right, click a side card. Each card shows the print size in inches and any extra price for that side. On a phone, tap the grid button at the top right ("Open print sides").

  4. Upload an image

    In the left sidebar open the "Images" tab and drag a file onto "Drop images here or browse", or click the box to choose a file. The image appears on the canvas once the upload finishes. Pictures you uploaded before are listed underneath; click one to add it again.

  5. Check the print quality badge

    Click the image. The toolbar in the top bar shows a quality badge: "Good" is ready to print, "OK" may print slightly soft, "Low" will print blurry. Make the image smaller or upload a bigger file if you see "Low".

  6. Add text

    Open the "Text" tab and click "Add Text Layer". In the "Add New Text" window type your words under "Text content", choose a size, colour and font, then click "Add Text".

  7. Move, resize and arrange

    Drag a layer to move it and drag its corner handles to resize or rotate it. Keep everything inside the dashed box: the hint under the canvas reads "Place images and text inside the dashed design area." Use the "Product" tab to see your layers and change their order.

  8. Repeat for other sides (optional)

    Click another card in "Print Sides" and add content there too. Every side you use adds its own price, shown at the bottom of the "Print Sides" panel.

  9. Click "Add to Cart"

    Click "Add to Cart" in the top bar (on a phone, in the bar at the bottom). If part of a layer is outside the print area you will see "Some layers will be cropped" or "Some layers won't print". Choose "Fix automatically" to pull the layers back inside, or "Continue anyway".

  10. Wait for the progress window

    A window titled "Adding your design to cart" lists each step, starting with "Preparing your design" and ending with "Adding to cart". When it shows "Redirecting to cartโ€ฆ" you are taken to your cart, where the item shows a "Custom design" line.

Want to keep designing after adding one item? Open the small arrow next to "Add to Cart" and choose "Add to cart and stay on page".

Editor ยท Basics

Print sides and design areas

Switch between print sides, pick a design area and see how sides affect the price.

Choose which sides of the product to print on, keep your design inside each side's dashed print area, and see exactly what each side adds to the price.

Switch between sides

  1. Open "Print Sides"

    The "Print Sides" panel is on the right. On a phone, tap the grid button at the top right ("Open print sides"). Products with only one side do not show side cards.

  2. Click a side card

    The canvas switches to that side and its name appears at the end of the top bar breadcrumb. The first side is usually "Front".

  3. Design that side

    Images and text you add go onto the side you are viewing. Each side keeps its own layers.

What a side card shows

Side card contents
Item on the card Meaning
Name The side, for example "Front".
Price (for example +250) Extra cost added when this side has any content. Not shown for sides that are included free.
Description A short note from CeePrinto about the side, when set.
Print size (for example 12" ร— 16") The largest size CeePrinto can print on this side, width ร— height in inches.
Layer count (for example 2 layers) How many images and texts are on this side.

Design areas and the dashed box

Every side has at least one printable area, drawn on the canvas as a dashed box. Only what sits inside the dashed box is printed; anything outside is cut off. The hint under the canvas reads "Place images and text inside the dashed design area." and, next to it, "Print" with the side's print size and, when set, "Placement" notes from CeePrinto (for example how far below the collar the print sits).

Some sides have more than one printable area. In that case a "Design Area" drop-down appears at the bottom of the left sidebar. Choose an area before you add an image or text; new layers are placed in the chosen area.

When something sticks out

If a layer crosses the dashed edge, the hint under the canvas turns amber and says how many layers are outside the print area, with a "Fit to area" button. The layer list marks those layers "Partly cropped" or "Won't print".

When you click "Add to Cart", "Save Design", "Update design", "Download PNG (current side)" or "Export PDF (current side)", the editor checks every side and shows a window if anything is outside:

Overflow warning
Window title When Your choices
"Some layers will be cropped" At least one layer is partly outside the print area. "Fix automatically" shrinks and moves the layers inside. "Continue anyway" keeps them as they are; the outside part will not print.
"Some layers won't print" Every listed layer is completely outside the print area. Same two buttons. With "Continue anyway" those layers are left off the print.

How sides affect the price

You pay the product price plus the price of each side you actually use. A side with no layers adds nothing. The bottom of the "Print Sides" panel lists the sides you have used with their prices, and the note "Price applies when you add design to a side." In the cart the extra appears as a "Customization fee" line.

Example side pricing (illustrative amounts; see the product's Pricing Chart for real prices)
Example Sides used Extra charge
Front only Front (+250) 250
Front and back Front (+250), Back (+300) 550
Nothing added to the back Front (+250), Back empty 250

Print specifications

What CeePrinto prints from
Specification Value
Print method DTF (direct to film) transfer
Print file resolution 300 DPI
Print size Set per side by CeePrinto, in inches (shown on each side card and under the canvas)
Files made per used side A preview image, a print PNG and a print PDF

Tips

  • Leave a small margin inside the dashed box; edges of a print can shift slightly.
  • Use "Fit to area" before adding to cart so you do not get the warning window.
  • Check every side before ordering. A stray layer on the back adds that side's price.

Editor ยท Tools

Text

Add and style text: fonts, size, colour, spacing, alignment and effects.

Add text to the current side, style it with any of 13 fonts, and change it later from the toolbar without starting over.

Add text

  1. Open the "Text" tab

    In the left "Design Tools" sidebar, click "Text", then "Add Text Layer".

  2. Type and style your text

    The "Add New Text" window shows a live preview at the top. Type under "Text content" (the box says "Enter your text"), then set the size, colour, spacing, alignment, style and font.

  3. Click "Add Text"

    The text is placed inside the dashed area of the current side. Click "Cancel" to close the window without adding anything.

"Add New Text" fields

Fields in the Add New Text window
Field What it does Allowed values
"Text content" The words to print. Any text
"Size (px)" Text size on the canvas. 8 to 200
"Color" Text colour, picked from a colour chooser. Any colour
"Letter spacing (px)" Space between letters. Negative values squeeze letters together. -5 to 50, steps of 0.5
"Line height" Space between lines, as a multiple of the text size. 0.8 to 3, steps of 0.1
"Formatting" Align left, centre or right, plus "Bold", "Italic", "Underline" and "Strikethrough" toggles (B, I, U, S). On or off
"Font family" The font. Type in "Search fontsโ€ฆ" to filter the list. 13 fonts, see below

Available fonts

13 fonts in the font picker
Font Type
Inter (default) Web font, looks the same on every device
Roboto Web font
Open Sans Web font
Lato Web font
Montserrat Web font
Poppins Web font
Playfair Display Web font
Oswald Web font
Source Sans 3 Web font
Nunito Web font
Arial (System) Uses the font installed on your device
Times New Roman (System) Uses the font installed on your device
Helvetica (System) Uses the font installed on your device

The three "(System)" fonts depend on your computer or phone. If a device does not have that font, a similar one is used instead. For a guaranteed look, choose one of the 10 web fonts.

Edit existing text

Click a text layer on the canvas (or in the "Product" tab's layer list). The text toolbar appears in the top bar (on a phone, just below it). Changes apply immediately.

Text toolbar
Control What it does
"Edit textโ€ฆ" box Change the words.
Font button (its tooltip names the current font) Opens the "Font family" list with "Search fontsโ€ฆ".
"Font size" Number box, 8 to 200.
"Text color" Colour swatch; click to pick a new colour.
"Bold", "Italic", "Underline", "Strikethrough" Turn each style on or off.
"Align left", "Align center", "Align right" Align lines of text inside the text box.
"Bring forward" / "Send backward" Move the text one step above or below other layers.
Drag handles on the canvas Move, resize (keeps proportions) and rotate the text.

Tips

  • Keep text a little away from the dashed edge so nothing gets trimmed.
  • Very thin fonts at small sizes may not print cleanly. Use "Bold" or a larger size for small lettering.
  • Keyboard shortcuts (such as Delete) are paused while you type in a text box, so you will not delete a layer by accident.

Editor ยท Tools

Images and uploads

Upload artwork, check print quality, crop, remove backgrounds, mask and fit images.

Upload your artwork, check that it is sharp enough to print, then crop, clean up the background, shape and fit it without leaving the editor.

Upload an image

  1. Open the "Images" tab

    In the left sidebar, click "Images".

  2. Drop or choose files

    Drag one or more files onto "Drop images here or browse", or click the box (or press Enter when it is focused) to pick files. While you drag, the box says "Drop to upload".

  3. Watch the progress list

    Each file gets its own row with a progress bar showing how much has been sent. When a row shows "Uploaded", the image is placed in the middle of the dashed area on the current side.

Upload limits
Item Value
Recommended file types PNG, JPG, WebP (GIF is also accepted)
SVG Accepted. Vector artwork is cleaned on upload (scripts and external references are removed) and scales without losing sharpness, so it is the best choice for logos and text-based designs.
Largest file 200 MB
Files at once As many as you like; they upload one after another
Automatic retries per file Up to 4 attempts
Best for transparent artwork PNG with a transparent background

Large files and poor connections

Big files are sent in small pieces. If your connection drops, the row shows "Waiting for connectionโ€ฆ" and carries on by itself when you are back online. Short hiccups show "Retrying" with the attempt number. If a file still fails, its row turns red with the reason and a "Retry" button. Retrying continues from the last piece that arrived, so nothing is sent twice. Click the X on a row to cancel an upload or to clear a finished one.

Reuse earlier uploads

Below the upload box the editor lists the images you uploaded before. Type in "Search your uploadsโ€ฆ" to find one by name and click a thumbnail to add it to the current side. If you have not uploaded anything yet it says "Nothing uploaded yet."

Print quality badge

When you select an image, the image toolbar starts with a coloured badge that tells you how sharp it will print at its current size on this side. Hover over the badge to see the exact DPI (dots per inch). Making an image bigger lowers its DPI; making it smaller raises it.

DPI badge thresholds
Badge DPI at current size What it means What to do
"Good" (green) 300 or more Prints sharp. Nothing.
"OK" (amber) 150 to 299 "may print slightly soft" Fine for large, bold artwork. For fine detail, shrink the image or upload a larger file.
"Low" (red) Below 150 "will print blurry" Shrink the image or upload a higher-resolution file.

The badge only appears when the side has a print size set, and on wide screens. Print files are always produced at 300 DPI; the badge tells you whether your picture has enough detail to fill that.

Image toolbar

Select an image to show these controls in the top bar (on a phone, just below it).

Image toolbar
Control What it does
"Crop" Opens the "Crop Image" window. Drag the box to keep only part of the picture, then "Apply crop". "Reset crop" brings back the full picture.
"Remove solid background" Removes a plain background colour (white, green screen or any single colour). See below.
"Mask shape" Shows the picture through a shape: "None", "Rectangle", "Circle" or "Ellipse".
"Fit mode" How the picture fills its box: "Default", "Cover", "Contain", "Fill (stretch)", "Fit width" or "Fit height".
"Background fill" Puts a solid colour behind a transparent PNG. "Clear" removes it.
"Flip horizontal" / "Flip vertical" Mirror the picture left to right or top to bottom.
"Bring forward" / "Send backward" Move the picture one step above or below other layers.
Drag handles on the canvas Move, resize (keeps proportions) and rotate the picture.

Remove a solid background

  1. Open "Remove solid background"

    Select the image and click "Remove solid background". The window explains: "Best for white or green-screen images. Not for photo backgrounds."

  2. Choose the colour to remove

    Click a preset ("White" or "Green screen"), click "Auto-detect", or click "Eyedropper" and then click the background on the canvas. While picking, the hint under the canvas reminds you to click the background colour. Press Esc or click "Cancel pick" to stop. You can also type a colour code.

  3. Adjust and apply

    Raise "Tolerance" to remove more shades close to the chosen colour, and raise "Edge softness" to smooth the cut edge. Click "Remove background".

Tips

  • Use PNG with a transparent background for logos so no box prints around them.
  • A light halo after background removal usually means "Tolerance" is too low; raise it a little and try again.
  • "Background fill" is only useful for images with transparent areas; on a photo you will not see a difference.

Editor ยท Tools

Layers

Reorder, select and delete the elements on each print side.

Use the layer list to select, reorder and delete every image and text on the current side, and to spot anything that will not print.

Find the layer list

Open the "Product" tab in the left sidebar. Under "Layers" you see one card for each image or text on the side you are viewing. The top card is the layer in front; the bottom card is the one at the back. Each card shows a small preview, the layer's name and whether it is an image or text.

When a side has no content yet, the list reads "No layers yet" and "Upload an image or add text to start".

Work with layers

  1. Select a layer

    Click a card in the list, or click the layer on the canvas. The selected card is highlighted and the matching toolbar (text or image) appears in the top bar.

  2. Change the order

    Click "Move up" to bring a layer in front of the one above it, or "Move down" to send it behind. The top layer cannot move up and the bottom layer cannot move down. The toolbar buttons "Bring forward" and "Send backward" do the same thing.

  3. Delete a layer

    Click "Delete layer" (the red bin) on its card, or select the layer and press Delete or Backspace. Changed your mind? Click "Undo" in the top bar or press Ctrl+Z (Cmd+Z on a Mac).

Controls

Layer list controls
Control What it does
Layer card Selects the layer.
"Move up" Moves the layer one step towards the front.
"Move down" Moves the layer one step towards the back.
"Delete layer" Removes the layer from the side.
"Partly cropped" badge (amber) Part of the layer is outside the dashed print area. That part will not print.
"Won't print" badge (red) The whole layer is outside the dashed print area. None of it will print.

Tips

  • The list only shows layers on the current side. Switch sides in "Print Sides" to see the others; each side card shows how many layers it holds.
  • If a layer shows a warning badge, use "Fit to area" under the canvas to pull everything on that side back inside the print area.
  • Layers cannot be hidden, locked or grouped. Delete a layer you do not want printed.

Editor ยท Tools

Designing per size and colour

Apply a change to every variation or only the one you are editing.

When you edit a saved design, you can keep one shared design for every size and colour of the product, or give a single option its own version.

When this applies

Per-option editing is available when you open a design you saved earlier (with "Use design" in Saved Designs, or from "My Designs" in the editor) on a product that comes in options such as sizes or colours. Merchants use it to adjust artwork for, say, a dark shirt colour or a small size, while every other option keeps the shared design.

In this mode the top bar shows "Update design" next to "Add to Cart", and the "Product" tab lists the product's options.

The option list

Open the "Product" tab. Under the product name you see one row per option, with a small picture and the option name. The option you are editing is highlighted.

Option list in the Product tab
Item What it means or does
Option row Click to switch the canvas to that option.
Dot marked "Customized for this option" This option has its own version of the design. Options without the dot use the shared (base) design.
"Reset to base design" Shown when the selected option has its own version. Throws that version away so the option uses the shared design again.
"Change Product" Move the design to a different product (see below).
"View product page" Opens the product page.

Change one option or all of them

  1. Select an option and make an edit

    Click an option row, then move, add or change anything on the canvas.

  2. Answer "Apply this change toโ€ฆ"

    On your first edit the editor asks where the change belongs. Choose "Change this variation only" to give this option its own version, or "Change every variation" to update the shared design used by every option that does not have its own version. Closing the window cancels the edit.

  3. Keep editing

    The editor remembers your answer for that option, so it will not ask again for every small change. After "Change every variation" it stops asking for the rest of the session.

  4. Click "Update design"

    This saves the shared design and every option's own version. The progress window shows "Updating your design" and ends with "Design updated".

Switching options with unsaved changes

If you click another option (or another product) before saving, the editor asks "Save changes before switching?".

Save changes before switching?
Button Result
"Save and switch" Saves your changes, then opens the other option.
"Discard and switch" Drops your unsaved changes and keeps the last saved version.
Close the window Stays on the current option; nothing is lost.

Move a design to another product

Click "Change Product" in the "Product" tab. The "Switch Product" window lets you search with "Search productsโ€ฆ"; for products with options, pick one with "Search optionsโ€ฆ". The last step, "How do you want to switch?", offers:

Switch Product choices
Button Result
"Keep my design, just change product" Carries your layers over to the new product.
"Start a new design for this product" Opens the new product with an empty canvas.

Tips

  • Make shared changes first with "Change every variation", then fine-tune individual options.
  • Look for the "Customized for this option" dot before a big change; those options will not pick up shared edits.

Editor ยท Tools

Keyboard shortcuts

Every keyboard shortcut the editor supports.

Use these keyboard shortcuts to delete, undo and redo faster while you design.

Editor keyboard shortcuts
Action Windows / Linux Mac
Delete the selected layer Delete or Backspace Delete or Backspace
Undo Ctrl+Z Cmd+Z
Redo Ctrl+Y or Ctrl+Shift+Z Cmd+Y or Cmd+Shift+Z
Stop picking a colour with the "Eyedropper" Esc Esc
Close an open toolbar menu (font, "Mask shape", "Fit mode", "Background fill") or the "Add to Cart" menu Esc Esc
Open the file browser from a focused upload box Enter or Space Enter or Space

Shortcuts are paused while you are typing in a box (for example "Edit textโ€ฆ" or "Font size"), so pressing Backspace there edits your text instead of deleting the layer.

Buttons that do the same thing

The "Undo" and "Redo" buttons sit in the middle of the top bar; their tooltips read "Undo (Ctrl+Z)" and "Redo (Ctrl+Y)". "Delete layer" in the layer list removes a layer with the mouse.

Save a design to My Designs, download a PNG or PDF, or add it to your cart.

Add your design to the cart, save it to your account to reuse or sell later, or download a copy of the current side as a PNG or PDF.

Guests and signed-in customers

What guests and signed-in customers can do
You canโ€ฆ Guest Signed in
Design and "Add to Cart" Yes Yes
"Add to cart and stay on page" Yes Yes
"Download PNG (current side)" / "Export PDF (current side)" Yes Yes
"Save Design" for later No (sign in first) Yes
Open "My Designs" in the editor No Yes (wide screens)
Manage designs under "Saved Designs" in My Account No Yes

Guests: your design is attached to your cart item, but it is not kept in a library. Sign in before you start if you want to reuse the design later.

The "Add to Cart" button and its menu

The main button in the top bar (on a phone, in the bottom bar) is "Add to Cart". The small arrow beside it ("More actions") opens a menu.

Add to Cart menu
Menu item What it does Who sees it
"Add to Cart" (main button) Saves the design, adds the product to your cart and takes you to the cart. Everyone
"Add to cart and stay on page" Adds to cart and keeps the editor open. The window says "Added to cart. You can keep editing." with a "View cart" link. Everyone
"View Cart" Opens your cart without adding anything. Everyone
"Save Design" Asks "Name your design (optional):" and saves the design to your account. Signed-in customers, when not already editing a saved design
"Download PNG (current side)" Downloads an image of the side you are viewing, at print scale. Everyone
"Export PDF (current side)" Downloads a PDF of the side you are viewing, sized to the side's print size in inches. Everyone

All of these (except "View Cart") first check for layers outside the print area and may show the "Some layers will be cropped" window. See Print sides and design areas.

Progress window

Adding to cart can take a little while because the editor makes high-resolution print files for every side you used. A window titled "Adding your design to cart" lists each step and ticks it off. Keep the page open until it finishes.

Add to cart steps (steps 2 to 4 repeat for each used side)
# Step What happens
1 "Preparing your design" Collects your layers and checks the sides you used.
2 "Creating preview" (per side) Makes a preview picture of the side.
3 "Uploading print file" (per side) Uploads the 300 DPI print PNG. Usually the longest step.
4 "Uploading print PDF" (per side) Uploads the print PDF.
5 "Saving design to store" Stores the design with the product and price.
6 "Adding to cart" Puts the product in your cart.
7 "Redirecting to cartโ€ฆ" Takes you to the cart (skipped with "Add to cart and stay on page").
  • If your internet drops, the window says it is waiting for your connection and resumes by itself.
  • If a step fails, the window shows "Could not add to cart" with "Retry" and "Close". "Retry" carries on from where it stopped; work already saved is not redone.

Save a design to your account

  1. Choose "Save Design"

    Open the menu next to "Add to Cart" and choose "Save Design". You must be signed in.

  2. Name it

    Type a name in "Name your design (optional):" or keep the suggested one, then confirm. Cancel stops the save.

  3. Wait for "Saved to My Designs"

    The window runs "Preparing your design", "Uploading preview" and "Saving to My Designs".

  4. Open it again later

    Click "My Designs" in the top bar to open "My Saved Designs", then click a design. The editor shows "Loading saved designโ€ฆ" and then "Design loaded. You can edit it and add to cart." To save changes to that same design, click "Update design".

Download PNG or PDF

"Download PNG (current side)" and "Export PDF (current side)" save a file to your device straight away. They cover only the side you are viewing, do not add anything to your cart, and are meant for proofs or sharing with a colleague. The files CeePrinto prints from are made when you click "Add to Cart".

"Saved Designs" in My Account

Signed-in customers find every saved design under My Account, "Saved Designs". Each card shows the design's name (or "Design #" and a number), "Last used on:" with the product, and the "Saved:" date.

Saved Designs actions
Button What it does
"Use design" Opens the product's design page with this design loaded, ready to edit or add to cart.
"Publish to store" Merchants: opens Store Connections, "Get Started", with this design selected so you can publish it to your connected store. Tick several cards and use the "Publish to store" button above them to publish in bulk.
"Delete" Asks "Delete this saved design?" and removes the design.
Delete results
Message after "Delete" Why
"Design deleted." The design was removed.
"This design belongs to one of your orders and cannot be deleted." An order uses this design, so CeePrinto keeps it for printing and reprints.
"This design is still linked to a store listing. Unlink it under Store Connections โ†’ Listings first." A product in your connected store uses it. Unlink the listing, then delete.

Tips

  • Use "Add to cart and stay on page" to order the same product in several designs without reopening the editor.
  • Give saved designs clear names; the list and the cart show them.
  • After you order, your order page shows a "Custom Design" panel with the print size, placement and "Design Files" links.

Editor ยท Saving

Editor troubleshooting

Fixes for upload failures, low-quality warnings, overflow prompts and other common issues.

Find the message or problem you are seeing, then follow the fix in the same row.

Problem, cause and fix
Problem Likely cause Fix
Upload fails with "That file type is not accepted here." The file is not PNG, JPG, WebP, GIF or SVG, or it is an SVG the sanitizer could not clean. Save or export the artwork as PNG (transparent background for logos) or JPG and upload again.
Upload fails with "That file is too large" (limit 200 MB) The file is over 200 MB. Export a smaller file. A PNG sized for the side's print size at 300 DPI is plenty.
Upload row turns red with "Upload failed" or stops at "Waiting for connectionโ€ฆ" The connection dropped or timed out. Wait for your connection to come back; the upload resumes on its own. Otherwise click "Retry"; it continues from where it stopped.
"That upload session has expired. Please start over." An unfinished upload was left too long before retrying. Click the X to clear the row and upload the file again.
Image badge shows "Low" or "OK" The picture does not have enough pixels for its size on the product (below 150 DPI is "Low", 150 to 299 is "OK"). Make the image smaller on the canvas, or upload a higher-resolution original. Aim for "Good" (300 DPI or more).
"Some layers will be cropped" or "Some layers won't print" appears A layer crosses or sits outside the dashed print area. Click "Fix automatically", or move and shrink the layer yourself. "Continue anyway" prints only the part inside the box.
Editor stays on "Setting up your designerโ€ฆ" or the design never appears A slow connection, a browser extension blocking scripts, or a very old browser. Reload the page, turn off ad or script blockers for this site, and use an up-to-date Chrome, Edge, Firefox or Safari.
"No design configuration found for this product." or "This product has no design configuration yet." The product (or the product you tried to switch to) is not set up for customisation. Choose another product, or contact CeePrinto support.
"Could not load design" when opening a saved design The design was deleted, belongs to another account, or you are signed out. Sign in with the account that saved it and open it again from "Saved Designs" with "Use design".
"Customize" button is greyed out The product has options and none is selected. Choose a size, colour or other option on the product page first.
"Save Design" is missing from the menu You are not signed in, or you are editing a saved design (use "Update design" instead). Sign in (your cart is kept), then reopen the editor. When editing a saved design, click "Update design".
"My Designs" button is missing You are signed out, or the screen is narrow (the button shows on wide screens only). Sign in, or open your designs from My Account, "Saved Designs".
Cannot delete a saved design: "This design belongs to one of your orders and cannot be deleted." An order uses this design and CeePrinto keeps it for printing. Nothing to fix; the design stays in your list.
Cannot delete a saved design: "This design is still linked to a store listing." A product in your connected store still uses the design. Unlink it under Store Connections, Listings, then delete.
Light or coloured fringe after "Remove background" "Tolerance" is too low for shades near the background colour, or the edge is too hard. Undo, open "Remove solid background" again, raise "Tolerance" a little and add some "Edge softness". For photos, remove the background in another tool and upload a transparent PNG.
Text looks different in print than on screen A "(System)" font (Arial, Times New Roman, Helvetica) was replaced by a similar font on your device, or the text was very thin or small. Use one of the 10 web fonts (for example Inter or Montserrat), and use "Bold" or a bigger size for small lettering.
Price is higher than expected A layer sits on a side you did not mean to use; every used side adds its price. Check each card in "Print Sides" for a layer count and delete stray layers.
Progress window shows "Could not add to cart" A network or server error during one of the steps. Click "Retry"; finished steps are not repeated. If it keeps failing, click "Close", reload, and try again.

Still stuck? Note the exact message and the product, and contact CeePrinto support.

Merchant

Selling with CeePrinto from My Account and Store Connections

Merchant ยท Setup

Selling with CeePrinto: the journey

The path from design to product, store, orders and payouts at a glance.

CeePrinto prints, packs, ships and collects cash on delivery for your brand, and this page shows the six steps from a first design to money in your bank account.

You are a merchant: a brand owner who sells print-on-demand products (you design, we print only when an order comes in). Everything you need lives under My Account on ceeprinto.com. Your own shop can be a Shopify store, a WooCommerce (WordPress) store, or your own software talking to the CeePrinto API.

The journey in six steps

  1. 1. Design

    Open a blank product (an unprinted T-shirt, hoodie and so on) in the editor, add your artwork and click Save Design. It appears in My Account โ†’ Saved Designs. See Saved Designs.

  2. 2. Build a product

    In Store Connections โ†’ Products, create a product from a blank, choose a Default design and, if you like, give some Size or Color variants their own design. See Products.

  3. 3. Connect your store

    In Store Connections โ†’ Get Started, install the Shopify app or the CeePrinto Connect WordPress plugin, then generate a connection code and paste it into the app. See Get Started.

  4. 4. Publish

    Publish the product (or selected saved designs) to a connected store. The store app creates the product with every Size and Color variant and uploads the mockup images. See Products and Connected stores.

  5. 5. Orders and COD

    When a buyer orders on your store, the order arrives in Order Activity and becomes an order in My Account โ†’ Orders with status Pending payment. You choose whether we collect cash on delivery (COD) from your buyer, then pay the order. You can also order directly in the cart. See Orders and COD and Cart order options.

  6. 6. Payouts

    After the courier confirms delivery we hold the collected cash for 7 days, then transfer it to the bank account saved on Pay Out. See Payouts.

Where everything lives in My Account

The My Account menu is ordered for sellers. Some items only appear when they apply to you.

My Account menu, top to bottom
Menu item What you do there Docs
Dashboard Overview, alerts that need action, orders waiting on you. This section
Orders Every order, including ones sent by your store. Open a Pending payment order to set COD. Orders and COD
Returns Parcels returned by your buyers. Shown only when you have returned orders. Orders and COD
Saved Designs Designs saved from the editor: use, publish or delete. Saved Designs
Store Connections Get Started, Products, Stores, Orders and Developer tabs. Get Started
Brand Brand name and brand icon used on packaging. Cart order options
Pay Out COD collections, payout status and your bank details. Payouts
Support Open and follow support tickets. FAQ
Addresses Your billing address. Orders copy it, so keep it complete. Orders and COD
Account details Name, email and password.

Store Connections tabs

Top tab Sub-tabs Purpose
Get Started Install links, connection codes and a quick bulk publish of saved designs.
Products Hub products: default design, per-variant overrides, publish.
Stores Your connected stores: rename, disconnect or reconnect.
Orders Order Activity, Listings, Fulfillment Profile Incoming store orders, design-to-store mappings, address check.
Developer API Keys, Webhooks, Product Mapping For your own software or developer.

Dashboard alerts

The Dashboard shows an alert card for each setup task you have not finished. Each card has a button that takes you to the fix.

Alert Button What it means Fix
Add your bank details Add bank details No bank account is saved, so payouts cannot be sent. Payouts show Missing bank details. Payouts
Complete your fulfillment profile Update fulfillment profile Your billing Phone, Address line 1, City or Country is empty. Store orders copy these, so shipments would be incomplete. Orders and COD
Connect your store Connect your store No store is connected yet. Get Started

Connect your Shopify or WooCommerce store from Store Connections โ†’ Get Started.

Connect a Shopify or WooCommerce store to CeePrinto in about five minutes from My Account โ†’ Store Connections โ†’ Get Started.

Get Started is the first tab of Store Connections. It walks you through four numbered steps: Install, Keys, Create Your Design and Publish. A step turns green when it is done. New brands land here straight after signing up.

What a connection code is

A connection code is a single line of text that starts with cp1.. It bundles the CeePrinto site address and a private access key made just for your store app, so you paste one thing instead of several. Treat it like a password: anyone holding it can publish to and send orders into your account.

Fact Value
Starts with cp1.
Buttons that create one Generate Shopify Code, Generate WordPress Key
How many times it is shown 1 (on the page that loads right after you click)
How long the page waits to show it 2 minutes. After that the code is never shown again.
How long the key inside stays valid Until you generate a new code of the same type, or revoke the key
Active codes per type 1 for Shopify, 1 for WordPress. Generating a new one switches off the old one.
What the store app can do with it Read your designs and the blank catalog, create and remove store listings, send orders, register the store and webhooks
What it cannot do Read your order list, payouts or shipping quotes through the API. The store apps do not need these.

Connect a Shopify store

  1. Install the Shopify app

    In step Install, click Install Shopify App and approve the app in your Shopify admin.

  2. Generate the code

    Come back to Get Started. In step Keys, click Generate Shopify Code. The page reloads with a box titled Your Shopify connection code.

  3. Paste it into Shopify

    Copy the whole code (it starts with cp1.) and paste it into the CeePrinto app Settings in Shopify. Save.

  4. Check the store is connected

    Open Store Connections โ†’ Stores. Your Shopify store is listed with status Active, and Get Started shows how many stores are connected.

Connect a WooCommerce (WordPress) store

  1. Download and install the plugin

    In step Install, click Download WordPress Plugin. In your own WordPress admin, go to Plugins โ†’ Add New โ†’ Upload Plugin, upload the file and activate CeePrinto Connect.

  2. Generate the code

    Back on Get Started, in step Keys, click Generate WordPress Key. The page reloads with a box titled Your WordPress connection code.

  3. Paste it into CeePrinto Connect

    Copy the code, open CeePrinto Connect on your WooCommerce store, paste it and click Connect.

  4. Done

    The plugin registers your store and its webhooks automatically. Check Store Connections โ†’ Stores: the store is listed as Active.

If the code expired or you lost it

You cannot view a code again. Click the same button (Generate Shopify Code or Generate WordPress Key) to make a new one and paste the new code into your store app.

Regenerating switches off the old code. If a store is already connected with the old code, it stops talking to CeePrinto until you paste the new code into it. Only regenerate when you are ready to update the store app.

Where the install links come from

The Install Shopify App and Download WordPress Plugin buttons open links set by the CeePrinto team. If a button is greyed out with the note Configure in Settings โ†’ CeePrinto API, the link has not been set yet. Open a ticket from My Account โ†’ Support and we will send it to you.

The other two steps on this tab

Step What it shows
Create Your Design If you have no saved designs, a Browse blanks button to open the shop. Otherwise the number of saved designs ready.
Publish Pick a Target store, tick Designs and click Publish. Each design is published as one store product with every Size ร— Color variant of its blank. Designs already on that store are greyed out and marked already published. Progress appears under Publish progress.

For more control (a different design per variant, or a flat price) publish from Products instead.

Merchant ยท Setup

API keys and webhooks

Create and revoke API keys and manage webhook subscriptions from My Account.

If you or a developer connect your own software to CeePrinto, create a scoped API key and add webhooks from Store Connections โ†’ Developer.

You only need this if you are building your own integration. The Shopify app and CeePrinto Connect get their keys from Get Started and set up webhooks for you.

An API key is a secret password for software. A scope (shown as Permissions) limits what the key may do. A webhook is a message CeePrinto sends to your server when something happens, for example an order ships.

Create an API key

  1. Open API Keys

    Go to Store Connections โ†’ Developer โ†’ API Keys and scroll to Create a key.

  2. Name it

    Type a Name you will recognise later, for example the name of the app that will use it.

  3. Choose permissions

    Tick only the Permissions the software needs (table below). At least one is required.

  4. Create and copy

    Click Create key. The next page shows Your new API key once: This is the only time it will be shown. Store it somewhere safe. Copy it into your software or a password manager.

Permissions
Scope Label on screen In plain words
designs:read Read designs See your saved designs and their mockups.
products:read Read the blank-product catalog See the blanks you can sell and their stock.
listings:read Read store listings See which store products are linked to which designs.
listings:write Create and remove store listings Link and unlink store products and designs; publish.
orders:read Read orders See your orders, payouts and shipping quotes.
orders:write Submit orders Send new orders to CeePrinto.
shops:write Connect and disconnect stores Register a store and disconnect it.
webhooks:write Manage webhook subscriptions Add and remove webhooks.

Your keys

The Your keys table shows Name, Key ID (the public part of the key, safe to share with Support), Permissions, Last used and Status.

Status Meaning Action
Active Working. Revoke
Active (legacy) An old key from before API keys had permissions. It can do everything. None here: Managed on Get Started
Revoked Switched off for good. None

The keys your store apps use appear here too, named Shopify App and WooCommerce Connect.

Revoke a key

Click Revoke on the key. API key revoked. Any integration using it will stop working immediately. This cannot be undone; create a new key if you need access again. Revoke straight away if a key may have leaked.

Add a webhook

  1. Open Webhooks

    Go to Store Connections โ†’ Developer โ†’ Webhooks and find Add a webhook.

  2. Pick a topic

    Choose the Topic (the event) from the list below.

  3. Enter your URL

    Type the Target URL on your server, starting with https://. Click Add webhook.

  4. Copy the signing secret

    The webhook appears under Your webhooks with a Signing secret. Your server uses it to check that a message really came from CeePrinto.

Webhook topics
Topic Sent when
order.status_changed A CeePrinto order changes status.
order.shipped An order moves to Completed with a courier booking. Includes the tracking number.
design.updated One of your designs is saved again.
design.publish_requested You click Publish, so your store app should create or update a product.
product.stock_changed A blank's stock level or in-stock status changes.
product.updated A hub product's default design or overrides change.

Each subscription covers one topic; add one row per topic you need. Click Remove to delete a subscription. Recent deliveries lists the last attempts with When, Topic, URL, Response and Status, which helps when your server is not receiving messages.

The message format, signature check and retry schedule are in the developer docs: API authentication and Webhooks (API).

Product Mapping

Developer โ†’ Product Mapping is the older, advanced screen for mapping products from earlier integrations. New stores do not need it; use Products.

Merchant ยท Selling

Saved Designs

Reuse, publish or delete the designs you saved from the editor.

Saved Designs is your design library: every design you save in the editor lands here, ready to reuse, publish to a store or delete.

Open it from My Account โ†’ Saved Designs. A design is saved when you click Save Design in the editor (you must be logged in). See Saving, downloading and adding to cart for the editor side.

What is saved

A saved design keeps your artwork and text for every print side of the product you designed on, plus a preview picture. Each card shows:

On the card Meaning
Preview A thumbnail of the design. No preview means the picture has not been made yet; the design itself is fine.
Name The name you gave it, or Design #ID if you did not.
Last used on: The blank product the design was made on.
Saved: Date and time of the last save.

The three actions

Button What happens When to use it
Use design Opens the editor on the same product with this design loaded. Edit the artwork, or add it to your cart to order.
Publish to store Opens Store Connections โ†’ Get Started with this design ticked in the Publish step. Put the design on a connected store as a new product.
Delete Asks Delete this saved design? and removes it. Clean up designs you no longer need.

Publish several designs at once

  1. Tick the designs

    Tick the checkbox on each design card you want to publish.

  2. Click Publish to store at the top

    Use the Publish to store button above the grid. Get Started opens with those designs ticked.

  3. Pick the store and publish

    Choose the Target store and click Publish. See Get Started.

Why Delete can be blocked

We never delete artwork that is still needed to print something. You will see one of these messages instead:

Message Reason What to do
This design belongs to one of your orders and cannot be deleted. An order (past or current) uses this design, so we keep the print file. Nothing. The design stays in your library. Rename your next version to tell them apart.
This design is still linked to a store listing. Unlink it under Store Connections โ†’ Listings first. A product on one of your stores still sells this design. Go to Store Connections โ†’ Orders โ†’ Listings, click Unlink on its rows, then delete. Remove the product from your store too, or buyers could order a design we no longer have.

Empty library

If you have no designs yet the page says You have no saved designs yet. Customize a product and click Save Design to add one here. Click Browse products, open a blank, design it and save.

Build hub products with a default design, override it per variant and publish them.

A product on the Products tab is one blank plus a default design, with optional per-variant designs, that you can publish to any connected store and keep changing afterwards.

Open My Account โ†’ Store Connections โ†’ Products. We call these hub products: they live on CeePrinto first, before they exist on any store, so one product can be published to several stores and stay in sync.

Word Meaning
Blank The unprinted item we print on, for example a T-shirt, with its Size and Color options.
Variant One Size and Color combination of the blank, for example Medium / Black.
Default design The design every variant uses unless you say otherwise.
Override A different design given to one or more variants.
Mockup The product photo with your design on it that we render for your store.

Create a product from a blank

  1. Open the create form

    On Products, click Create a product from a blank.

  2. Enter the blank ID

    Type the Blank product ID: the number of the blank on ceeprinto.com (ask Support if you cannot find it). Click Create product.

  3. Result

    The product is created with every variant of the blank and opens straight away with the message to set a default design. It appears in the product list with the columns Product, Blank, Variants and Stores (Not published until you publish). Click Manage to return to it.

If you see This blank has no customize options configured, so the designer cannot open for it. the blank is not set up for designing yet. Pick another blank or contact Support.

Set the default design

At the top of the product, choose a saved design in Default design and click Set default. Every variant marked Inherits default now uses it.

Give some variants their own design

  1. Tick the variants

    In the variant table, tick the rows you want to change, for example all Black sizes.

  2. Pick the design

    Under Assign design to selected, choose a saved design and click Assign. Those rows now say Overrides default and show a dot next to the variant name.

  3. Go back to the default

    To undo an override, tick the rows, choose Inherit default and click Assign.

Each row has an Edit link that opens the editor for that variant with its current design loaded, so you can tweak the artwork.

Publish

The Publish box is at the bottom of the product. It only appears once you have an active store; otherwise it says Connect a store to publish this product.

  1. Choose the store

    Pick the Store.

  2. Choose the pricing

    Pick Pricing (see the table below). For Flat price, type the price in the box next to it.

  3. Publish

    Click Publish. The page shows Publish progress: one line per design with its status and a previews N/M count of mockups rendered so far. The store app picks the job up, creates the product and uploads the images.

Pricing
Pricing option Value sent to the store app Store price
Match blank price blank Each variant starts at the blank's price on CeePrinto. Change it in your store admin afterwards.
Flat price flat Every variant gets the one price you type.

Create, update and link

Mode When it happens Result on your store
create First publish of this product to that store. A new store product with every enabled variant.
update You publish again to a store that already has this product. The existing store product is edited in place; nothing is duplicated.
link Only through the API or a store app, not on this screen. Attaches the design to a product that already exists on your store. See Publishing (API).

Each progress line shows the mode (create or update) and a status.

Publish progress statuses
Status Meaning
pending Waiting for the store app to pick it up.
processing Some designs are done, others are still running.
completed The store confirmed the product was created or updated.
failed The store app reported an error, shown after the status. Fix it and publish again.

What changes after publishing

You do not need to republish to change designs. When you use Set default or Assign on a product that is already on one or more stores, the change goes to every store it is on, and the store apps swap the design for the variants involved. The buyer always gets the design shown in their variant.

Republish (click Publish again, which runs in update mode) when you want the store to refresh its images or prices.

Mockups and when images appear

Mockups are rendered in the background, one per variant and print side, when you publish. The store app waits until they are ready before uploading them. Watch the previews N/M count in Publish progress: when N equals M, all previews are rendered.

Troubleshooting: product published with no images

Check Fix
Publish progress still shows pending or processing. Wait. Large blanks (many sizes and colours) take a few minutes. The page updates itself.
previews N/M has N lower than M for a long time. Rendering is queued. Leave the page and come back later, then publish again so the store picks up the finished images.
Status is failed with a reason. Read the reason after the status, fix it (for example reconnect the store), then publish again.
Images are still missing after a completed publish. Click Publish again to the same store. It runs in update mode and re-uploads. If it still fails, open a ticket in Support with the job number shown as #ID.

Merchant ยท Selling

Connected stores

See, manage and disconnect the stores linked to your account.

The Stores tab lists every store connected to your CeePrinto account and lets you rename, disconnect or reconnect each one.

Open My Account โ†’ Store Connections โ†’ Stores. The table is titled Your connected stores. A store appears here automatically when you paste a connection code into the Shopify app or CeePrinto Connect (see Get Started). If nothing is connected you will see No stores are connected yet. Connect one from your Shopify or WooCommerce app.

The table

Column What it shows
Store The store name, or Unnamed store. You can rename it.
Channel Which kind of store it is, for example shopify or woocommerce.
Identifier The store's own address or ID, for example my-brand.myshopify.com.
Connected The date the store was first connected.
Status Active or Disconnected.

Statuses

Status Publishing to it Counts as connected on Get Started and the Dashboard
Active Yes Yes
Disconnected No. It is left out of every Store and Target store list. No

Actions

Button What it does
Rename Type a name in the New name box and click Rename. Only changes the name you see on CeePrinto.
Disconnect Marks the store Disconnected. Shown on active stores.
Reconnect Marks the store Active again. Shown on disconnected stores.

What Disconnect does and does not do

Thing After Disconnect
Publishing Stops. You cannot publish to the store until you reconnect.
Listings (the links between your designs and the store's products) Kept. They still show under Orders โ†’ Listings.
Order history and Order Activity Kept.
Products already on your store Not touched. We do not delete anything on your store.
The connection code the store app uses Still valid. Disconnect does not switch off the key.

To fully cut a store off, also uninstall the app (or deactivate CeePrinto Connect) on that store, or generate a new code of the same type on Get Started, which switches the old one off. Until then the store app can still send orders.

Moving to a new store

Connect the new store with a fresh code from Get Started, then publish your products to it from Products. Disconnect the old store when you no longer want to publish to it.

Merchant ยท Selling

Cart order options

Ordering for, Ship using, Ship under my brand and Set prices in the cart.

When you order through the ceeprinto.com cart, the Order options box above the cart decides who the order is for, who ships it, and whether it goes out under your brand.

You must be logged in to see Order options. Logged out, the cart shows Please login to your account to view the order options for My Customer and Daraz. Orders that come from your connected store skip the cart; for those see Orders and COD.

Ordering for

Option What happens Choose it when
Myself Ships to your address and is invoiced to you. No cash is collected. Stock, samples, or orders your buyer already paid you for.
My Customer (White-label) We ship straight to your buyer under your brand. The invoice shows your price, never ours. A Customer Price box appears under each item. Dropshipping to a buyer, usually with cash on delivery.

Switching back to Myself clears every Customer Price you entered.

Customer Price and Set prices for all items

With My Customer, type your selling price in each item's Customer Price box. Under it, You pay Rs X for this line. reminds you of your cost, so you can see your margin. The price is for the whole cart line, not per unit.

To price several items in one go, click Set prices for all items. A window titled Ordering For My Customer lists every item with a price box.

Customer Price Result
Blank or 0 on every line Nothing is collected and no COD fee is charged.
Above 0 on at least one line COD is turned on for the order. A COD Fee of Rs 80 is added to your cart, and a COD to collect row shows the total we will collect from your buyer.

The collected amount is paid out to you in full 7 days after delivery. See Payouts.

Ship using

Option What happens Choose it when
Ceeprinto (Easiest) We pick the carrier, book the shipment and handle tracking. Nothing to upload. Most orders, and every COD order you want us to collect for.
My Shipper / Daraz (Cheaper) You ship on your own carrier account at your negotiated rates. Our shipping charge is replaced by a drop-off and handling charge. You already have a courier account or sell on Daraz.

Carrier and shipping label (My Shipper / Daraz)

  1. Pick the carrier at checkout

    Under Your shipping carrier, Select carrier: M&P, TCS, Leopards, TRAX or Daraz (Drop-off).

  2. Book the shipment yourself

    Create the booking in your courier or Daraz account and download the shipping label.

  3. Upload the label

    Click Upload your shipping label: PDF or image, up to 5MB. Tap to choose a file. You cannot place the order without it (Uploading a Shipper Label is required in order to checkout.).

Carrier Note
M&P, TCS, Leopards, TRAX We pack the order, attach your label and hand it to that courier.
Daraz We drop off at Daraz. Set your Daraz drop-off / pickup location to DHA Phase 2, Karachi, Sindh, Pakistan. Watch your email: we tell you when to mark the order Ready to Ship in your Daraz portal. See Read the Daraz FAQ at checkout.

COD payouts start their 7-day hold only when M&P or TRAX tracking confirms delivery. If you ship on your own courier account, that courier collects any cash and settles with you; ask Support before relying on a CeePrinto COD payout for such an order.

Packaging: Ship under my brand

If you have set up a brand, a Packaging option appears: Ship under my brand, followed by your brand name in brackets. Tick it to put your brand icon on the packaging, hangtag and thank-you card for this order.

No brand yet? The cart shows Set up your brand. On My Account โ†’ Brand, enter a Brand name (the name buyers see on the packaging and courier slip) and a Brand icon (PNG, JPG, GIF or WEBP, under 2MB, square works best), then click Save brand.

COD fee

Fee Amount Charged when
COD Fee Rs 80 per order Any My Customer line has a Customer Price above 0, or you tick Collect COD from my customer on a pending order.

Merchant ยท Money

Orders and COD From Customer

Track orders from your stores and how cash on delivery collected from customers is handled.

Orders from your connected stores arrive in Order Activity, become CeePrinto orders in Pending payment, and you decide per order whether we collect cash on delivery (COD) from your buyer before you pay us.

Orders reach CeePrinto in two ways:

Source Where you see it first COD set up
Your connected store (Shopify app, CeePrinto Connect or the API) Store Connections โ†’ Orders โ†’ Order Activity, then My Account โ†’ Orders On the order page, with Collect COD from my customer
You, in the ceeprinto.com cart My Account โ†’ Orders In the cart, with Ordering for set to My Customer. See Cart order options.

Order Activity

Every order your store sends is first recorded as an intake (an order we have received but not yet turned into a CeePrinto order). Order Activity lists them with the columns Received, External order (your store's order number), Status, WC order (a link to the CeePrinto order) and Detail.

Order Activity statuses
Status Meaning What to do
Received The order arrived and is queued. Nothing. It is processed within a minute or two.
Processing We are matching its items to your products. Nothing.
Created A CeePrinto order was made. The WC order column links to it. Open the order and set COD (below).
Failed No order was made. Detail says why. Fix the cause, then click Retry.
Ignored Reserved for orders we skip on purpose. Current versions do not set it. Contact Support if you see it.

Failure reasons and fixes

Detail text starts with Cause Fix
No listing, design or legacy product matched this line item The store product the buyer ordered is not linked to a CeePrinto design. Publish that product from Products (or link it in your store app), check it under Listings, then Retry.
WooCommerce product ... no longer exists. The blank behind the listing was removed from CeePrinto. Republish the product on a current blank, then Retry.
Order has no line items. Your store sent an empty order. Check the order in your store; nothing to retry.
A line item could not be added to the order. A temporary error. Click Retry.

If any item in an order cannot be matched, no CeePrinto order is created at all, so a successful Retry always produces one complete order. After Retry you see Order re-processed successfully. or Retry failed. Check the reason and try again after fixing it.

Fulfillment Profile

Store orders copy your billing details. Orders โ†’ Fulfillment Profile checks four fields and marks each Set or Missing: Phone, Address line 1, City, Country. Orders are still created when one is missing, but with an incomplete address. Click Edit billing address to fix it.

Order statuses

Status Meaning
Pending payment Created and waiting for you. Store orders always start here. This is the only status in which you can change COD.
Processing Paid and accepted. We are preparing it.
On hold Paused, usually waiting for a payment check or for information from you.
Printing In production (CeePrinto status).
Completed Printed and handed to the courier. COD payouts only count completed orders.
Returned From Customer The buyer refused or returned the parcel (CeePrinto status). It appears under Returns.
Re Shipped A returned or failed parcel was sent out again (CeePrinto status).
Cancelled, Refunded, Failed Standard WooCommerce outcomes: stopped, money returned, or payment failed.

The Dashboard panel Orders waiting on you lists orders that are pending, processing or on hold.

COD From Customer, step by step

COD From Customer means our courier collects cash from your buyer on delivery and we pay that cash out to you. You pay us the order total (print, shipping and fees) as usual.

  1. Open the order

    Go to My Account โ†’ Orders and open the order in Pending payment (or click its number in Order Activity). The Cash on delivery panel is at the top.

  2. Turn COD on

    Tick Collect COD from my customer. A COD fee of Rs 80 is added to what you pay us.

  3. Check the amounts

    Products total is filled in for you: the sum of the buyer prices your store sent for each item. Optionally type your own delivery or handling charge in Additional charges. Total to collect is what the courier collects from your buyer.

  4. Save

    Click Save COD settings. The order total updates to include the COD fee.

  5. Pay the order

    Pay the order total from My Account โ†’ Orders. Once paid, the order leaves Pending payment and COD can no longer be changed.

Untick Collect COD from my customer and save if your buyer already paid you online. The COD fee is removed and nothing is collected.

Worked example (illustrative prices)

Line Amount (PKR) Who pays whom
Buyer price for 1 T-shirt (sent by your store) 2,500 Buyer โ†’ courier
Additional charges you added 150 Buyer โ†’ courier
Total to collect 2,650 Collected on delivery, paid out to you
Print and blank cost (example) 1,200 You โ†’ CeePrinto
Shipping (example) 250 You โ†’ CeePrinto
COD fee 80 You โ†’ CeePrinto
Order total you pay 1,530 You โ†’ CeePrinto
Your margin (2,650 โˆ’ 1,530) 1,120

The 2,650 appears on Pay Out and is transferred to your bank 7 days after delivery. Print, blank and shipping prices depend on the product; only the Rs 80 COD fee is fixed.

Tracking

When we book the shipment you get an email with the tracking number and a tracking link. Store apps subscribed to the order.shipped webhook receive the tracking number too and can mark the order shipped on your store. On Pay Out, the Delivered column shows the courier status until delivery is confirmed.

Returns

If a buyer refuses or returns a parcel, the order moves to Returned From Customer and My Account โ†’ Returns appears. Each return shows a Return date and a Keep until date. Returned parcels are discarded or sold if you do not arrange shipment to yourself within 1 month of the return date. Returned orders are not delivered, so nothing is collected or paid out for them.

Merchant ยท Money

Payouts

How payouts are calculated, their statuses and when you get paid.

CeePrinto pays you the full cash collected from your buyers on COD orders, 7 days after the courier confirms delivery, into the bank account saved on My Account โ†’ Pay Out.

Open My Account โ†’ Pay Out. The page header reads Track COD collections and payouts. Funds release 7 days after delivery.

How the payout amount is worked out

You pay CeePrinto the order total (print, blank, shipping and the Rs 80 COD fee) when you pay the order. Nothing is deducted later. The payout is the cash the courier collected from your buyer:

Formula
Payout per order = sum of the Customer Price on each item
                 + Additional charges you entered on the order

Only orders with COD turned on (Collect COD from my customer, or My Customer with a price in the cart) and in status Completed appear on Pay Out. See the worked example.

How the money flows

The How COD payouts work panel sums it up:

  1. Your customer pays cash on delivery
  2. We hold the amount for 7 days after delivery
  3. Then we transfer it to your bank account

The 7-day hold starts on the day the courier (M&P or TRAX) reports the parcel delivered, not on the day it was shipped.

Payout statuses

Checked in this order: Paid out, Missing bank details, Awaiting delivery, 7-day hold, Ready for payout
Status shown Code Meaning What to do
Missing bank details missing_bank No bank account is saved, so we cannot pay. Checked before anything else. Save your bank details (below).
Awaiting delivery awaiting_delivery The courier has not confirmed delivery yet. The Delivered column shows the latest courier status. Wait. If it stays here long after the buyer received it, open a ticket in Support.
7-day hold in_hold Delivered; the 7-day hold is running. Shows Pays in N days (date). Nothing.
Ready for payout ready Hold finished; in the next transfer. Shows Eligible since date. Nothing.
Paid out processed Transferred. Paid on shows the date. Check your bank statement.

The Status filter also has All payouts and Not paid yet (every order that is not Paid out).

The Pay Out page

Part What it shows
Summary cards Total COD collected, Still owed, Already paid out, Ready for payout and 7-day hold, each with an amount and an order count.
Next payouts Up to 5 orders closest to their payout date: Eligible now or the date they pay.
All orders Columns Order, Amount, Delivered, Payout due, Status, Paid on. Filter by Status, From and To dates, then Apply or Clear.
Bank details Where you save the account we pay into.

Add or change bank details

  1. Choose your bank

    In Bank details, pick your bank in Bank name. Wallets such as SadaPay and NayaPay are in the list.

  2. Enter the account number

    Type it in Account number. If you use SadaPay, enter your IBAN. Double-check it: wrong details delay payments.

  3. Save

    Click Save bank details. You see Bank details saved. Orders that were Missing bank details move to their real status straight away.

Until bank details are saved, Pay Out shows Missing payout information! and the Dashboard shows the Add your bank details alert.

Export

There is no export button on your Pay Out page. The CSV and XLSX bank transfer file is produced by the CeePrinto team when sending payouts. To keep your own records, filter All orders by Paid out and a date range, or ask Support for a statement.

Common questions

Question Answer
Why is my order not on Pay Out? It is not Completed yet, or COD was not turned on for it. Orders your buyer paid online never appear.
Why is the payout bigger than my profit? Because it is the whole cash collected. Your costs were paid when you paid the order.
The buyer refused the parcel. Do I get anything? No cash was collected, so there is no payout. The order moves to Returned From Customer. See Returns.
Can I change the COD amount after paying? No. COD can only be changed while the order is Pending payment.
How long from delivery to money? 7 days on hold, then the order is Ready for payout and included in the next transfer.

Merchant ยท Money

FAQ

Answers to common merchant questions.

Short answers to the questions merchants ask most, each with a link to the full explanation.

How do I connect my Shopify store?

Install the app from Store Connections โ†’ Get Started, click Generate Shopify Code and paste the cp1. code into the app Settings. Get Started

I closed the page before copying my connection code. What now?

Codes are shown once. Click the same generate button again; the new code replaces the old one, so paste it into your store app right away. Get Started

My store app stopped working after I generated a new code. Why?

Generating a code switches off the previous one of the same type. Paste the newest code into the store app. Get Started

Can I use a different design on some sizes or colours?

Yes. On Products, tick those variants, choose a design under Assign design to selected and click Assign. Products

Do I need to republish after changing a design on a product?

No. Changing the default design or a variant override updates every store the product is already on. Products

My product appeared on my store without pictures. What should I do?

Mockups render in the background. Wait until previews N/M is complete in Publish progress, then click Publish again to refresh the images. Products

Why can't I delete a saved design?

It is used by an order, or still linked to a store listing. Unlink it under Listings first; designs used by orders are kept for good. Saved Designs

An order from my store is not in my Orders. Where is it?

Check Order Activity. If it says Failed, read the Detail, fix it (usually a missing listing) and click Retry. Orders and COD

How do I make CeePrinto collect cash from my buyer?

Open the Pending payment order, tick Collect COD from my customer, click Save COD settings, then pay the order. Orders and COD

How much is the COD fee?

Rs 80 per order, added to what you pay us when COD is on. Cart order options

Can I add my own delivery charge for the buyer?

Yes. Enter it in Additional charges on the order; it is collected and paid out with the rest. Orders and COD

When do I get paid?

7 days after the courier confirms delivery the order becomes Ready for payout and is included in the next transfer. Payouts

Why does my payout say Missing bank details?

No bank account is saved. Add it under Bank details on Pay Out and click Save bank details. Payouts

Is anything deducted from my payout?

No. You pay print, shipping and the COD fee when you pay the order; the payout is the full cash collected. Payouts

An order has been Awaiting delivery for weeks. What should I do?

The courier has not confirmed delivery, so the hold never started. Open a ticket in Support with the order number. Payouts

Can orders ship in my own brand's packaging?

Yes. Save a brand on My Account โ†’ Brand, then tick Ship under my brand in the cart. Cart order options

Can I use my own courier or Daraz?

Choose My Shipper / Daraz in the cart, pick the carrier at checkout and upload your shipping label. Cart order options

What happens to a parcel my buyer refused?

It moves to Returned From Customer and appears under Returns. Arrange shipment to yourself within 1 month of the return date or it is discarded or sold. Orders and COD

What happens if I disconnect a store?

You can no longer publish to it; listings and history are kept and nothing is deleted on your store. Regenerate the code to stop the app sending orders too. Connected stores

I want my developer to use the API. Where are the keys?

Create a scoped key on Developer โ†’ API Keys; it is shown once. API keys and webhooks

Still stuck? Open a ticket from My Account โ†’ Support, or use Get help with this order on the order page.