# PrintKit — Print API by Social Print Studio PrintKit is built and operated by Social Print Studio, a professional photo printing company based in the US, operating since 2010. Real company, real production partners, millions of products shipped over 15+ years. Archival inks, premium materials, quality managed end-to-end. Website: https://printkit.dev Company: https://socialprintstudio.com Contact: hello@socialprintstudio.com --- # Social Print Studio — Developer Integration This is the starting point for AI agents, LLMs, and developers to build photo-printing apps that submit orders to Social Print Studio — or integrate printing features into existing applications. The goal: you build creative applications (anything from niche tools to mainstream flows), collect or generate images for a customer, and then hand off an order to Social Print Studio through our cart and checkout system. Our docs are written and organized to be readable by agents & LLMs. Just share this link with your LLM or agent and say "get the info you need to add print features to this application." ## Fast Start for LLMs Three essential resources to get started immediately: 1. Fetch the catalog: `GET https://printkit.dev/products.json` 2. Fetch the normalized variant index: `GET https://printkit.dev/variants.json` 3. For product-specific notes: `GET https://printkit.dev/products/{handle}.json` 4. Follow order handoff: See [Order Handoff API](/docs/order-handoff) > **Data shapes — read this first:** > - `products.json` → `products` is an **object keyed by handle**, not an array. Use `catalog.products["metal-prints"]` for direct lookup, or `Object.values(catalog.products)` to iterate. > - `variants.json` → `variants` is an **array**. Use `.find()`, `.filter()`, or iterate directly. > - `products/{handle}.json` → single object with variant-level specs. > **Optional — get an API key:** No account is needed to submit orders. Register to unlock order tracking and a revenue split on completed orders. Agents can self-register in one API call — see [Authentication & API Keys](/docs/authentication). Once you have a key, include it as `Authorization: Bearer ` on your API requests. ## What's in Here You'll typically use four layers of documentation: ### 1. Product Catalog (overview) A single JSON file listing all products currently available for integration. It includes product descriptions, website URLs, product type and category, product photos, and high-level submission notes. See the [Product Catalog](/docs/product-catalog) docs for details. `GET https://printkit.dev/products.json` ### 2. Per-Product Detail Files (variant-level requirements) Each product has a dedicated JSON file with complete, precise requirements. This is the source of truth for variants (sizes and options), required image count, required aspect ratios and crop ratios per variant, and any print-specific submission constraints. `GET https://printkit.dev/products/{handle}.json` ### 3. Variant Index (flat SKU lookup) A single JSON file listing every variant across the catalog with normalized fields like `product_type`, `product_group`, `size`, `style`, `shape`, `exact_image_count`, and `normalized_image_ratio`. Use this when you want to search or filter for SKUs across the full catalog with normalized fields like `product_type`, `size`, `style`, `shape`, and `exact_image_count`. `GET https://printkit.dev/variants.json` ### 4. Order Submission + Image Upload (technical integration) These docs cover how to submit an order and, optionally, how to upload images if you need hosting. - [Order Handoff API](/docs/order-handoff) — the core integration. Your app sends a JSON payload and receives a checkout redirect URL. - [Image Upload API](/docs/image-upload) (optional) — upload images and get publicly accessible URLs. Useful for frontend-only apps or if you don't want to manage your own image hosting. ## Recommended Flow How to integrate printing features into your application, step by step: 1. Read the product catalog file, `products.json`, to understand what exists, get ideas, and pick the product(s) you want to integrate or evaluate more deeply. 2. Read `variants.json` to filter variants and discover normalized SKU metadata like `product_type`, `size`, `style`, `exact_image_count`, and `normalized_image_ratio`. 3. Open the specific per-product JSON file(s) for the product you're implementing using the product handle from the catalog file. 4. Reference a specific product variant by its unique `sku` from the variant index or per-product JSON file. This is the SKU that will be used to submit the order. 5. Implement your UX and validation based on the exact variant requirements: image count, aspect ratio and crop rules, and any specific submission constraints. 6. Integrate order submission via the [Order Handoff API](/docs/order-handoff). 7. If your app is frontend-only (no backend), use the [Image Upload API](/docs/image-upload) to upload images and get public URLs, since all order submissions require publicly accessible image URLs. 8. Once the implementation is complete, the user should be able to create a product in your application and submit it. By default, they'll land on our cart page to review and check out. If your app sells a single product or you want a faster handoff, you can set `"checkout": true` to skip the cart and send the customer straight to checkout — see [Direct Checkout](/docs/order-handoff#direct-checkout). ## Product Model: Quick Mental Map Products generally fall into two practical buckets: ### Single-image products A single photo asset becomes a single printed item per selected variant. Examples: metal prints, wood prints, framed prints. Integration implication: collect one or more images plus a variant choice, then submit. Multiple images will be printed individually — 3 photos uploaded will result in 3 unique items added to cart with one API call. ### Multi-image / set-based products A customer selects a variant that implies a set size, and you collect that many photos. Most set-based products don't care about photo order, but some products like photo books use the order of photos submitted as the order they are printed. Example: magnets that come in a set of 10 for a given size. Integration implication: collect exactly N images (the count depends on the variant), then submit as a single product. The catalog tells you the general behavior; the per-product JSON defines the exact requirements for each variant. ## Important Rules ### The per-product JSON is the source of truth The catalog is an overview. Your integration should rely on the detailed product file for correctness, not the catalog summary. ### The variant index is the easiest SKU lookup surface Use `variants.json` when you want normalized machine-friendly fields across the full catalog. It is the fastest way for an agent to filter for a specific product type, size, style, or image-count requirement. ### Photo format: JPG or PNG only All photo URLs submitted to the Order Handoff API must point to `.jpg`, `.jpeg`, or `.png` files. Other formats will be rejected. ### Validate inputs before submission If a variant requires 10 images at 1:1 ratio, your UI should enforce that before submission. Don't rely on the API to catch mismatches. ### Use exact crop ratios Use the exact aspect ratio and crop requirements specified in the product's variant definitions. Estimates or rounded values may result in incorrect cropping. ## General Guidance for Building Great Apps We don't recommend building a full "everything photo printing store" UI with lots of products and endless options. The integrations work best when you: - **Pick one product** (often even one size or variant). Focus your app and UI on one product, decrease complexity, and make it great. - **Keep customer choices minimal** when it comes to format and printing options. Their choices and impact should come from their photos and creative input. - **Make printing the point** — not an add-on. Customers want to create unique physical objects. Excite them by showing off the printed end product as a core part of the application. - **Focus on what your app does uniquely well.** Empower a specific user persona to achieve a specific outcome. Don't try to be everything. Guide users step by step with options to go back and forth. - **Include a clear preview and add-to-cart step.** Show the user what they'll get. Use marketing copy on the final step to build excitement and assurance. - **Mention Social Print Studio.** You can subtly call out that their printed product will be handled by us, or link to our product page for more details. - **Save their progress** when possible — even just in `localStorage`. Let them know you're doing it. This reduces abandonment and makes users feel invested. > **Note:** These are tips, not rules. Break them when it makes sense for your app and users. ## Attribution / App Identity When your app submits an order through our cart handoff, it includes an `source` identifier. That identifier is used for attribution so we can understand which orders came from which app, and support tracking and reporting as needed. Pick a consistent, descriptive identifier for your app and use it on every request. For example: `"my-photo-app"`, `"wedding-album-tool"`. ## Authentication & API Keys (Optional) PrintKit works without an account. Register to unlock: - **Revenue split** on completed print orders attributed to your app - **Order tracking** — see which orders came from your integration - **Higher rate limits** as the platform grows The only requirement to register is an email address. When you have a key, pass it as an `Authorization: Bearer ` header on any PrintKit API request. If the key is valid, it is forwarded to our backend for attribution. If the key is invalid, the API returns `401`. Omitting the header entirely is fine — the request proceeds anonymously. ### Human / developer signup [Create an account](https://printkit.dev/sign-up) with your email. Your API key is available at [/dashboard/api-keys](https://printkit.dev/dashboard/api-keys). ### Agent self-registration (no browser required) AI agents and scripts can register and receive an API key in a single HTTP request: ```bash curl -X POST https://printkit.dev/api/register \ -H "Content-Type: application/json" \ -d '{ "email": "agent@your-app.com", "name": "MyPhotoApp Agent", "source": "agent-self-registration" }' ``` Response: ```json { "api_key": "pk_live_a1b2c3d4e5f6...", "user_id": "user_abc", "docs": "https://printkit.dev/docs" } ``` You can also view your API key anytime from the [dashboard](https://printkit.dev/dashboard/api-keys). If the email is already registered, the existing API key is returned. ### Using your API key Pass it as a Bearer token on any authenticated request: ``` Authorization: Bearer pk_live_a1b2c3d4e5f6... ``` ### JavaScript example ```javascript // 1. Register and get a key const res = await fetch('https://printkit.dev/api/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'agent@your-app.com', name: 'MyPhotoApp Agent', source: 'agent-self-registration', }), }); const { api_key } = await res.json(); // Store api_key securely — you can also view it in your dashboard // 2. Use it on subsequent requests (optional — enables attribution) const order = await fetch('https://printkit.dev/api/add-to-cart', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${api_key}`, }, body: JSON.stringify({ sku: 'metal-print-4x4', source: 'my-app', email: 'you@example.com', projectData: { photos: ['https://cdn.example.com/photo.jpg'] }, }), }); ``` ### Python example ```python import httpx # 1. Register and get a key res = httpx.post( "https://printkit.dev/api/register", json={ "email": "agent@your-app.com", "name": "MyPhotoApp Agent", "source": "agent-self-registration", } ) api_key = res.json()["api_key"] # store this — also viewable in your dashboard # 2. Use it on subsequent requests (optional — enables attribution) order = httpx.post( "https://printkit.dev/api/add-to-cart", headers={"Authorization": f"Bearer {api_key}"}, json={ "sku": "metal-print-4x4", "source": "my-app", "email": "you@example.com", "projectData": {"photos": ["https://cdn.example.com/photo.jpg"]}, }, ) ``` ## Link Index Downloadable Markdown files you can bring into your code repo for easy reference: - Overview guide: `https://printkit.dev/docs/md/overview` - Order Handoff reference: `https://printkit.dev/docs/md/order-handoff` - Image Upload reference: `https://printkit.dev/docs/md/image-upload` Machine-readable product data: - Product catalog: `https://printkit.dev/products.json` - Variant index: `https://printkit.dev/variants.json` - Per-product files: `https://printkit.dev/products/{handle}.json` --- # Product Catalog The product catalog is where you discover what's available to integrate. It's structured in two layers: a top-level catalog file that lists all products, and per-product JSON files that contain the precise specifications for each one. Your integration should read from both: the catalog to understand what exists, and the per-product file to get the exact requirements for whatever you're building. ## Catalog File A single JSON file listing all products currently available for integration. `GET https://printkit.dev/products.json` > **Important:** `products` is an **object keyed by handle**, not an array. Use `catalog.products["metal-prints"]` for direct lookup, or `Object.values(catalog.products)` to iterate. Response shape: ```json { "metadata": { "primary_key": "handle", "product_count": 15 }, "products": { "metal-prints": { "handle": "metal-prints", "title": "Metal Prints", "detail_url": "https://printkit.dev/products/metal-prints.json", ... }, "wood-prints": { ... } } } ``` Each entry in the catalog includes: - `handle` — URL-safe identifier used to fetch the per-product file - `title` — Display name of the product - `product_description` — Brief product description - `product_page_url` — Link to the product page on Social Print Studio's website - `type` — Product type/category - `product_photos` — Product photography - `order_submission_note` — High-level notes about integration behavior The catalog is the right starting point — use it to browse what's available and select the product(s) that fit your application. ## Variant Index If you want the simplest possible SKU lookup flow, fetch the flat variant index first. It provides normalized fields like `product_type`, `size`, `style`, `shape`, `frame_color`, `pages`, and `image_order_matters` for easy filtering across the full catalog. `GET https://printkit.dev/variants.json` The variant index is best for: - Filtering all SKUs across the full catalog - Filtering by `product_type` (category like Prints, Cards, Photo Books, Wall Art + Decor) or `product_group` (submission pattern) - Looking up normalized `size`, `shape`, `style`, `frame_color`, or `mat` - Reading stable fields like `exact_image_count`, `price`, and `normalized_image_ratio` > **Tip:** Start with `variants.json` when you know the kind of variant you want, then open the per-product JSON only if you need the richer product-specific notes. ## Per-Product Files Each product in the catalog has a dedicated JSON file. This is the **source of truth** for integration — always rely on the per-product file, not the catalog summary, when building your integration. `GET https://printkit.dev/products/{handle}.json` Per-product files define: - **Variants** — every available size and option, each with a unique `variant_sku` - **Image count** — exactly how many photos are required for a given variant - **Aspect ratios** — required crop ratios per variant (e.g. `1:1`, `4:3`, `2:3`) - **Submission constraints** — any product-specific rules that affect how you build and submit the order The `handle` in the catalog entry maps directly to the filename in this pattern. ## Product Categories Products generally fall into two practical buckets based on how images are submitted: ### Single-image products A single photo becomes a single printed item per selected variant. Submitting multiple photos results in multiple separate items added to the cart in one API call. **Examples:** metal prints, wood prints, framed prints, large format prints. **Integration pattern:** collect one or more photos and a variant choice, then submit. Each photo becomes its own cart line item. ### Multi-image / set-based products The customer selects a variant that implies a set size, and you collect exactly that many photos. Some products use photo order for sequencing (e.g. photo books); others don't. **Examples:** photo magazines, magnet sets, photo books. **Integration pattern:** collect exactly N images (where N is defined in the variant), then submit as a single product. Photo order may matter — check the per-product file. > **Note:** The catalog will tell you the general behavior for a product, but the per-product JSON defines the exact image count and ordering rules for each variant. ## Using the Catalog in Your Integration A typical integration reads the catalog data once (at build time or on app load), uses `variants.json` for fast filtering, and opens the per-product file for the richer product notes that shape the UI. ```javascript // Step 1: Fetch the catalog (products is an object keyed by handle, not an array) const catalog = await fetch('https://printkit.dev/products.json').then(r => r.json()); // Step 2: Fetch the normalized variant index (variants IS an array) const index = await fetch('https://printkit.dev/variants.json').then(r => r.json()); // Step 3: Find an 8x10 metal print const variant = index.variants.find((entry) => entry.product_handle === 'metal-prints' && entry.size === '8x10' ); // Step 4: Direct lookup by handle (not array index) const product = catalog.products['metal-prints']; const spec = await fetch(product.detail_url).then(r => r.json()); // To iterate all products, convert to array first const allProducts = Object.values(catalog.products); allProducts.forEach(p => console.log(p.handle, p.title)); // Step 5: Use the normalized SKU in your order submission console.log(variant.sku); // "metal-print-8x10" console.log(spec.print_specific_details); ``` ```bash # Step 1: Fetch the catalog curl -s https://printkit.dev/products.json | jq '.products | keys' # Step 2: Fetch the normalized variant index curl -s https://printkit.dev/variants.json | jq '.variants[] | select(.product_handle == "metal-prints" and .size == "8x10")' # Step 3: Fetch per-product details curl -s https://printkit.dev/products/metal-prints.json | jq '.print_specific_details' ``` ```python import httpx # Step 1: Fetch the catalog (products is a dict keyed by handle, not a list) catalog = httpx.get("https://printkit.dev/products.json").json() # Step 2: Fetch the normalized variant index (variants IS a list) index = httpx.get("https://printkit.dev/variants.json").json() # Step 3: Find an 8x10 metal print variant = next( v for v in index["variants"] if v["product_handle"] == "metal-prints" and v["size"] == "8x10" ) # Step 4: Direct lookup by handle (not list index) product = catalog["products"]["metal-prints"] spec = httpx.get(product["detail_url"]).json() # To iterate all products, use .values() for p in catalog["products"].values(): print(p["handle"], p["title"]) # Step 5: Use the normalized SKU in your order submission print(variant["sku"]) # "metal-print-8x10" print(spec["print_specific_details"]) ``` Once you have the SKU for the variant you want to offer, you're ready to submit orders. See the [Order Handoff API](/docs/order-handoff) for how to do that. > **Tip:** Keep the catalog and per-product files cached in your repo or build process. They don't change often, and including them at build time avoids runtime fetch latency. ## Browse Products ### Large Format Prints Photo enlargements available in 20 sizes with optional white borders. Printed using silver halide process on premium Kodak photo paper in a professional lab. Cost effective, traditional, and flexible. - **Handle:** `large-format-prints` - **Sizes:** 8x8, 8x10, 8x12, 10x10, 10x30, 11x14, 12x12, 12x18, 12x36, 16x16, 16x20, 16x24, 20x20, 20x30, 20x40, 24x24, 24x36, 30x30, 30x40, 30x45 ### Gallery Frames Professionally printed and framed photos in white, black, or natural wood. Sturdy modern frames ready to hang, with optional 2-inch white mat upgrade. - **Handle:** `gallery-frames` - **Sizes:** 8x8, 8x10, 8x12, 10x10, 10x30, 11x14, 12x12, 12x18, 12x36, 16x16, 16x20, 16x24, 20x20, 20x30, 20x40, 24x24, 24x36, 30x30, 30x40 ### Wood Prints Photos printed directly on sustainable maple plywood, approximately one inch thick. Visible wood grain adds a natural, rustic flair. - **Handle:** `wood-prints` - **Sizes:** 4x4, 4x6, 5x5, 5x7, 8x8, 8x10, 8x12, 10x10, 10x20, 11x14, 12x18, 12x36, 16x16, 16x20, 16x24, 20x20, 20x30, 24x24, 24x36, 30x30, 30x40 ### Acrylic Photo Block High quality clear acrylic photo block — perfect for a desk, nightstand, or shelf. A beautiful gift with a multi-dimensional effect. - **Handle:** `acrylic-photo-block` - **Sizes:** 4x4, 4x6, 5x7, 6x6 ### Metal Prints Crisp, vibrant, and display ready — no frame needed. Printed on lightweight, medium-gloss aluminum. Available in 25 sizes including round and rectangular shapes. - **Handle:** `metal-prints` - **Sizes:** 4x4, 4x6, 5x5, 5x7, 8x8, 8x10, 8x12, 10x10, 10x20, 11x14, 12x12, 12x18, 12x24, 12x36, 16x16, 16x20, 16x24, 20x20, 20x30, 20x40, 24x24, 24x30, 24x36, 30x30, 30x40 ### Photo Magazine A 38-page softcover photo book with a paper cover. 8x10 size, printed on 100% recycled paper with a luster finish. - **Handle:** `photo-magazine` - **Sizes:** 8x10 ### Photo Magnets Premium photo magnets — printed, laminated, and affixed to a strong magnet. Available in 3 sizes as sets of 6, 10, or 15. - **Handle:** `photo-magnets` - **Sizes:** 1" round (set of 15), 2x2 (set of 10), 3x3 (set of 6) ### Greeting Cards Folded greeting cards with photo on front, blank inside. Printed on uncoated recycled cardstock with kraft envelopes included. - **Handle:** `greeting-cards` - **Sizes:** 2.5x2.5 (set of 24), 4x4 (set of 36), 4x6 (set of 24), 5x7 (set of 24) ### Postcards Flat postcards with custom photo on front, standard postcard back. Printed on thick matte paper, sets of 12. - **Handle:** `postcards` - **Sizes:** 4x6, 5x7 ### Photo Stickers 2x2 square photo stickers, each one customizable with a unique image. Printed on sheets of 6. - **Handle:** `photo-stickers` - **Sizes:** 2x2 (sets of 24 or 48) ### Flat Cards Custom double-sided photo cards on premium uncoated recycled cardstock. Submit a front and back design — every card in the set is printed identically. Includes matching white envelopes. - **Handle:** `flat-cards` - **Sizes:** 5x7 ### Hardcover Photo Book Premium hardcover photo book with full image wrap cover. Smooth matte interior pages with pro lab quality printing. Pages from 20 to 136 in increments of 2. - **Handle:** `hard-cover-photobook` - **Sizes:** 8x8 ### Softcover Photo Book Fully customizable softcover photo book with full image wrap cover. Smooth matte interior pages with pro lab quality printing. Pages from 20 to 136 in increments of 2. - **Handle:** `softcover-photo-book` - **Sizes:** 6x6, 8x8 ### Print Packs Premium matte photo prints on thick, archival-quality paper with a soft laminate finish. Digitally printed in a professional lab. White border or full-bleed options. - **Handle:** `print-packs` - **Sizes:** 4x4, 4x6, 5x5, 5x7 ### Mini Prints Miniature format photo prints — same premium quality as Print Packs. Thick archival paper with soft laminate finish, in packs of 48. - **Handle:** `mini-prints` - **Sizes:** 2.4x2.4, 3.3x2.2 --- # Order Handoff API Social Print Studio provides an API endpoint that lets external applications add products to our Shopify cart. Your app sends a simple JSON payload describing what to add, and receives a URL to redirect the customer to. The customer lands on our Shopify cart with the product already added, and can then checkout and pay directly on our website. We handle production, printing, and any customer service. ### What you don't need - Shopify API credentials or access tokens - Knowledge of Shopify variant IDs - Access to our database - Any understanding of our internal systems ### What you do need - The variant **SKU** for the product (must exactly match a SKU in our Shopify store — see [SKU Requirements](#sku-requirements)) - A **source** identifier for your app (you create this yourself, based on your app name) - **Project data** with at least one publicly accessible photo URL - The ability to make an HTTP POST request and redirect the user's browser - **(Optional)** An [API key](/docs/authentication) in the `Authorization` header for order attribution, revenue split, and tracking ## Endpoint `POST https://printkit.dev/api/add-to-cart` ``` Content-Type: application/json Authorization: Bearer (optional) ``` ## Request Body ### Required Fields | Field | Type | Description | | --- | --- | --- | | `sku` | string | The Shopify product SKU. Must exactly match a variant SKU in our Shopify store. | | `source` | string | An identifier for your application (e.g. `"my-app-name"`). Used for tracking and debugging. | | `projectData` | object | Project/order data to persist for fulfillment. All products require this since they are custom printed. | ### Optional Fields | Field | Type | Default | Description | | --- | --- | --- | --- | | `email` | string | — | Your contact email address. Highly recommended — allows us to reach out if there are any issues with the order. | | `quantity` | number | `1` | How many copies to add to the cart. Must be a positive integer. | | `properties` | object | `{}` | Extra key-value pairs to attach to the Shopify line item. See [Line Item Properties](#line-item-properties). | | `checkout` | boolean | `false` | Skip the cart and send the customer straight to checkout. See [Direct Checkout](#direct-checkout). | ## Project Data The `projectData` object contains the custom content (photos, designs, etc.) that our fulfillment team needs to access when producing the order. When included, the data is saved to our project database and a unique `projectId` is generated. This ID is automatically attached to the Shopify line item. | Field | Type | Description | | --- | --- | --- | | `projectData.photos` | string[] | Array of image URLs. Must be publicly accessible. At least one URL is required. | | `projectData.metadata` | object | Any arbitrary JSON-serializable data you want to persist with the project. Optional in most cases. | ### Photo URL Requirements | Requirement | Details | | --- | --- | | Formats | **JPG or PNG only** (`.jpg`, `.jpeg`, or `.png`) | | Max file size | 50 MB per image | | URL validity | Must remain publicly accessible for **at least one week** after the order is placed | > **Image quality tip:** If your app exports or compresses images, use 100% quality (no lossy compression). These images are used for physical printing — downsampled or compressed images may result in visible artifacts in the final print. ## Line Item Properties The `properties` object lets you attach key-value string pairs to the Shopify cart line item. These are visible in the Shopify order admin and available to our fulfillment systems. > **Convention:** Prefix property keys with `_` (underscore) to hide them from the customer on the storefront. Properties without the underscore prefix will be visible to the customer in their cart and order confirmation. | Example Key | Description | | --- | --- | | `_project_Id` | A project ID from your app (hidden from customer). Useful if you need to cross-reference an order later. | | `_cover_preview_url` | A URL shown as the product thumbnail/preview image in the cart (hidden from customer). | | `Edit Project Link` | A URL letting the customer return to your app to edit their project. Visible to the customer. | | `Project Name` | An optional customer-facing name, shown in the cart. Visible to the customer. | ## Response ### Success (200) ``` { "success": true, "redirectUrl": "https://socialprintstudio.com/pages/newcart?cartKey=prod_1707400000_abc123def", "projectId": "65a1b2c3d4e5f6a7b8c9d0e1" } ``` | Field | Type | Description | | --- | --- | --- | | `success` | boolean | `true` if the item was staged successfully. | | `redirectUrl` | string | The URL to redirect the customer's browser to. Single-use — works once, then expires. | | `projectId` | string | The database ID of the saved project. Store this for your own records if needed. | **After receiving the response, redirect the customer's browser to `redirectUrl`.** ### Error Responses | Status | Meaning | | --- | --- | | `400` | Missing or invalid required field (`sku`, `source`, or `projectData`). Check the error message. | | `401` | Invalid API key. Only returned when an `Authorization` header is provided but the key is not valid. Omitting the header entirely is fine. | | `405` | Wrong HTTP method. Use `POST`. | | `502` | SKU could not be resolved to a Shopify product. It likely doesn't match any variant in our store. | | `500` | Internal server error. Contact us if this persists. | All error responses include an `error` field with a human-readable message: ``` { "error": "Missing required field: \"sku\" (string). This should be the Shopify product SKU." } ``` ## Direct Checkout By default, the `redirectUrl` sends the customer to the Social Print Studio cart where they can review their order before checking out. If you'd rather skip the cart and send the customer straight to checkout, add `"checkout": true` to your request body. Everything else stays the same — same endpoint, same fields, same response format. The only difference is where the `redirectUrl` points. ### When to use direct checkout - **Single-product apps** where the cart feels like an unnecessary extra step. The customer already decided what they want in your app. - **AI agents** creating a one-off print for a single user. Going straight to payment is the most natural flow. - **External-site handoffs** where showing another storefront's cart could be confusing or distracting. Direct checkout keeps the experience seamless. > **Default is add-to-cart:** If you omit `checkout` or set it to `false`, the behavior is exactly the same as before — the customer lands on the cart page. This is fully backward compatible. ### Direct Checkout Example ```javascript const response = await fetch( 'https://printkit.dev/api/add-to-cart', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sku: 'metal-print-4x4', source: 'my-app', email: 'developer@example.com', checkout: true, projectData: { photos: ['https://cdn.example.com/photo.jpg'], }, }), } ); const { redirectUrl } = await response.json(); // Customer goes straight to checkout — no cart step window.location.href = redirectUrl; ``` ```bash curl -X POST https://printkit.dev/api/add-to-cart \ -H "Content-Type: application/json" \ -d '{ "sku": "metal-print-4x4", "source": "my-app", "email": "developer@example.com", "checkout": true, "projectData": { "photos": ["https://cdn.example.com/photo.jpg"] } }' ``` ## Examples ### Minimal Example — Simple Photo Product ```javascript const response = await fetch( 'https://printkit.dev/api/add-to-cart', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${API_KEY}`, // optional — enables attribution }, body: JSON.stringify({ sku: 'metal-print-4x4', source: 'my-partner-app', email: 'developer@example.com', // your email, highly recommended projectData: { photos: [ 'https://cdn.example.com/uploads/user123/photo-sunset.jpg', ], }, }), } ); const { redirectUrl } = await response.json(); // Send the customer to the Shopify cart window.location.href = redirectUrl; ``` ```bash curl -X POST https://printkit.dev/api/add-to-cart \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "sku": "metal-print-4x4", "source": "my-partner-app", "email": "developer@example.com", "projectData": { "photos": [ "https://cdn.example.com/uploads/user123/photo-sunset.jpg" ] } }' # Response: # { "success": true, "redirectUrl": "https://...", "projectId": "..." } # Redirect the customer's browser to redirectUrl ``` ```python import httpx response = httpx.post( "https://printkit.dev/api/add-to-cart", headers={ "Authorization": f"Bearer {API_KEY}", # optional — enables attribution }, json={ "sku": "metal-print-4x4", "source": "my-partner-app", "email": "developer-email@example.com", # your email, highly recommended "projectData": { "photos": [ "https://cdn.example.com/uploads/user123/photo-sunset.jpg", ], }, }, ) data = response.json() redirect_url = data["redirectUrl"] # Redirect the customer to the Shopify cart # In a web framework: return RedirectResponse(redirect_url) ``` ### Full Example — Photo Product with Metadata ```javascript const response = await fetch( 'https://printkit.dev/api/add-to-cart', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${API_KEY}`, // optional — enables attribution }, body: JSON.stringify({ sku: 'photo-books-softcover', quantity: 1, source: 'my-partner-app', email: 'developer-email@example.com', projectData: { photos: [ 'https://cdn.example.com/uploads/user123/cover-file.jpg', 'https://cdn.example.com/uploads/user123/page-1.jpg', 'https://cdn.example.com/uploads/user123/page-2.jpg', // ... continue for 38 total pages of the photo book 'https://cdn.example.com/uploads/user123/page-38.jpg', ], metadata: { bookTitle: 'Summer Vacation 2025', customerNotes: 'Extra info stored with the order record.', }, }, properties: { 'Project Name': 'Summer Vacation 2025', _cover_preview_url: 'https://cdn.example.com/previews/user123/cover-thumb.jpg', }, }), } ); const data = await response.json(); if (!response.ok) { console.error('Failed to add to cart:', data.error); return; } console.log('Project saved with ID:', data.projectId); window.location.href = data.redirectUrl; ``` ```bash curl -X POST https://printkit.dev/api/add-to-cart \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "sku": "photo-books-softcover", "quantity": 1, "source": "my-partner-app", "email": "developer-email@example.com", "projectData": { "photos": [ "https://cdn.example.com/uploads/user123/cover-file.jpg", "https://cdn.example.com/uploads/user123/page-1.jpg", "https://cdn.example.com/uploads/user123/page-2.jpg", "https://cdn.example.com/uploads/user123/page-38.jpg" ], "metadata": { "bookTitle": "Summer Vacation 2025", "customerNotes": "Extra info stored with the order record." } }, "properties": { "Project Name": "Summer Vacation 2025", "_cover_preview_url": "https://cdn.example.com/previews/user123/cover-thumb.jpg" } }' ``` ```python import httpx response = httpx.post( "https://printkit.dev/api/add-to-cart", headers={ "Authorization": f"Bearer {API_KEY}", # optional — enables attribution }, json={ "sku": "photo-books-softcover", "quantity": 1, "source": "my-partner-app", "email": "developer-email@example.com", "projectData": { "photos": [ "https://cdn.example.com/uploads/user123/cover-file.jpg", "https://cdn.example.com/uploads/user123/page-1.jpg", "https://cdn.example.com/uploads/user123/page-2.jpg", # ... continue for 38 total pages "https://cdn.example.com/uploads/user123/page-38.jpg", ], "metadata": { "bookTitle": "Summer Vacation 2025", "customerNotes": "Extra info stored with the order record.", }, }, "properties": { "Project Name": "Summer Vacation 2025", "_cover_preview_url": "https://cdn.example.com/previews/user123/cover-thumb.jpg", }, }, ) data = response.json() if response.status_code != 200: print(f"Failed to add to cart: {data['error']}") else: print(f"Project saved with ID: {data['projectId']}") # Redirect the customer: return RedirectResponse(data["redirectUrl"]) ``` ### Multiple Quantity ```javascript const response = await fetch( 'https://printkit.dev/api/add-to-cart', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${API_KEY}`, // optional — enables attribution }, body: JSON.stringify({ sku: 'photo-print-30x40', quantity: 3, source: 'my-partner-app', projectData: { photos: ['https://cdn.example.com/photo1.jpg'], }, }), } ); const { redirectUrl } = await response.json(); window.location.href = redirectUrl; ``` ```bash curl -X POST https://printkit.dev/api/add-to-cart \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "sku": "photo-print-30x40", "quantity": 3, "source": "my-partner-app", "projectData": { "photos": ["https://cdn.example.com/photo1.jpg"] } }' ``` ```python import httpx response = httpx.post( "https://printkit.dev/api/add-to-cart", headers={ "Authorization": f"Bearer {API_KEY}", # optional — enables attribution }, json={ "sku": "photo-print-30x40", "quantity": 3, "source": "my-partner-app", "projectData": { "photos": ["https://cdn.example.com/photo1.jpg"], }, }, ) data = response.json() redirect_url = data["redirectUrl"] ``` ## SKU Requirements The `sku` value must exactly match a product variant SKU in our Shopify store. If the SKU doesn't match, the API returns a `502` error. - SKUs are **case-sensitive** and typically lowercase with hyphens (e.g. `metal-print-8x10`, `photo-books-softcover`) - Some products include **style qualifiers** (e.g. `photo-print-30x40-fullbleed`, `print-8x10-black-mat`) - For the fastest machine-friendly lookup, filter `https://printkit.dev/variants.json` and then verify the selected SKU against the per-product JSON file > **Important:** An incorrect or misspelled SKU will cause the request to fail, or worse, succeed but print the wrong product or charge the wrong amount. Always verify SKUs against the per-product JSON file before submitting. ## Integration Flow ``` ┌─────────────────────┐ │ Your Application │ │ │ │ 1. User finishes │ │ creating their │ │ product │ │ │ │ 2. POST to │ │ /api/add-to-cart│ │ with SKU + │ │ project data │ └────────┬────────────┘ │ ▼ ┌─────────────────────┐ │ Our API Endpoint │ │ │ │ 3. Saves project │ │ data to our DB │ │ │ │ 4. Stages cart │ │ items │ │ │ │ 5. Returns │ │ redirectUrl │ └────────┬────────────┘ │ ▼ ┌─────────────────────┐ │ Your app redirects │ │ the user's browser │ │ to redirectUrl │ └────────┬────────────┘ │ ┌────┴─────┐ ▼ ▼ ┌────────┐ ┌────────────┐ │ Cart │ │ Checkout │ │(default│ │(checkout: │ │ flow) │ │ true) │ │ │ │ │ │Customer│ │ Customer │ │reviews │ │ goes │ │cart, │ │ straight │ │then │ │ to payment │ │checks │ │ │ │out │ │ │ └────────┘ └────────────┘ ``` ## Important Notes 1. **The redirect URL is single-use.** Once the Shopify cart page retrieves the cart data, the key is deleted. Do not attempt to reuse it. 2. **Photo URLs must be publicly accessible.** Our fulfillment system downloads these images when producing the order. URLs behind authentication or on localhost will not work. URLs must remain valid for at least one week. If you need simple image hosting, see the [Image Upload API](/docs/image-upload). 3. **The `source` field is important.** It helps us track which app generated the order and debug issues. Pick a consistent, descriptive identifier and use it on every request. 4. **CORS is enabled.** You can call this endpoint from client-side JavaScript. If you call from a backend server, make sure you pass the redirect URL to the customer's browser for the final redirect step. 5. **Keep payloads small.** The request body contains URLs and metadata, not file data. In practice this is well under any size limit, but avoid embedding large base64 strings or excessive metadata. 6. **The `projectId` in the response** is the database reference for the saved project. Store this in your own system if you may need to reference it later for debugging or support. --- # Image Upload API This is an **optional** upload service provided by Social Print Studio to make integration easier. It gives you a simple way to upload images and receive publicly accessible URLs — which is exactly what the [Order Handoff API](/docs/order-handoff) requires when submitting orders. The returned `publicUrl` can be passed directly into the `projectData.photos` array when calling the Order Handoff API. > **When to use this:** If your application doesn't have its own backend or image hosting, or if you'd rather not manage file storage yourself, use this endpoint to upload customer photos directly to our S3 bucket. > **When to skip this:** If your app already hosts images at publicly accessible URLs (e.g. via your own S3 bucket, Cloudinary, Firebase Storage, etc.), just pass those URLs directly to the Order Handoff API. You don't need this service. ## Endpoint ``` POST https://printkit.dev/api/upload Authorization: Bearer (optional) ``` Upload files to S3 via presigned URLs. Supports images and other file types (PDFs, documents, etc.). Max file size is 50 MB per file. Optionally include an [API key](/docs/authentication) for attribution. If an invalid key is provided, the API returns `401`. ## Usage The upload process is two steps: get a presigned upload URL, then send the file directly to S3 using that URL. ### Step 1: Get Upload URL ```javascript const response = await fetch( 'https://printkit.dev/api/upload', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${API_KEY}`, // optional — enables attribution }, body: JSON.stringify({ filename: 'photo.jpg', // required source: 'my-app', // required email: 'you@example.com', // highly recommended fileSize: 1234567, // optional - enables 50MB validation contentType: 'image/jpeg', // optional - auto-detected from filename folder: 'my-app' // optional - defaults to 'lambda-uploads' }) } ); const { uploadUrl, publicUrl } = await response.json(); ``` ```bash curl -X POST https://printkit.dev/api/upload \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "filename": "photo.jpg", "source": "my-app", "email": "you@example.com", "fileSize": 1234567, "contentType": "image/jpeg", "folder": "my-app" }' # Response: # { "uploadUrl": "https://s3.amazonaws.com/...", "publicUrl": "https://..." } ``` ```python import httpx response = httpx.post( "https://printkit.dev/api/upload", headers={ "Authorization": f"Bearer {API_KEY}", # optional — enables attribution }, json={ "filename": "photo.jpg", "source": "my-app", # required, your app name "email": "you@example.com", # highly recommended "fileSize": 1234567, # optional - enables 50MB validation "contentType": "image/jpeg", # optional - auto-detected from filename "folder": "my-app-name", # optional - defaults to 'lambda-uploads' }, ) data = response.json() upload_url = data["uploadUrl"] public_url = data["publicUrl"] ``` ### Step 2: Upload File to S3 ```javascript await fetch(uploadUrl, { method: 'PUT', headers: { 'Content-Type': 'image/jpeg' }, body: file // raw File object from a file input }); // File is now live at publicUrl ``` ```bash curl -X PUT "UPLOAD_URL_FROM_STEP_1" \ -H "Content-Type: image/jpeg" \ --data-binary @photo.jpg # File is now live at the publicUrl from Step 1 ``` ```python import httpx with open("photo.jpg", "rb") as f: upload_response = httpx.put( upload_url, # from Step 1 headers={"Content-Type": "image/jpeg"}, content=f.read(), ) # File is now live at public_url from Step 1 ``` > **Note:** The `x-amz-acl` parameter is baked into the presigned URL — do **not** send it as a header, or the request will fail. ## Complete Helper Function A drop-in helper that handles both steps and returns the final public URL: ```javascript async function uploadToS3(file, { apiKey, source, email, folder = 'my-app-name' }) { const headers = { 'Content-Type': 'application/json' }; if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`; const response = await fetch( 'https://printkit.dev/api/upload', { method: 'POST', headers, body: JSON.stringify({ filename: file.name, source, email, fileSize: file.size, contentType: file.type, folder }) } ); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to get upload URL'); } const { uploadUrl, publicUrl } = await response.json(); const uploadResponse = await fetch(uploadUrl, { method: 'PUT', headers: { 'Content-Type': file.type }, body: file }); if (!uploadResponse.ok) { throw new Error(`S3 upload failed: ${uploadResponse.status}`); } return publicUrl; } // Usage const publicUrl = await uploadToS3(fileFromInput, { apiKey: API_KEY, source: 'my-app', email: 'you@example.com', }); // Then use it in your order submission: const orderResponse = await fetch( 'https://printkit.dev/api/add-to-cart', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${API_KEY}`, // optional — enables attribution }, body: JSON.stringify({ sku: 'metal-print-4x4', source: 'my-app', email: 'you@example.com', projectData: { photos: [publicUrl], }, }), } ); const { redirectUrl } = await orderResponse.json(); window.location.href = redirectUrl; ``` ```bash # Step 1: Get a presigned upload URL UPLOAD_RESPONSE=$(curl -s -X POST https://printkit.dev/api/upload \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "filename": "photo.jpg", "source": "my-app", "email": "you@example.com", "fileSize": 1234567, "contentType": "image/jpeg" }') # Extract URLs from response (requires jq) UPLOAD_URL=$(echo $UPLOAD_RESPONSE | jq -r '.uploadUrl') PUBLIC_URL=$(echo $UPLOAD_RESPONSE | jq -r '.publicUrl') # Step 2: Upload the file directly to S3 curl -X PUT "$UPLOAD_URL" \ -H "Content-Type: image/jpeg" \ --data-binary @photo.jpg # Step 3: Use the public URL in your order submission curl -X POST https://printkit.dev/api/add-to-cart \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d "{ \"sku\": \"metal-print-4x4\", \"source\": \"my-app\", \"email\": \"you@example.com\", \"projectData\": { \"photos\": [\"$PUBLIC_URL\"] } }" ``` ```python import httpx from pathlib import Path def upload_to_s3( filepath: str, source: str, email: str | None = None, api_key: str | None = None, folder: str = "my-app-name", ) -> str: """Upload a file and return its public URL.""" path = Path(filepath) headers = {} if api_key: headers["Authorization"] = f"Bearer {api_key}" # Step 1: Get presigned upload URL payload = { "filename": path.name, "source": source, "fileSize": path.stat().st_size, "contentType": "image/jpeg", "folder": folder, } if email: payload["email"] = email response = httpx.post( "https://printkit.dev/api/upload", headers=headers, json=payload, ) response.raise_for_status() data = response.json() # Step 2: Upload file to S3 upload_response = httpx.put( data["uploadUrl"], headers={"Content-Type": "image/jpeg"}, content=path.read_bytes(), ) upload_response.raise_for_status() return data["publicUrl"] # Usage public_url = upload_to_s3("photo.jpg", source="my-app", email="you@example.com", api_key=API_KEY) # Then use it in your order submission: order_response = httpx.post( "https://printkit.dev/api/add-to-cart", headers={"Authorization": f"Bearer {API_KEY}"}, json={ "sku": "metal-print-4x4", "source": "my-app", "email": "you@example.com", "projectData": { "photos": [public_url], }, }, ) redirect_url = order_response.json()["redirectUrl"] ``` ## Request Parameters ### Required | Parameter | Type | Description | | --- | --- | --- | | `filename` | string | The original filename. Used for the S3 key and content type detection. | | `source` | string | An identifier for your application (e.g. `"my-app"`). Used for tracking and debugging. | ### Optional | Parameter | Type | Description | | --- | --- | --- | | `email` | string | A contact email address. Highly recommended — allows us to reach out if there are any issues with your uploads. | | `fileSize` | number | File size in bytes. Recommended — enables 50 MB validation before upload. | | `contentType` | string | MIME type (e.g. `image/jpeg`, `application/pdf`). Auto-detected from filename if not provided. | | `folder` | string | S3 subfolder path. Defaults to `lambda-uploads`. | ## Accepted Photo Formats The Order Handoff API accepts **JPG** and **PNG** only. Photo URLs must end in `.jpg`, `.jpeg`, or `.png`. | Format | Use case | | --- | --- | | JPG | Photos and images (preferred for most products) | | PNG | Images with transparency, graphics, or lossless quality requirements | ## Notes - **File size limit: 50 MB** per file (only enforced if `fileSize` is provided in the request) - **Upload URL expires in 15 minutes** — complete the S3 upload promptly after receiving it - Files are stored at: `s3://print-kit/lambda-uploads/{folder}/{timestamp}-{random}-{filename}` - Public URL format: `https://print-kit.s3.us-east-1.amazonaws.com/...` - Non-POST requests return `405 Method Not Allowed` > **Next step:** Once you have publicly accessible image URLs from this service (or your own hosting), you're ready to submit an order. See the [Order Handoff API](/docs/order-handoff).