# Printeers API documentation > Developer documentation for the Printeers partner API, a REST API for selling print-on-demand phone cases and tech accessories: product catalog and stock, mockup rendering, order creation and tracking, claims and webhooks. --- # Getting Started Canonical URL: https://api-docs.test-printeers.com/guides/getting-started Learn how to set up your integration with the Printeers API. ## Prerequisites Before you begin, you need: - A Printeers partner account. [Create an account](https://dashboard.printeers.com/signup) if you don't have one. - A V2 API secret key, generated in the [Printeers Dashboard](https://dashboard.printeers.com) under API Settings. ## Environments Printeers provides two environments: | Environment | Base URL | Purpose | |-------------|----------|---------| | Test | `https://api.test-printeers.com/v2` | Development and testing. Orders are not produced or invoiced. | | Production | `https://api.printeers.com/v2` | Live orders. Created orders are produced, delivered, and billed. | Always develop and test your integration against the test environment first. ## Authentication All API requests require authentication via the `X-Printeers-Secret-Key` header: ``` X-Printeers-Secret-Key: YOUR_SECRET_KEY ``` Keep your secret key secure and never expose it in client-side code. ## Making Your First Request Validate your credentials by calling the auth endpoint: ```bash curl -i -X POST https://api.test-printeers.com/v2/auth/validate \ -H "X-Printeers-Secret-Key: YOUR_SECRET_KEY" ``` A successful response confirms your credentials are valid: ```json { "message": "Authentication is valid" } ``` If authentication fails, you'll receive a `401 Unauthorized` response with an error message. ## Exploring the API Once authenticated, you can explore available resources: - **Catalog**: `GET /products` and `GET /packagings` to browse available products and packagings. - **Renders**: `POST /renders` to create product mockups. - **Orders**: `GET /orders/{orderReference}` to retrieve order details. - **Shipments**: `GET /shipments/{shipmentReference}` for tracking information. See the [Shipments and Tracking](https://api-docs.test-printeers.com/guides/shipments-and-tracking.md) guide. See the [API Reference](https://api-docs.test-printeers.com/reference/v2) for complete endpoint documentation. ## Error Handling The API uses standard HTTP status codes: | Code | Meaning | |------|---------| | `200` | Success | | `400` | Bad request. Check your request data. | | `401` | Unauthorized. Invalid or missing secret key. | | `403` | Forbidden. Valid credentials but insufficient permissions. | | `404` | Not found. Resource doesn't exist. | | `501` | Not implemented. Endpoint not yet available. | | `5xx` | Server error. Retry later. | Error responses include a JSON body with details: ```json { "code": "invalid_request", "message": "Description of what went wrong" } ``` ## Using AI coding agents If an AI coding assistant (Claude Code, Cursor, Copilot or an agent framework) builds your integration, point it at the machine-readable sources instead of the HTML pages: - [llms.txt](https://api-docs.test-printeers.com/llms.txt) indexes this documentation and links to the markdown version of every guide; [llms-full.txt](https://api-docs.test-printeers.com/llms-full.txt) is the full text of all guides in one file. Any guide is also available as markdown by appending `.md` to its URL. - The [OpenAPI specification of the v2 API](https://api-docs.test-printeers.com/static/printeers-api-v2.openapi.json) is the source of truth for endpoints, parameters and schemas. A starting point for your agent's project instructions (`AGENTS.md`, `CLAUDE.md` or similar): ```text Printeers API: read https://api-docs.printeers.com/llms.txt first. The OpenAPI specification at https://api-docs.printeers.com/static/printeers-api-v2.openapi.json is the source of truth for endpoints and schemas. Develop and test against the test environment, https://api.test-printeers.com/v2, where orders are never produced or invoiced; switch to https://api.printeers.com/v2 only when explicitly asked. Authenticate with the X-Printeers-Secret-Key header and never commit the key. Set a unique customIdentifier on every order you create so a retried request cannot create a duplicate order. ``` ## Next Steps - Read the [Organization Structure](https://api-docs.test-printeers.com/guides/organization-structure.md) guide to understand accounts, stores, and brands. - Browse the [API Reference](https://api-docs.test-printeers.com/reference/v2) for detailed endpoint documentation. - Read the [API Versioning](https://api-docs.test-printeers.com/guides/api-versioning.md) guide to understand our release cycle. - Learn how to [paginate through large result sets](https://api-docs.test-printeers.com/guides/pagination.md). --- # Organization Structure Canonical URL: https://api-docs.test-printeers.com/guides/organization-structure Learn how your organization is structured with accounts, stores, and brands. ## Overview Printeers uses a hierarchical structure to organize your business: ``` Organization ├── Account │ ├── Store │ │ └── Orders │ └── Store │ └── Orders └── Account └── Store └── Orders Brands (organization-wide) ``` ## Organization An **Organization** is the top-level entity representing your company. Your API secret key is scoped to the organization, giving you access to all accounts and stores within it. Retrieve your organization details: ```bash curl -i https://api.test-printeers.com/v2/organization \ -H "X-Printeers-Secret-Key: YOUR_SECRET_KEY" ``` ## Accounts An **Account** represents a billing entity within your organization. Each account: - Has its own balance and invoicing. - Can have zero or more stores attached. - Can have different payment terms. - Can be tied to a different legal entity and billing address. List all accounts in your organization: ```bash curl -i https://api.test-printeers.com/v2/accounts \ -H "X-Printeers-Secret-Key: YOUR_SECRET_KEY" ``` Most organizations have a single account, but larger organizations may use multiple accounts to separate billing for different business units or regions. ## Stores A **Store** is where orders are created. Each store: - Is attached to exactly one account for billing purposes. - Has an integration type (`api`, `shopify`, etc.). - Can only create orders via API if its integration type is `api`. - Can be moved to a different account if needed. List all stores: ```bash curl -i https://api.test-printeers.com/v2/stores \ -H "X-Printeers-Secret-Key: YOUR_SECRET_KEY" ``` When an order is created, the products and shipping costs are billed to the account the store is attached to. ## Brands A **Brand** represents a customer-facing identity used on shipping labels and tracking website. Brands are organization-wide and can be used across any store. Each store has a default brand that is used when an order doesn't specify one. Each brand includes: - Brand name - Logo - Primary and secondary colors - Customer service contact information List all brands: ```bash curl -i https://api.test-printeers.com/v2/brands \ -H "X-Printeers-Secret-Key: YOUR_SECRET_KEY" ``` When creating an order, you can specify a brand to customize the shipment tracking page for your customers. ## Orders An **Order** contains one or more orderlines, each representing a product to be printed (or simply picked) and shipped. Orders: - Belong to a store (and by extension, an account). - Can optionally use a brand for white-label fulfillment. - Are shipped as one shipment. Retrieve order details: ```bash curl -i https://api.test-printeers.com/v2/orders/ORDER_REFERENCE \ -H "X-Printeers-Secret-Key: YOUR_SECRET_KEY" ``` ## References All entities use unique reference strings in the format `Entity_xxxx`: - `Account_xyz789...` - `Store_def456...` - `Brand_gh1012...` - `Order_jkl345...` References are: - **Globally unique**: No two entities share the same reference, even across different organizations or environments. A reference from the test environment will never collide with one from production. - **Stable**: Once assigned, a reference never changes. You can safely store references in your system for later use. - **Future-proof**: References introduced in V2 will continue to work in future major versions (V3 and beyond). ## Managing Your Organization Organization structure (accounts, stores, brands) is managed through the [Printeers Dashboard](https://dashboard.printeers.com), not via the API. The API provides read-only access to this information. --- # Shipments and Tracking Canonical URL: https://api-docs.test-printeers.com/guides/shipments-and-tracking Understand how orders become shipments and what tracking information is available. ## Orders and Shipments A **Shipment** represents a physical package sent by Printeers. How orders relate to shipments as well as the destination of the shipment depends on the requested shipment kind: ### Dropship Orders For orders with shipment kind 'dropship', each order results in exactly one shipment sent directly to your customer. The relationship is one-to-one: ``` Order A → Shipment 1 → Customer A Order B → Shipment 2 → Customer B ``` ### Combined Orders For orders with shipment kind 'combined', multiple orders going to the same address are consolidated into a single shipment: ``` Order A ─┐ Order B ─┼→ Shipment 1 → Your Warehouse Order C ─┘ ``` This is typically used when you handle final distribution yourself. ## Shipping Levels Printeers offers three shipping levels, each with different tracking capabilities: | Level | Tracking | |-------|----------| | Basic | No | | Tracked | Yes | | Express | Yes | ### Basic The most economical option. No tracking information is available. You receive a shipment reference when the package is dispatched, but cannot track its journey. ### Tracked Standard tracked shipping. Full tracking information is available, including: - Carrier name and tracking number. - Tracking URL for a white-label tracking page branded with your [Brand](https://api-docs.test-printeers.com/guides/organization-structure.md#brands). This page is available immediately, unlike carrier tracking which can take a while to update. - Delivery confirmation when the package arrives. ### Express Priority shipping with faster delivery times. Provides the same tracking information as Tracked shipping. [More information on shipping levels, costs and delivery times.](https://printeers.com/shipping-delivery-times/) ## Retrieving Shipment Information Get shipment details using the shipment reference: ```bash curl -i https://api.test-printeers.com/v2/shipments/SHIPMENT_REFERENCE \ -H "X-Printeers-Secret-Key: YOUR_SECRET_KEY" ``` The response includes: - `reference`: The shipment reference (`Shipment_xxx...`). - `orders`: List of orders included in this shipment. For Tracked and Express shipments, you also receive: - `carrier`: The shipping carrier name. - `trackingNumber`: The carrier's tracking number. - `trackingUrl`: Link to the white-label tracking page. --- # API Versioning Canonical URL: https://api-docs.test-printeers.com/guides/api-versioning Understand how Printeers versions the API, the release cycle, and what to expect when upgrading. The Printeers API follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Beta preview releases use the `-beta` suffix (e.g., `2.0.0-beta.1`). ## Selecting a Version The API version is specified in the URL path: - V2 (stable): `https://api.printeers.com/v2/...` recommended for new integrations. - V1 (legacy): `https://api.printeers.com/v1/...` maintained during the deprecation period. Each major version has its own base path. Minor and patch versions are applied automatically: you always get the latest release within your chosen major version. ## Version Lifecycle The following diagram illustrates how API versions progress through their lifecycle: ![Version Lifecycle](https://api-docs.test-printeers.com/assets/version-lifecycle.svg) ### Beta Preview New major versions are first released as a public beta preview. During the beta phase: 1. The current stable version becomes **frozen** (no new features). 1. New features are added only to the beta preview. 1. Bug fixes and security patches continue for the frozen version. ### Stable Once a beta preview is declared stable (the `-beta` suffix is removed), new non-breaking features are added through minor version increments. Breaking changes are reserved for the next major version. ### Forward Compatibility Within a Major Version The OpenAPI spec is designed so that you can keep using a version of the spec you downloaded last month, last quarter, or last year against the live server, as long as you stay within the same major version. The spec uses two OpenAPI conventions to make this possible: - **Response schemas are open.** They follow OpenAPI's default of allowing undocumented additional properties (`additionalProperties: true`). The server may add new fields to a response in a minor version, and a client that generated its types from an older spec will simply ignore those fields when decoding. No client update is required. - **Request schemas are strict.** They set `additionalProperties: false`. The server rejects undocumented fields in request bodies. This catches typos (`customIdentfier` instead of `customIdentifier`) and obsolete fields you forgot to remove, at the API boundary instead of silently misbehaving. This split, "open in what you accept, strict in what you require", is what keeps your integration stable across non-breaking server changes. To benefit from it, your client must tolerate the changes listed below without treating them as breaking. #### Non-breaking changes you must tolerate Within a single major version, we may make any of the following additive changes. None of them are considered breaking, and we may ship them in any minor or patch release: - **New response fields.** We may add new fields to any response body. Your decoder must ignore unknown fields rather than rejecting them. Most JSON libraries do this by default; some "strict" modes need to be turned off. - **New optional request fields.** We may accept new optional fields on existing endpoints. Existing clients that do not send them continue to work unchanged. - **New endpoints.** We may add new endpoints under the same major-version base path. - **New values for already-extensible enums.** Each enum in the spec is marked as either **strict** (fixed set; we will not add values within a major version) or **open** (designed to grow; new values may appear without notice). Examples of open enums: `Claimcategory` references (always fetch dynamically), `Store.integration` (documented as free-form), and the `code` field in error responses. Examples of strict enums: `OrderStatusCode`, `RenderStatus`, `ShippingLevel`, `ShipmentKind`. For open enums your client must handle unknown values gracefully, typically by treating them as a generic fallback or by passing the raw string through to your UI. - **New error codes.** We may add new `code` values for existing HTTP status responses on existing endpoints. - **Relaxed validation.** A field may become accepted in more situations than before (e.g. wider character set, longer max length). If your client tolerates the above, it remains compatible across all minor and patch releases of a major version, even if you never re-download the spec. Some operational details may also change without being considered a breaking change: - **Rate limits.** The set of rate-limit policies, their quotas, refill windows, and which endpoints consume from which buckets may all change as traffic patterns evolve. Your integration should always rely on the values returned in the `X-RateLimit-Policy` and `X-RateLimit` response headers, never on hard-coded numbers. See the [Rate Limiting guide](https://api-docs.test-printeers.com/guides/rate-limiting.md). ### Deprecation When a new major version becomes stable, the previous major version enters a **one-year deprecation period** before being retired. During this period: - The deprecated version continues to function normally. - Security patches are applied as needed. - No new features or non-critical bug fixes are made. ## Release Cadence V1 maintained backwards compatibility for nearly 10 years. While we're proud of that stability, it resulted in a larger gap between V1 and V2. Going forward, we may release new major versions occasionally; roughly once per year at most. When we do, each release will contain only a few breaking changes, keeping upgrades simple. Combined with the one-year deprecation period, you'll always have plenty of time to migrate. ## Changelog Each version has a changelog documenting all changes. For major versions, the changelog includes migration information: - Breaking changes and how to address them. - Deprecated endpoints and their replacements. - New required fields or changed data formats. Review the changelog before upgrading to understand what has changed and what actions you need to take. ## Staying Informed We notify API users about upcoming changes via email. Major releases, deprecation notices, and security advisories are sent to the email addresses configured in your organization. ## Beta Feedback We encourage API users to test beta versions before they become stable. If you encounter issues or have feedback: - Report bugs via [support@printeers.com](mailto:support@printeers.com). - Include your organization name and the beta version number in your report. --- # Pagination Canonical URL: https://api-docs.test-printeers.com/guides/pagination Learn how to paginate through large result sets using cursors. ## Overview Endpoints that return lists of resources use cursor-based pagination. Instead of page numbers, you receive an opaque cursor string that points to your position in the result set. ## How It Works When a response contains more results, it includes a `nextCursor` field. The list itself is wrapped under a key named after the resource (`orders`, `claims`, `images`, ...): ```json { "orders": [...], "nextCursor": "c1AAYa6zbXmgBhFNVccOVcnA" } ``` To fetch the next page, pass this cursor as a query parameter: ``` GET /v2/orders?cursor=c1AAYa6zbXmgBhFNVccOVcnA ``` When there are no more results, `nextCursor` is `null`: ```json { "orders": [...], "nextCursor": null } ``` ## Page Size Each paginated endpoint specifies its own page size. Some endpoints use a fixed size, others accept a user-specified limit. See the documentation for each endpoint. ## When nextCursor Is Present The response includes `nextCursor` whenever the number of returned items equals the page size. This means there may be more results. When the number of items is less than the page size, `nextCursor` is `null` and you've reached the end. Note that the final page may be empty. This happens when the total number of items is exactly a multiple of the page size. An empty page with `nextCursor: null` is a normal end-of-results signal. ## Cursor Rules Cursors are opaque strings. Your integration should: - **Treat cursors as arbitrary strings**: Do not parse, decode, or interpret the cursor value. The format may change without notice. - **Do not store cursors**: Fetch all pages in a single session. Do not persist cursors for later use. - **Do not reuse previous cursors**: Once you've moved to the next page, discard earlier cursors. To re-fetch a list, start from the beginning without a cursor. - **Never construct cursors**: Only use cursor values returned by the API. ## Example: Fetching All Orders ```python def fetch_all_orders(): orders = [] cursor = None while True: url = "/v2/orders" if cursor: url += f"?cursor={cursor}" response = api_request(url) orders.extend(response["orders"]) cursor = response.get("nextCursor") if cursor is None: break return orders ``` ## Empty Results A response with zero items is normal in two cases: - **No matches**: Your query filters excluded all resources. - **Final page**: The total count was a multiple of the page size (see above). ## Ordering Paginated endpoints return results in a fixed order (typically newest first by creation date). The ordering is not configurable. ## Where this is used Cursor pagination applies to: `GET /orders`, `GET /claims`, `GET /products`, `GET /images`, and `GET /items`. See each endpoint in the [API Reference](https://api-docs.test-printeers.com/reference/v2) for endpoint-specific filters and the default sort field. --- # Items, Products, and SKUs Canonical URL: https://api-docs.test-printeers.com/guides/items-products-and-skus Understand how physical stock items relate to orderable products, how SKUs work, and how to migrate from SKU-based to reference-based integrations. ## V1 "Items" Are V2 "Products" and "Items" In V1, the term **item** covers both the orderable catalog entry and the physical stock unit in the warehouse, with no distinction. In V2, these concepts are split: the orderable catalog entry is called a **product**, and the physical stock unit is called an **item**. So a V1 "item" maps to both a V2 "product" and a V2 "item". When V1 refers to an "item" with an "item SKU" (e.g. in `POST /order`, `GET /stock`, or `POST /quote`), it is talking about the same orderable catalog entry that V2 calls a "product". V2 items (physical warehouse stock) are only relevant if you store your own stock at Printeers (e.g. custom packaging or pre-purchased components). For public products where Printeers handles purchasing, items are not visible. You only interact with **products**. If you do store your own stock, the V2 `GET /items` endpoint returns your warehouse items. Each item has its own `reference` (`Item_...`) and `sku` (e.g. `PART0037`), along with `logicalQuantity`, `physicalQuantity`, and a list of `products` linked to it. Note that the item SKU (`PART0037`) is the warehouse identifier. It is not the same as the suffixed legacy item SKUs (`PART0037P001`) that identify individual products. ``` V1 "item" (SKU: PART0037) ═══ V2 "product" (Product_4j8k...) │ └── linked to warehouse item PART0037 ``` Multiple products can share the same warehouse item. For example, a matte-printed and a glossy-printed version of the same Luggage Tag are different products (different print techniques, different productgroups) but both use the same physical blank from the warehouse. ``` Warehouse item: PART0037 (Luggage Tag) ├── Product: Luggage Tag (matte print) legacyItemSKU: PART0037 └── Product: Luggage Tag (glossy print) legacyItemSKU: PART0037P001 ``` ## Product References vs SKUs The V2 API identifies products by **reference**: a unique, opaque identifier like `Product_4j8k2m5n7p9q1r3s6t8v0w2x4y`. References are stable and globally unique. The V1 API identifies products by **SKU** (e.g. `PART0037`). These SKUs are carried forward in V2 as the `legacyItemSKU` field on products, allowing you to match V1 SKUs to V2 product references during migration. For products created before 2026-03-22, the legacy item SKU is identical to the warehouse item's SKU (e.g. `PART0037`). Since 2026-03-22, new products receive a suffixed legacy item SKU (e.g. `PART0037P001`) to keep them unique across products that share the same warehouse item. | Concept | V1 Item | V2 Product | V2 Item | |---------|----|----|----| | Orderable catalog entry | ✓ | ✓ | × | | Physical warehouse stock | ✓ | × | ✓ | | SKU (`PART0037`, `PART0037P001`) | `sku` | `legacyItemSKU` | × | | V2 Item-only SKU (`PART0037`) | × | × | `sku` | | Product Reference | `v2_product_reference` | `productReference` | × | | Item Reference | × | × | `itemReference` | ## Backward Compatibility for V1 The V1 API continues to work as before. All V1 endpoints accept and return SKUs. Existing integrations do not need any changes. Remember that V1 calls products "items". The `item_sku` field in V1 is the same value as `legacyItemSKU` in V2: - **`GET /stock`**: Returns products (called "items" in V1) with their `sku` field. - **`POST /quote`**: Accepts `item_sku` to look up product pricing. - **`POST /order`**: Accepts `item_sku` in orderlines to identify the product. - **`GET /order/{id}`**: Returns `item_sku` on each orderline. Existing products keep their original SKU values. If you create orders using SKUs you already have, they continue to work without modification. ## Migrating from V1 to V2 When migrating to V2, replace SKU-based product lookups with product references. ### Step 1: Look Up Product References Use the V2 lookup endpoint to translate a legacy SKU to a product reference: ``` GET /v2/utilities/lookup-product-reference-by-legacy-sku?legacyItemSku=PART0037 ``` ```json { "productReference": "Product_4j8k2m5n7p9q1r3s6t8v0w2x4y" } ``` ### Step 2: Use Product References for Orders In V2, orders use `productReference` instead of `item_sku`: ```json { "order": { "storeReference": "Store_9jmn3dewwvy13rv12k7aannqzy", "shippingKind": "dropship", "dropshipAddress": { "..." : "..." }, "orderlines": [ { "productReference": "Product_4j8k2m5n7p9q1r3s6t8v0w2x4y", "imageReference": "Image_pdcptbw3k48sv03wy338bb8jzm", "quantity": 1 } ] } } ``` ### Step 3: Update Your Product Catalog Instead of storing SKUs, store product references from the V2 `GET /products` endpoint. Each product in the response includes both `reference` (the product reference) and `legacyItemSKU` (the V1 SKU), so you can match them to your existing SKU-based records during migration. ## Deprecation Timeline The legacy SKU lookup endpoint and the `legacySku` field on orderlines are provided to ease the V1-to-V2 transition. They will be removed in V3. We recommend migrating to product references as soon as practical. --- # Products Canonical URL: https://api-docs.test-printeers.com/guides/products Learn how products are organized and priced. ## Productgroups Products are organized into **productgroups**. A productgroup represents a category of similar products that share the same pricing and production technique. For example, a productgroup might be "Soft Cases (back printed, transparent)" which contains individual products for each compatible device model: iPhone 15, iPhone 15 Pro, Samsung Galaxy S24, and so on. ```txt Productgroup: Soft Cases (back printed, transparent) ├── iPhone 15 Soft Case ├── iPhone 15 Pro Soft Case ├── iPhone 15 Pro Max Soft Case ├── Samsung Galaxy S24 Soft Case └── ... ``` ## Pricing Prices are set at the **productgroup level**, not per individual product. All products within a productgroup share the same price. This means when you query the price for any product, you're effectively getting the price of its productgroup. If pricing changes, it changes for all products in that group simultaneously. Some productgroups may be **private** to your organization. These contain custom products created specifically for you, with pricing agreed upon separately. ## Lifecycle Every product follows a defined lifecycle. The lifecycle determines whether a product is visible to your organization and whether it can be ordered. ``` announced → prelaunched → launched → sunsetted → ended → archived ``` | Stage | Description | |-------|-------------| | `announced` | Product is visible in the catalog. Print specifications may still be incomplete. | | `prelaunched` | Product can be ordered. Orders are queued as pre-orders until launch. | | `launched` | Product is fully available for production and shipping. | | `sunsetted` | Product is being phased out. Still orderable, but consider alternatives. | | `ended` | Product is no longer orderable. Existing orders will be fulfilled. | | `archived` | Product has been removed from the catalog entirely. | ## Syncing your catalog When you mirror our catalog into your own system, page through `GET /products` to pick up `name`, `status` and `availableQuantity` changes. Stock levels change often, so you will typically sync the full list regularly anyway. To avoid re-fetching detail you already have, each product in the list carries two timestamps: | Field | Bumped when | |-------|-------------| | `layersModified` | The product's print layers (the layer image assets) change. | | `printModified` | The product's print dimensions change. | Store these alongside each product on your side. On a later sync, only call `GET /products/{productReference}` (and re-download the layer assets) for products where the timestamp moved past what you stored. A product whose timestamps are unchanged needs no detail fetch. The detail response mirrors these as `layers.modified` and `print.modified`. The layer image URLs also carry `ETag` and `Last-Modified` headers and answer `304 Not Modified` to a matching `If-None-Match` or `If-Modified-Since`, so an HTTP client or proxy that caches the images revalidates them without re-downloading. These timestamps cover product configuration only. Live stock is not reflected here; read `availableQuantity` from the list on each sync. Product media is not covered by these timestamps. The `media` array on `GET /products/{productReference}` can change whenever an example image is uploaded or an example render is replaced, so read it whenever you fetch the detail. The downloads at `GET /products/{productReference}/media/{filename}` carry cache headers instead: a render is immutable and sends `Cache-Control: private, max-age=31536000, immutable`, an example image sends `ETag` and `Last-Modified` so you can compare them with the copy you stored and skip files that did not change. --- # Creating Orders Canonical URL: https://api-docs.test-printeers.com/guides/creating-orders Learn how to create orders through the API, including image uploads, shipping options, and address handling. ## Overview Creating an order is a two-step process: upload print images, then place the order. The flow looks like this: ``` Upload print image (JPEG/PNG) POST /images │ Browse catalog │ GET /products │ │ │ ▼ ▼ Product_xxx + Image_xxx + quantity POST /orders │ ▼ Order_xxx │ ┌────────────┴────────────┐ ▼ ▼ "dropship" "combined" Ships to customer Ships to your location ``` ## Images Print products require a print image. Upload each image as a `POST /images` request with the raw image bytes in the body. ```bash curl -X POST https://api.printeers.com/v2/images \ -H "X-Printeers-Secret-Key: $KEY" \ -H "Content-Type: application/octet-stream" \ --data-binary @design.png ``` ```json { "image": { "reference": "Image_pdcptbw3k48sv03wy338bb8jzm" } } ``` Accepted formats are JPEG and PNG. Images larger than 7000x7000 pixels are automatically downsized. Uploading the same image twice returns the same reference. Image uploads count against the `uploads` rate-limit bucket; see the [Rate Limiting](https://api-docs.test-printeers.com/guides/rate-limiting.md) guide. Orderlines with non-print products (e.g. a screenprotector) must not have an image reference. ## Shipping Kinds Every order has a shipping kind that determines where the package is sent. | Kind | Destination | Use case | |------|-------------|----------| | `dropship` | Customer address you provide | Direct-to-consumer fulfillment | | `combined` | Your warehouse | You handle final distribution | Dropship orders ship directly to your customer. You provide the destination address and can optionally set a minimal shipping level and preferred packaging. Combined orders are consolidated with other orders and shipped to your business address. No address or shipping options are needed; they use the combined address configured on your store. ## Quoting Shipping Costs Before placing a dropship order you can ask which shipping levels it can ship with, and what each costs, with `POST /quote`. Nothing is stored. The request takes only the fields of `POST /orders` that determine shipping, under the same names; leave out the other order fields, such as images, annotations and the custom identifier. ```bash curl -X POST https://api.printeers.com/v2/quote \ -H "X-Printeers-Secret-Key: $KEY" \ -H "Content-Type: application/json" \ -d '{ "quote": { "storeReference": "Store_9jmn3dewwvy13rv12k7aannqzy", "shippingKind": "dropship", "dropshipAddress": { "company": "", "firstname": "John", "lastname": "Doe", "streetname": "Koelmalaan", "housenumber": "330", "housenumberAddition": "", "additionalInfo": "", "state": "", "zipcode": "1432 PV", "city": "Aalsmeer", "countryCode": "NL", "phonenumber": "+31612345678", "email": "john@example.com" }, "orderlines": [ { "productReference": "Product_4j8k2m5n7p9q1r3s6t8v0w2x4y", "quantity": 1 } ] } }' ``` ```json { "shippingOptions": [ { "level": "basic", "price": 395 }, { "level": "tracked", "price": 695 }, { "level": "express", "price": 1295 } ] } ``` Prices are in EUR cents. The first option is the lowest level the order can ship with: the `dropshipMinimalShippingLevel` you asked for (default `basic`), raised or lowered where the destination, the weight or the value of the order requires it. The higher levels follow as alternatives, and only levels the order can actually ship with are listed: a Packstation, for example, is served by `tracked` only, so the level you asked for is not guaranteed. Pass the level your customer picks as `dropshipMinimalShippingLevel` when you create the order. Combined orders ship to your business address together with other orders and have no shipping options of their own, so their quote has an empty `shippingOptions` list. A quote validates the store, the products and the destination country the way order creation does, and returns the same error codes, for example `store_not_writable_via_api`, `product_not_orderable` or `invalid_address_denied_country`. The remaining address checks (phone number, email address, field lengths) happen when the order is created. ## Creating an Order ### Dropship ```bash curl -X POST https://api.printeers.com/v2/orders \ -H "X-Printeers-Secret-Key: $KEY" \ -H "Content-Type: application/json" \ -d '{ "order": { "storeReference": "Store_9jmn3dewwvy13rv12k7aannqzy", "shippingKind": "dropship", "customIdentifier": "my-order-123", "dropshipAddress": { "company": "", "firstname": "John", "lastname": "Doe", "streetname": "Koelmalaan", "housenumber": "330", "housenumberAddition": "", "additionalInfo": "", "state": "", "zipcode": "1432 PV", "city": "Aalsmeer", "countryCode": "NL", "phonenumber": "+31612345678", "email": "john@example.com" }, "dropshipMinimalShippingLevel": "tracked", "orderlines": [ { "productReference": "Product_4j8k2m5n7p9q1r3s6t8v0w2x4y", "imageReference": "Image_pdcptbw3k48sv03wy338bb8jzm", "quantity": 1 } ] } }' ``` ```json { "order": { "reference": "Order_5jcavdegg8n9pvgxq887jq0zbc" } } ``` ### Combined ```bash curl -X POST https://api.printeers.com/v2/orders \ -H "X-Printeers-Secret-Key: $KEY" \ -H "Content-Type: application/json" \ -d '{ "order": { "storeReference": "Store_9jmn3dewwvy13rv12k7aannqzy", "shippingKind": "combined", "orderlines": [ { "productReference": "Product_4j8k2m5n7p9q1r3s6t8v0w2x4y", "imageReference": "Image_pdcptbw3k48sv03wy338bb8jzm", "quantity": 5 } ] } }' ``` ```json { "order": { "reference": "Order_j2rznt7qr6ra78zb0aq2hsp3fd" } } ``` When order creation fails, the response includes an error `code` and a human-readable `message`: ```json { "code": "missing_address", "message": "Dropship address is required for dropship orders" } ``` Common errors include `missing_address` (dropship order without a destination address) and `missing_image_reference` (print product without an uploaded image). For the full list of request fields and error codes, see the [Orders API Reference](https://api-docs.test-printeers.com/reference/v2#tag/orders). ### Annotations Each orderline accepts up to 10 key-value `annotations`, for example to keep your own line identifiers or variant details with the order: ```json { "productReference": "Product_4j8k2m5n7p9q1r3s6t8v0w2x4y", "quantity": 1, "annotations": [ { "key": "shop-line-id", "value": "4711" }, { "key": "erp-article-number", "value": "ART-20481" } ] } ``` Annotations are stored with the orderline and returned unchanged in the `annotations` array of each orderline in `GET /orders/{orderReference}`. The array is always present and empty for orderlines that were created without annotations. ## Canceling Orderlines As long as an order is not in production, you can cancel (part of) the quantity of its orderlines with `POST /orders/{orderReference}/cancel`. The endpoint uses absolute totals: for every orderline you list, `canceledQuantity` is the desired total canceled quantity of that line, exactly as returned by `GET /orders/{orderReference}`. It is not the amount to cancel on top of the current value. Orderlines you do not list keep their current canceled quantity. A typical flow is to fetch the order first and then submit the new totals: ```bash curl https://api.printeers.com/v2/orders/Order_5jcavdegg8n9pvgxq887jq0zbc \ -H "X-Printeers-Secret-Key: $KEY" ``` Response: ```json { "order": { … "orderlines": [ { "index": 1, "quantity": 5, "canceledQuantity": 0, "shippedQuantity": 0, … }, { "index": 2, "quantity": 2, "canceledQuantity": 0, "shippedQuantity": 0, … } ] } } ``` To cancel 3 pieces of the first line and the complete second line: ```bash curl -X POST https://api.printeers.com/v2/orders/Order_5jcavdegg8n9pvgxq887jq0zbc/cancel \ -H "X-Printeers-Secret-Key: $KEY" \ -H "Content-Type: application/json" \ -d '{ "orderlines": [ { "index": 1, "canceledQuantity": 3 }, { "index": 2, "canceledQuantity": 2 } ] }' ``` A successful request returns `204 No Content`. Because the request describes the desired end state, sending the same request again also returns `204` without canceling anything further. This makes retries after a timeout safe. The request is atomic: when any listed orderline fails validation, nothing is canceled. Canceling cannot be undone. If you send a `canceledQuantity` below the current canceled quantity of a line, the request is rejected with `409 canceled_quantity_below_current`. This usually means the order changed since you last fetched it, for example because our support team canceled quantity on your behalf. Fetch the order again to see the current state. Other things to know: - Once a quantity has been shipped it can no longer be canceled. The canceled quantity is limited to `quantity - shippedQuantity` per line (`409 canceled_quantity_exceeds_shippable`). - Orders that are in production cannot be canceled (`409 already_in_production`). - Orders in stores backed by an upstream platform such as Shopify or WooCommerce must be canceled in that platform instead (`403 store_not_writable_via_api`). For the full list of error codes, see the [Orders API Reference](https://api-docs.test-printeers.com/reference/v2#tag/orders). ## Changing the Dropship Address As long as a dropship order is open, you can replace its destination address with `PUT /orders/{orderReference}/dropship-address`. An order is open until every orderline has been shipped or canceled. The request carries a complete address, validated with the same rules as `POST /orders`. It replaces the previous address entirely: there is no partial update, so send every field, including the ones that stay the same. ```bash curl -X PUT https://api.printeers.com/v2/orders/Order_5jcavdegg8n9pvgxq887jq0zbc/dropship-address \ -H "X-Printeers-Secret-Key: $KEY" \ -H "Content-Type: application/json" \ -d '{ "dropshipAddress": { "company": "", "firstname": "Jane", "lastname": "Doe", "streetname": "Laan van Alkmaar", "housenumber": "12", "housenumberAddition": "A", "additionalInfo": "", "state": "", "zipcode": "1811 AA", "city": "Alkmaar", "countryCode": "NL", "phonenumber": "+31612345678", "email": "jane@domain.example" } }' ``` A successful request returns `204 No Content`. The order now carries a new `dropshipAddressReference`, which `GET /orders/{orderReference}` returns. The previous address is no longer linked to the order, so its reference stops resolving through `GET /addresses/{addressReference}` unless a shipment of the order was already created with it. Fetch the order again to pick up the new reference. Other things to know: - A shipment that was already created keeps the address it was created with. Change the address before the order ships; once every orderline has shipped or been canceled the request is rejected (`409 order_completed`). - Combined orders have no destination address (`409 order_not_dropship`). - Replacement orders cannot change their address (`409 replacement_address_locked`): they are sent to the destination of the claim they settle. - Address validation errors use the same `400` codes as order creation, for example `invalid_address_missing_email` or `invalid_address_denied_country`. For the full list of error codes, see the [Orders API Reference](https://api-docs.test-printeers.com/reference/v2#tag/orders). ## Typical Integration A typical order creation flow follows these steps: 1. **Browse the catalog**: call `GET /products` to find available products and their references. 2. **Upload images**: send each print image to `POST /images` and store the returned references. 3. **Create the order**: call `POST /orders` with the product references, image references, quantities, and shipping details. 4. **Track the order**: use `GET /orders/{orderReference}` to check order status and retrieve shipment references. 5. **Track shipments**: once shipped, use `GET /shipments/{shipmentReference}` for tracking information. See the [Shipments and Tracking](https://api-docs.test-printeers.com/guides/shipments-and-tracking.md) guide. --- # Claims Canonical URL: https://api-docs.test-printeers.com/guides/claims Learn how to file and manage claims for shipment issues such as missing packages, damaged items, or print defects. ## Overview When something goes wrong with a shipment (a package never arrives, an item is damaged, or a print is misaligned), you can file a claim through the API. Printeers reviews each claim and decides on a replacement, refund, or rejection. The claims flow looks like this: ``` Package missing? Item issues? │ │ │ Upload evidence (JPEG/PNG) │ POST /evidence │ │ │ ▼ │ Evidence_xxx │ │ ▼ ▼ POST /claim-package-missing POST /claim-item-issues │ │ └───────────────┬────────────────┘ │ ▼ Claim_xxx │ ▼ ┌─────────── pending ──┬─────────┐ │ │ │ ▼ ▼ ▼ retracted resolved rejected ``` ## Evidence Most item-issues claims must carry evidence: photos that show the problem. Whether evidence is required depends on the claim category — categories where there is usually nothing to photograph, such as "Item missing", accept claimlines without evidence (see `evidenceRequired` in [Claim Categories](#claim-categories)). Evidence is optional for package-missing claims. Upload each file as a `POST /evidence` request with the raw image bytes in the body. ```bash curl -X POST https://api.printeers.com/v2/evidence \ -H "X-Printeers-Secret-Key: $KEY" \ -H "Content-Type: application/octet-stream" \ --data-binary @photo.jpg ``` ```json { "evidence": { "reference": "Evidence_0fa7gephpy70zt0qanfja9hvqt" } } ``` Accepted formats are JPEG and PNG. Each item in an item-issues claim accepts up to 3 evidence references; at least 1 is required when the item's claim category has `evidenceRequired: true`. Package-missing claims accept 0-3 evidence references. Unused evidence is automatically deleted after 1 day, so upload evidence right before creating the claim. Evidence uploads count against the `uploads` rate-limit bucket; see the [Rate Limiting](https://api-docs.test-printeers.com/guides/rate-limiting.md) guide. ## Claim Categories Item-issues claims require a claim category for each claimed item. Retrieve the current list of valid categories from `GET /claimcategories`: ```bash curl https://api.printeers.com/v2/claimcategories \ -H "X-Printeers-Secret-Key: $KEY" ``` ```json { "claimcategories": [ { "reference": "Claimcategory_zt7cdqy06zecmwh2shry4cpq04", "name": "Damage (broken)", "evidenceRequired": true }, { "reference": "Claimcategory_yvtazxbe3m9nrp409a96x9bxh4", "name": "Item missing", "evidenceRequired": false }, ... ] } ``` `evidenceRequired` tells you whether claimlines in that category must carry at least one evidence reference. Always fetch these dynamically rather than hardcoding them; the list may change. > **Note:** "Package missing" is not a valid claim category for item-issues claims (error code `package_missing_category_not_allowed`). If an entire package never arrived, use `POST /claim-package-missing` instead. ## Creating a Claim There are two types of claims, depending on the situation. ### Package Missing Use `POST /claim-package-missing` when an entire package never arrived. This automatically claims all items in the shipment. ```bash curl -X POST https://api.printeers.com/v2/claim-package-missing \ -H "X-Printeers-Secret-Key: $KEY" \ -H "Content-Type: application/json" \ -d '{ "claim": { "shipmentReference": "Shipment_9jmn3dewwvy13rv12k7aannqzy", "evidenceReferences": [] } }' ``` ```json { "claim": { "reference": "Claim_216kraceftv49wpzsdvp4wyv8n" } } ``` Requirements: - The promised delivery window for the shipment's destination and shipping level must have passed (never earlier than two calendar days after the ship day). The `shipment_too_recent` error message states the date from which the claim can be filed. - The shipment must be within the 100-day warranty period. - There must be no existing claims on the shipment. - The order must not already have a replacement. ### Item Issues Use `POST /claim-item-issues` when specific items have problems (damage, print defects, wrong product, etc.). ```bash curl -X POST https://api.printeers.com/v2/claim-item-issues \ -H "X-Printeers-Secret-Key: $KEY" \ -H "Content-Type: application/json" \ -d '{ "claim": { "shipmentReference": "Shipment_cxb6287xjnt5726d1dnpnqg6mc", "orderReference": "Order_5jcavdegg8n9pvgxq887jq0zbc", "comment": "Two cases arrived with visible scratches on the print.", "claimlines": [ { "orderlineIndex": 1, "quantity": 2, "claimcategoryReference": "Claimcategory_8k2m4p6r9t1w3y5z7b9d1f3h5j", "evidenceReferences": [ "Evidence_sq3bkad1t0hq3q83s7897bs8cg", "Evidence_8jssahgpgx4qafb6c8aebreja2" ] } ] } }' ``` ```json { "claim": { "reference": "Claim_9czkfrkk0kww2f7wsfsnmk8g2j" } } ``` Requirements: - The shipment must be within the 100-day warranty period. - Every `orderlineIndex` must be an item of the order that was shipped in the shipment (error codes `order_not_in_shipment` and `orderline_not_in_shipment` otherwise). - The claimed quantity per item must not exceed the shipped quantity (existing open claims count toward this). - Each item accepts up to 3 evidence references; at least 1 is required when its claim category has `evidenceRequired: true` (error code `evidence_required` otherwise). - The `comment` field is optional (max 1000 characters). ## Claim Status Every claim has one of four statuses: | Status | Meaning | |---|---| | `pending` | Claim is being reviewed by support. | | `resolved` | At least one item was awarded a replacement or refund. | | `rejected` | The claim was reviewed and denied. | | `retracted` | The claim was canceled by you before a decision was made. | ## Listing Claims `GET /claims` returns all open claims and recently closed claims for your organization. By default, closed claims from the last 14 days are included. Use `closedAfter` to change this window. ```bash curl "https://api.printeers.com/v2/claims" \ -H "X-Printeers-Secret-Key: $KEY" ``` ```json { "claims": [ { "reference": "Claim_9czkfrkk0kww2f7wsfsnmk8g2j", "orderReference": "Order_j2rznt7qr6ra78zb0aq2hsp3fd", "status": "pending", "claimcategories": [ { "reference": "Claimcategory_8k2m4p6r9t1w3y5z7b9d1f3h5j", "name": "Damage (scratches)" } ], "created": "2025-12-15T10:30:00Z", "retracted": null, "decided": null } ], "nextCursor": null } ``` Results are paginated. See the [Pagination](https://api-docs.test-printeers.com/guides/pagination.md) guide for how to iterate through pages using `cursor`. ## Getting Claim Details Use `GET /claims/{claimReference}` to see full details, including individual claimlines and the resolution per item. ```bash curl "https://api.printeers.com/v2/claims/Claim_7zx1f95d90k54ymatsa45tz5dj" \ -H "X-Printeers-Secret-Key: $KEY" ``` ```json { "claim": { "reference": "Claim_7zx1f95d90k54ymatsa45tz5dj", "orderReference": "Order_gegcgtb3a8d349nsp7x0vyntd1", "status": "resolved", "comment": "Two cases arrived with visible scratches on the print.", "createdBy": "user@example.com", "created": "2025-12-15T10:30:00Z", "retracted": null, "decision": { "decided": "2025-12-16T14:00:00Z", "refundShippingCosts": true, "comment": "Confirmed print defect. Replacement issued for 1, refund for 1." }, "shipmentReference": "Shipment_03t8w1hfbfdh6vfy7wz67kqy6v", "evidenceUrls": [], "claimlines": [ { "productReference": "Product_4j8k2m5n7p9q1r3s6t8v0w2x4y", "orderlineIndex": 1, "quantity": 2, "imageReference": "Image_78h3pwhvn2gnnsks3a92nww5zn", "claimcategory": { "reference": "Claimcategory_8k2m4p6r9t1w3y5z7b9d1f3h5j", "name": "Damage (scratches)" }, "decision": { "replacementQuantity": 1, "refundQuantity": 1, "rejectionQuantity": 0, "comment": "" }, "evidenceUrls": [ "https://files.printeers.com/evidence/Evidence_sq3bkad1t0hq3q83s7897bs8cg.jpeg", "https://files.printeers.com/evidence/Evidence_8jssahgpgx4qafb6c8aebreja2.jpeg" ] } ] } } ``` The `claim.status` field reflects the current claim status (see [Claim Status](#claim-status) above). The `decision` fields on both the claim and each claimline are `null` while the claim is still `pending`. Once a decision is made, the claim-level `decision` contains `decided`, `refundShippingCosts`, and `comment`, while each claimline's `decision` contains `replacementQuantity`, `refundQuantity`, `rejectionQuantity`, and `comment`. The `evidenceUrls` fields list the evidence images that were uploaded when the claim was created. Each claimline carries its own evidence; the claim-level `evidenceUrls` is only populated for package-missing claims. Evidence is stored for a limited time: roughly four months after a claim is decided or retracted the images are purged, the lists become empty, and previously returned URLs stop working — download the images in time if you want to keep them. ## Retracting a Claim If you created a claim by mistake, you can retract it as long as it is still `pending`: ```bash curl -X POST "https://api.printeers.com/v2/claims/Claim_5ecs3774nnk11rr07y4da2dm51/retract" \ -H "X-Printeers-Secret-Key: $KEY" ``` A successful retraction returns `204 No Content` (empty body). Retraction cannot be undone; once retracted, the claim is permanently closed. ## Typical Integration A typical claims integration follows these steps: 1. **Fetch claim categories**: call `GET /claimcategories` and present them to your user. 2. **Collect evidence**: have the user upload photos when the chosen category has `evidenceRequired: true` (or whenever photos are available). Send each to `POST /evidence`. 3. **Create the claim**: call `POST /claim-package-missing` or `POST /claim-item-issues` depending on the situation. 4. **Poll for resolution**: periodically call `GET /claims` or `GET /claims/{claimReference}` to check the status. 5. **Handle the outcome**: once the claim's `decision` is set, inspect it for overall details and each claimline's `decision` to determine what was replaced, refunded, or rejected. --- # Rendering Canonical URL: https://api-docs.test-printeers.com/guides/rendering Learn how to create product mockup renders using scenes, perspectives, and compositions. ## Overview Rendering generates realistic product mockups by placing your design on a product in a 3D scene, viewed from a specific camera angle. The full flow looks like this: ``` Browse catalog Discover scenes GET /products GET /scenes │ │ ▼ ▼ Product_xxx Scene_xxx │ │ └─────────┬───────────┘ │ ▼ List perspectives GET /perspectives │ ▼ Perspective_xxx │ ▼ Look up composition GET /lookup-composition │ ▼ Composition_xxx + Image_xxx POST /renders │ ▼ Render_xxx │ ▼ Poll status GET /renders/xxx │ ▼ completed │ ┌─────────┴─────────┐ ▼ ▼ Download media Download thumbnail GET /renders/xxx/ GET /renders/xxx/ media thumbnail ``` ## Concepts Rendering has four building blocks: - **Scene**: A 3D environment (studio backdrop, desk setup, flat lay) in which products are placed. Each scene supports specific productgroups. - **Perspective**: A camera angle with output settings (resolution, format). Scene perspectives can be used for all products in a scene. Product perspectives are tied to specific products, these usually highlight product features such as the camera island or charging port. - **Composition**: A valid combination of a scene, a product, and a perspective. Not all combinations exist. Use the lookup endpoint to check. - **Render**: The final output: your design placed on the product in the scene, from the chosen camera angle. ## Step 1: Discover Scenes and Perspectives List available scenes with `GET /scenes`. Use the optional `productgroupReference` filter to find scenes that support a specific productgroup. ```bash curl "https://api.printeers.com/v2/scenes?productgroupReference=Productgroup_7yrcm5hd2w8pz4x6k3qf9b1tnv" \ -H "X-Printeers-Secret-Key: $KEY" ``` ```json { "scenes": [ { "reference": "Scene_8hm3pz5wkr1yv7xn4btq9cdfsj", "name": "Studio White Backdrop" } ] } ``` Use `GET /scenes/{sceneReference}` to see the scene's compatible productgroups and perspectives: ```json { "scene": { "reference": "Scene_8hm3pz5wkr1yv7xn4btq9cdfsj", "name": "Studio White Backdrop", "description": "A simple photo studio with white backdrop", "private": false, "allowsProductPerspectives": false, "compatibleProductgroups": [ { "reference": "Productgroup_7yrcm5hd2w8pz4x6k3qf9b1tnv" } ], "perspectives": [ { "reference": "Perspective_6rv2mc8yqw3nt5xk9hpz1bdfgs" }, { "reference": "Perspective_3qn8yw1kfh6mp5xr7vzb9ctdsj" } ] } } ``` When `allowsProductPerspectives` is true, product-specific perspectives can also be used with this scene in addition to the scene's own perspectives. Not all scenes are suitable for rendering the product highlight perspectives. List perspectives with `GET /perspectives`, optionally filtered by `sceneReference` and/or `productReference`. Use `GET /perspectives/{perspectiveReference}` to see output details: ```json { "perspective": { "reference": "Perspective_6rv2mc8yqw3nt5xk9hpz1bdfgs", "name": "Upper Right", "output": { "widthPx": 2560, "heightPx": 2560, "format": "png" }, "scenes": [ { "reference": "Scene_8hm3pz5wkr1yv7xn4btq9cdfsj" } ], "products": null } } ``` For scene perspectives, `scenes` lists the linked scenes and `products` is null. For product perspectives, `products` lists the linked products and `scenes` is null. ## Step 2: Look Up a Composition Use `GET /lookup-composition` to check whether a scene + product + perspective combination is available: ```bash curl "https://api.printeers.com/v2/lookup-composition?sceneReference=Scene_8hm3pz5wkr1yv7xn4btq9cdfsj&productReference=Product_4j8k2m5n7p9q1r3s6t8v0w2x4y&perspectiveReference=Perspective_6rv2mc8yqw3nt5xk9hpz1bdfgs" \ -H "X-Printeers-Secret-Key: $KEY" ``` When the combination is available, the endpoint returns `200 OK`: ```json { "composition": { "reference": "Composition_8hm3pz5wkr1yv7xn4btq9cdfsj", "tokenCost": 10 } } ``` `tokenCost` is the number of rendertokens a render of this composition consumes; see [Rendertokens](#rendertokens) below. When the combination does not exist, the endpoint returns `404 Not Found`. When the combination exists but is no longer available, it returns `410 Gone`. **Note**: It may make sense to store or even hard-code references to the scenes, perspectives and/or compositions you require for your use-case. Just be aware that references are different between our test and prod environment and that referenced objects may be deprecated and become unavailable. ## Step 3: Upload an Image Upload the design you want rendered on the product. This is the same image upload used for creating orders. See the [Creating Orders](https://api-docs.test-printeers.com/guides/creating-orders.md) guide for full details. ```bash curl -X POST https://api.printeers.com/v2/images \ -H "X-Printeers-Secret-Key: $KEY" \ -H "Content-Type: application/octet-stream" \ --data-binary @design.png ``` ```json { "image": { "reference": "Image_pdcptbw3k48sv03wy338bb8jzm" } } ``` Accepted formats are JPEG and PNG. Images larger than 7000x7000 pixels are automatically downsized. Uploading the same image twice returns the same reference. ## Step 4: Create a Render Create a render by providing a composition reference and an image reference: ```bash curl -X POST https://api.printeers.com/v2/renders \ -H "X-Printeers-Secret-Key: $KEY" \ -H "Content-Type: application/json" \ -d '{ "render": { "compositionReference": "Composition_8hm3pz5wkr1yv7xn4btq9cdfsj", "imageReference": "Image_pdcptbw3k48sv03wy338bb8jzm" } }' ``` ```json { "render": { "reference": "Render_8eksvwek9pc58r1qmqqf3492mg" } } ``` Creating a render with the same composition and image returns the existing render reference. Every call consumes the composition's `tokenCost` rendertokens from your organization's weekly limit. When the limit is reached, the endpoint returns `429 Too Many Requests` with error code `rendertoken_limit_exceeded`; see [Rendertokens](#rendertokens) below. ## Step 5: Poll for Completion Renders are processed asynchronously. Poll `GET /renders/{renderReference}` to check the status. Use a 60-second polling interval. ```bash curl "https://api.printeers.com/v2/renders/Render_8eksvwek9pc58r1qmqqf3492mg" \ -H "X-Printeers-Secret-Key: $KEY" ``` ```json { "render": { "reference": "Render_8eksvwek9pc58r1qmqqf3492mg", "compositionReference": "Composition_8hm3pz5wkr1yv7xn4btq9cdfsj", "imageReference": "Image_pdcptbw3k48sv03wy338bb8jzm", "status": "completed", "thumbhash": "1QcSHQRnh493V4dIh4eXh4aTh0eHJ4dnhw==", "media": { "mimeType": "image/png", "extension": "png", "size": 524288 }, "thumbnail": { "mimeType": "image/png", "extension": "png", "size": 32768 }, "created": "2026-02-24T12:34:56Z", "completed": "2026-02-24T12:35:10Z" } } ``` | Status | Meaning | |---|---| | `pending` | Render is queued or being processed. | | `completed` | Render is ready for download. | | `failed` | Render failed during processing. | The `thumbhash` field contains a base64-encoded [thumbhash](https://evanw.github.io/thumbhash/) for generating a placeholder preview while the full image loads. It is only available when status is `completed`. ## Step 6: Download the Result Download the full render or its thumbnail: ```bash # Full render curl "https://api.printeers.com/v2/renders/Render_8eksvwek9pc58r1qmqqf3492mg/media" \ -H "X-Printeers-Secret-Key: $KEY" \ --output render.png # Thumbnail curl "https://api.printeers.com/v2/renders/Render_8eksvwek9pc58r1qmqqf3492mg/thumbnail" \ -H "X-Printeers-Secret-Key: $KEY" \ --output thumbnail.png ``` When the render is complete, these endpoints return `200 OK` with the binary file. When the render is not yet complete, they return `202 Accepted` with a `Retry-After` header indicating how many seconds to wait before retrying. A completed render's files never change, so the `200 OK` responses carry `Cache-Control: private, max-age=31536000, immutable`. An HTTP client or proxy that honors it keeps the file cached instead of downloading it again. Render downloads count against the `downloads` rate-limit bucket; see the [Rate Limiting](https://api-docs.test-printeers.com/guides/rate-limiting.md) guide. ## Rendertokens Render creation is metered in **rendertokens**. Each composition has a `tokenCost` (returned by `GET /lookup-composition` and `GET /compositions/{compositionReference}`), and every `POST /renders` call deducts that cost from your organization's weekly rendertoken limit. The limit resets every Monday at 00:00 Europe/Amsterdam, aligned with the billing week. Check your current usage with `GET /rendertokens`: ```bash curl "https://api.printeers.com/v2/rendertokens" \ -H "X-Printeers-Secret-Key: $KEY" ``` ```json { "rendertokens": { "used": 120, "limit": 1000, "remaining": 880, "resets": "2026-07-13T00:00:00+02:00" } } ``` When the remaining rendertokens do not cover the composition's `tokenCost`, `POST /renders` returns `429 Too Many Requests` with error code `rendertoken_limit_exceeded` and a `Retry-After` header set to the time until the weekly reset. Retrying earlier will not succeed. A rendertoken subscription with a higher weekly limit can be activated in the Printeers Dashboard under Settings, Subscriptions. ## Typical Integration A typical rendering integration follows these steps: 1. **Browse the catalog**: call `GET /products` to find products and their productgroup references. See the [Products](https://api-docs.test-printeers.com/guides/products.md) guide. 2. **Discover scenes**: call `GET /scenes` filtered by productgroup to find available scenes. 3. **Pick a perspective**: use `GET /scenes/{sceneReference}` to see the scene's perspectives, or `GET /perspectives` to browse all perspectives. 4. **Look up the composition**: call `GET /lookup-composition` with the scene, product, and perspective. Returns 404 if the combination doesn't exist, or 410 if it's no longer available. 5. **Upload an image**: send the design to `POST /images`. 6. **Create the render**: call `POST /renders` with the composition and image references. 7. **Wait for completion**: poll `GET /renders/{renderReference}` every 60 seconds until `status` is `completed`. 8. **Download the result**: fetch the full render from `/media` and optionally the thumbnail from `/thumbnail`. ## Next Steps - [Renders API Reference](https://api-docs.test-printeers.com/reference/v2#tag/renders): full endpoint documentation for all rendering endpoints. - [Products](https://api-docs.test-printeers.com/guides/products.md): learn how products and productgroups are organized. - [Creating Orders](https://api-docs.test-printeers.com/guides/creating-orders.md): place orders with your product images. --- # Rate Limiting Canonical URL: https://api-docs.test-printeers.com/guides/rate-limiting Learn how per-organization rate limits work, how to read the response headers, and how to react to a 429. The Printeers partner API enforces per-organization rate limits so one API-user cannot saturate the API for everyone else. A healthy integration should never see a 429. Limits are scoped to the organization that owns the API keys, so multiple keys belonging to the same organization share one budget. ## Limits Every authenticated request consumes from at least one rate-limit bucket: | Policy | Quota (`q`) | Window (`w`) | Sustainable rate | Applies to | |---|---|---|---|---| | `default` | 3000 | 60 seconds | 50 req/sec | every authenticated request | | `uploads` | 5000 | 24 hours | 5000 per day | `POST /v2/images`, `POST /v2/evidence` | | `downloads` | 100000 | 24 hours | 100000 per day | `GET /v2/renders/{renderReference}/media`, `GET /v2/renders/{renderReference}/thumbnail`, `GET /v2/products/{productReference}/media/{filename}` | Upload and download requests consume one token from `default` plus one token from their dedicated bucket. `default` lets you sustain 50 requests per second indefinitely, or burst up to 3000 requests over 60 seconds before the bucket runs dry. `uploads` caps total uploads per organization per day to bound storage growth. `downloads` caps total media downloads per organization per day to prevent re-fetching the same media indefinitely. All buckets refill continuously. The rate-limit policies, their numerical values, and which routes consume from which buckets may all change within a major API version as traffic patterns evolve. Tuning these is not considered a breaking change. Always read the live values from the `X-RateLimit-Policy` and `X-RateLimit` response headers; do not hard-code anything from this guide into your integration. ## Response Headers The two headers below follow the structured-field syntax from the [IETF draft for rate-limit headers](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/), the standardization effort for communicating rate limits over HTTP. We use the `X-` prefix because the draft is not yet a published RFC; partner-api v3 will switch to the unprefixed names (`RateLimit` and `RateLimit-Policy`) once it is. The parameter names (`q`, `w`, `r`, `t`) will not change. Every authenticated response carries these two headers showing the buckets the request consumed from: ``` X-RateLimit-Policy: "default";q=3000;w=60, "uploads";q=5000;w=86400 X-RateLimit: "default";r=2987;t=0, "uploads";r=4982;t=0 ``` - `X-RateLimit-Policy`: `q` is the bucket capacity, `w` is the refill window in seconds. The long-term sustainable rate is `q/w`. - `X-RateLimit`: `r` is your remaining tokens, `t` is the seconds until the bucket regains a full token (always `0` while the bucket still has tokens). Requests that only consume from `default` show a single entry in each header. If `r` is getting low for a bucket your integration depends on, slow down preemptively rather than waiting for a 429. ## When You Hit a Limit When any bucket is empty, you receive `HTTP 429 Too Many Requests`: ``` HTTP/1.1 429 Too Many Requests Content-Type: application/json Retry-After: 12 X-RateLimit-Policy: "default";q=3000;w=60 X-RateLimit: "default";r=0;t=12 { "code": "rate_limited", "message": "Rate limit exceeded; see Retry-After and X-RateLimit headers." } ``` Wait at least the number of seconds in `Retry-After`, then retry. The value already includes jitter so concurrent clients will not synchronize their retries. For `uploads` denials `Retry-After` is around 17 seconds (plus jitter), the time it takes the daily bucket to refill one token. Once you have hit the cap, you can resume at roughly one upload every 17 seconds until the bucket has filled back up. For `downloads` denials `Retry-After` is around 1 second (plus jitter), since the daily bucket refills a token roughly every 0.86 seconds. `Retry-After` follows [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html#field.retry-after). ## Need a Higher Limit? If your integration has a legitimate use case that bumps into these limits regularly, contact [support@printeers.com](mailto:support@printeers.com). Tell us which policy is the bottleneck and roughly what volume you need; we can raise the limits for your organization. --- # Webhooks Canonical URL: https://api-docs.test-printeers.com/guides/webhooks Receive signed event notifications on your own endpoint the moment something happens on the Printeers platform, instead of polling the API. > Looking for the single order-updated callback URL that predates this system? That mechanism keeps working unchanged and is documented in the [legacy webhook notification guide](https://api-docs.test-printeers.com/guides/legacy-webhooks.md). New integrations should use the webhooks described here. A webhook endpoint is an HTTPS URL on your side that Printeers calls with an HTTP `POST` whenever an event you subscribed to occurs. The payload tells you *what happened* and *to which resource*; you then fetch the current state through the [API](https://api-docs.test-printeers.com/reference/v2). The payload format and the available event kinds are documented in the [Webhooks reference](https://api-docs.test-printeers.com/reference/webhooks). The available event kinds follow an order through its lifecycle: `order.backordered`, `order.production_started`, `order.on_hold`, `order.hold_released`, `order.printed`, `order.completed` and `order.canceled`, plus `shipment.created`, fired when a shipment is created for one or more of your orders. Each kind is described in the [Webhooks reference](https://api-docs.test-printeers.com/reference/webhooks). More kinds will follow, but your endpoint only receives the kinds it is subscribed to, so new kinds are never delivered unannounced. ## Creating an endpoint Webhook endpoints are managed in the Dashboard, under **Settings**. An endpoint belongs to your organization, not to a single store: one endpoint receives the events of **all your stores** for the event kinds it is subscribed to. If you route per store, read the references in the payload and dispatch on your side. When you create an endpoint you choose which event kinds it is subscribed to, and the Dashboard shows you the endpoint's signing secret. Store the secret in your platform's secrets store; you need it to verify deliveries. You can create multiple endpoints, each with its own URL, subscriptions, and secret, for example one per environment or one per consuming system. ## The event payload Deliveries carry a deliberately thin JSON envelope: the event `reference`, the `kind`, when it happened, and a `subject` object holding the reference to the entity the event is about, keyed by the kind's subject prefix (`subject.shipment` for `shipment.*` kinds, `subject.order` for `order.*` kinds), plus a human-readable `message` for display only. It does not carry the subject's state, because that state may already have changed by the time you process the delivery. On receipt, fetch the current state from the API using the reference in the payload. For a `shipment.created` event, call [`GET /shipments/{shipmentReference}`](https://api-docs.test-printeers.com/reference/v2#tag/shipments/GET/shipments/{shipmentReference}) with `subject.shipment.reference` to retrieve the tracking details and the shipped items. This way you always act on the latest state, no matter when or in what order deliveries arrive. The exact envelope schema and each event kind are documented in the [Webhooks reference](https://api-docs.test-printeers.com/reference/webhooks). Unrecognized payload fields must be ignored: fields can be added at any time. The `message` field is for humans; its wording can change without notice, so never parse it. ## Verifying signatures Every delivery is signed following the [Standard Webhooks](https://www.standardwebhooks.com/) specification. Three headers accompany each delivery: | Header | Meaning | | --- | --- | | `webhook-id` | The event reference (`Event_...`). Identical across retries; use it to deduplicate. | | `webhook-timestamp` | Unix timestamp (seconds) of this delivery attempt, refreshed per retry. | | `webhook-signature` | `v1,` followed by the base64 HMAC-SHA256 of `{webhook-id}.{webhook-timestamp}.{body}`, keyed with your endpoint secret. | Verify the signature before trusting a delivery, and always verify against the **raw request body bytes**, not a re-serialized version of the parsed JSON. The easiest way is an off-the-shelf [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks/tree/main/libraries), available for most languages. For example, in Node.js: ```javascript import { Webhook } from "standardwebhooks"; const webhook = new Webhook(endpointSecret); // "whsec_..." // Throws when the signature is invalid or the timestamp is too old. const event = webhook.verify(rawBody, { "webhook-id": req.headers["webhook-id"], "webhook-timestamp": req.headers["webhook-timestamp"], "webhook-signature": req.headers["webhook-signature"], }); ``` The `webhook-signature` header can contain multiple space-separated signatures. The delivery is authentic if any one of them verifies; the standard libraries handle this for you. ## Rotating a secret You can rotate an endpoint's secret in the Dashboard. After a rotation the previous secret stays valid for **24 hours**: during that grace period every delivery is signed with both the new and the previous secret, so you can deploy the new secret on your side without dropping deliveries. After the grace period only the new secret's signature is sent. ## Delivery and retries - Respond with any **`2xx`** status code to acknowledge a delivery. Anything else, including timeouts and connection failures, counts as a failed attempt. - Acknowledge fast: record the delivery, respond, and do your API fetch afterwards, out of band. - Failed deliveries are retried with increasing backoff, up to **25 attempts spread over up to 8 days**. After that the delivery is permanently failed. - Delivery is **at-least-once**: the same event can arrive more than once, and the same event is delivered to every endpoint subscribed to its kind. Deduplicate by the `webhook-id` header. - Deliveries are **unordered**: a retried event can arrive after a newer event. Use the event's `created` timestamp to order them, or simply fetch current state on every delivery. The Dashboard shows a delivery log per endpoint with every attempt, its response status, and the response your endpoint returned, so you can see exactly what was delivered and debug failures on your side. Log entries are kept for **90 days**; captured response bodies are kept for **10 days**. ## Failing endpoints are disabled automatically If an endpoint keeps failing, Printeers disables it: an endpoint is automatically disabled once it has failed **10 or more consecutive attempts** sustained over **3 days**. A short outage never disables an endpoint. A disabled endpoint receives nothing: events that occur while an endpoint is disabled are **not delivered to it and not queued** for later. Once your endpoint is fixed, re-enable it in the Dashboard; delivery resumes with new events from that point on, and the failure streak is reset so a repaired endpoint starts with a fresh disable window. Fetch anything you missed through the API. --- # V1 vs V2 Canonical URL: https://api-docs.test-printeers.com/guides/v1-vs-v2 If you're moving from V1 to V2, this page is a quick orientation. V2 is a fresh design rather than a like-for-like upgrade, so the smoothest path is usually to write a new integration against V2 alongside your existing V1 code, then switch over once you're happy. The sections below walk through the conceptual differences and give you an endpoint mapping so you can find the V2 equivalent of what you use in V1. ## Authentication In V1 you sent two headers, `X-IPP-PARTNERCODE` and `X-IPP-APIKEY`. In V2 you send a single header: ``` X-Printeers-Secret-Key: ``` Your V1 partner code and API key won't work against V2. You'll generate a V2 secret key in the dashboard before your first call. See the [Getting Started](https://api-docs.test-printeers.com/guides/getting-started.md) guide for the steps. ## Base URL | Environment | V1 | V2 | | ----------- | ----------------------------------- | ----------------------------------- | | Production | `https://api.printeers.com/v1` | `https://api.printeers.com/v2` | | Test | `https://api.test-printeers.com/v1` | `https://api.test-printeers.com/v2` | ## Field naming V1 fields are `snake_case` (`partner_reference`, `image_reference`, `shipping_kind`). V2 fields are `lowerCamelCase` (`customIdentifier`, `imageReference`, `shippingKind`). When you port a request or response, you'll be renaming most field keys. ## Response shape V1 puts resource data at the top level of the response body. V2 wraps single-resource responses under a named field, and list responses under a field named for the resource: ```json // V1 { "reference": "TESTCODE-123", "status": "..." } // V2 (single) { "order": { "reference": "Order_...", "status": { ... } } } // V2 (list) { "orders": [ ... ], "nextCursor": "..." } ``` See the [Pagination](https://api-docs.test-printeers.com/guides/pagination.md) guide for cursor-based list responses. ## References In V1 you identified resources with composite codes: store codes (`TESTCODE`), order references (`TESTCODE-123`), SKUs (`PART0037`). In V2 each resource has a typed, opaque reference of the shape `_<26 opaque characters>`: | V1 | V2 | | --------------------------- | ---------------------------------------- | | `TESTCODE` | `Store_x04rwj6knrhks8m19s8j6nc4z8` | | `TESTCODE-123` | `Order_5x4rwj6knrhks8m19s8j6nc4z8` | | `PART0037` | `Product_6rgpv1t48gswz36g7apj12xygc` | | numeric image id | `Image_pdcptbw3k48sv03wy338bb8jzm` | Your old codes still appear on V2 resources as `legacyCode` / `legacySku` / `legacyOrderCode` fields so you can match them to V2 references during the move. These legacy fields will be removed in V3, so plan to switch your storage to V2 references over time. The `/utilities/lookup-*-by-legacy-*` endpoints help you translate codes you already have on file. See the [Items, Products and SKUs](https://api-docs.test-printeers.com/guides/items-products-and-skus.md) guide for how product identifiers map across versions. ## Endpoint mapping | V1 | V2 | | ------------------------------------------------ | ------------------------------------------------------ | | `GET /stock` | `GET /products`, `GET /productgroups`, `GET /items` | | `GET /stock-attributes` | (no equivalent; V2 products do not expose device or case attributes) | | `POST /images` | `POST /images` | | `POST /orders` | `POST /orders` | | `POST /quote` | `POST /quote`; see [Quoting Shipping Costs](https://api-docs.test-printeers.com/guides/creating-orders.md#quoting-shipping-costs) | | `GET /orders/{reference}` | `GET /orders/{orderReference}` | | `GET /orders/by-partner-reference/{ref}` | `GET /utilities/lookup-order-reference-by-custom-identifier` then `GET /orders/{orderReference}` | | `GET /orders/{reference}/cancel-all-lines` | `GET /orders/{orderReference}` then `POST /orders/{orderReference}/cancel` listing every orderline; see [Canceling Orderlines](https://api-docs.test-printeers.com/guides/creating-orders.md#canceling-orderlines) | | `POST /render` (deprecated in V1) | `POST /renders` plus the render polling flow | V2 also adds areas that didn't exist in V1: claims, shipments, organization read endpoints, addresses, scenes/perspectives/compositions for renders, and warehouse items. You can adopt these as you need them. ## Image upload V1 accepts a base64-encoded JPEG/PNG inside a JSON body. V2 expects the raw image bytes as the request body with `Content-Type: application/octet-stream`. If you're porting a V1 upload routine, you can drop the base64 step. ## Notifications The order-updated callback you configured for V1 keeps firing for orders created via V2, so your existing notification handler continues to work. It is not tied to the V1 lifecycle and stays available after V1 is retired. See the [legacy webhook notification](https://api-docs.test-printeers.com/guides/legacy-webhooks.md) guide for that flow. New integrations should use the [webhook system](https://api-docs.test-printeers.com/guides/webhooks.md) instead, which delivers signed events for multiple event types. ## Errors V1 and V2 both return a `{ "code": "...", "message": "..." }` body for `4xx` responses. The set of error codes differs per endpoint; see each endpoint's reference page for the codes you may receive. ## Rate limiting V2 publishes its rate-limit policies and returns a `429 Too Many Requests` response with `Retry-After`, `X-RateLimit`, and `X-RateLimit-Policy` headers, so you can back off without guessing. V1 limits were implicit. See the [Rate Limiting](https://api-docs.test-printeers.com/guides/rate-limiting.md) guide. ## Versioning V2 follows semantic versioning: additive changes ship as minor versions and won't break your client; breaking changes wait for the next major version. See the [API Versioning](https://api-docs.test-printeers.com/guides/api-versioning.md) guide for the deprecation policy and what counts as a non-breaking change.