name: Printful M2480 Crewneck Production description: >- Complete pipeline for Cotton Heritage M2480 (catalog 411) PREMIUM crewneck sweatshirts on Printful. Covers product creation, variant management, branding placement, mockup generation, and Supabase integration. Use when creating M2480 crewneck products, generating mockups, updating branding, or managing PREMIUM tier crewneck sweatshirts.
Printful M2480 Production Pipeline — PREMIUM Tier
Full production pipeline for SKAPARA PREMIUM crewneck sweatshirts on the Cotton Heritage M2480 blank via Printful API. This is the PREMIUM tier crewneck counterpart to the MC1087 PREMIUM tee and M2580 PREMIUM hoodie.
For MC1087 PREMIUM tees, see the printful-mc1087 skill.
For CC1717 SIGNATURE tees, see the printful-cc1717 skill.
For Printify-based products (legacy DTG on P26), see the design-dtg skill.
Product Specifications
| Property | Value |
|---|---|
| Blank | Cotton Heritage M2480 |
| Catalog ID | 411 |
| Tier | PREMIUM |
| Material | 100% cotton face / 65% cotton, 35% polyester (Charcoal Heather: 55% cotton, 45% polyester) |
| Fabric Weight | 8.5 oz/yd² (288.2 g/m²) |
| Fit | Classic, ribbed crew neck, long sleeve cuffs, flat hem, side-seamed, self-fabric patch on back |
| Sizes | S, M, L, XL, 2XL, 3XL |
| Colors (total) | 19 |
| Colors (DARK EU — design-first selection) | 6: Black, Navy Blazer, Charcoal Heather, Team Royal, Vintage Black, Forest Green |
| Colors (disabled — light/no-EU) | 13 (see VARIANTS.md) |
| Color selection | Design-first — analyze design palette, select 2-5 colors that maximize contrast |
| Print method | DTG (Direct-to-Garment) |
| Production facility | Printful Latvia (EU) |
Key differentiator vs MC1087 tee: M2480 has 8 placements including label_inside (same as M2580 hoodie). Sleeves are 450x1800px VERTICAL (NOT 600x525 like MC1087 tees). Front/back share the same 1800x2400 canvas as MC1087.
Key differentiator vs M2580 hoodie: M2480 is a crewneck (no hood, no kangaroo pocket). Same Cotton Heritage PREMIUM brand family. Shares the vertical sleeve canvas format (450x1800).
Placements & Dimensions
All canvases at 150 DPI unless noted:
| Placement | Printfile ID | Canvas (px) | DPI | Extra Cost | Notes |
|---|---|---|---|---|---|
front |
#1 | 1800 x 2400 | 150 | $0.00 (included) | Main design |
back |
#1 | 1800 x 2400 | 150 | +$5.25 | NOT USED (v3) — clean back |
sleeve_left |
#147 | 450 x 1800 | 150 | +$2.20 | SKAPARA wordmark 20% (VERTICAL) |
sleeve_right |
#147 | 450 x 1800 | 150 | +$2.20 | Unused currently |
label_outside |
#90 | 450 x 450 | 150 | +$2.20 | Neck label |
label_inside |
#212 | 750 x 750 | 300 | +$0.99 | Inside label (cheapest branding) |
embroidery_wrist_left |
#338 | 600 x 900 | 300 | — | Wrist embroidery |
embroidery_wrist_right |
#338 | 600 x 900 | 300 | — | Wrist embroidery |
IMPORTANT — SLEEVE DIFFERENCE:
- MC1087 tees: 600x525 (horizontal)
- M2480 crewnecks: 450x1800 (vertical) — requires NEW branding asset!
- The sleeve_left branding file_id from MC1087 (950410444) is 600x525 and CANNOT be reused for M2480
CONSTRAINT: back and label_outside are mutually exclusive in Printful. v3 does NOT use back (saves $5.25/unit), so label_outside is technically available but currently unused.
Base Costs
| Size | Base Cost (front only) |
|---|---|
| S | $22.09 |
| M | $22.09 |
| L | $22.09 |
| XL | $22.09 |
| 2XL | TBD |
| 3XL | TBD |
Some non-EU colors have slightly higher base cost ($22.25 for S-XL). Use the EU colors which are $22.09.
Additional placement costs stack on top of base cost. A product with front + back + sleeve_left costs:
- S-XL: $22.09 + $5.25 + $2.20 = $29.54
- 2XL: TBD + $5.25 + $2.20 = TBD
- 3XL: TBD + $5.25 + $2.20 = TBD
Printful API Authentication
ALL requests require these headers:
Authorization: Bearer ${PRINTFUL_API_TOKEN}
X-PF-Store-Id: ${PRINTFUL_STORE_ID}
Content-Type: application/json
Env vars are in frontend/.env.local:
PRINTFUL_API_TOKENPRINTFUL_STORE_ID
Rate Limits
| Endpoint | Limit | Recommended Delay |
|---|---|---|
| General API | ~120 req/min | 2000ms between calls |
| Mockup Generator | ~10 req/min | 10000ms between tasks |
| File uploads | ~30 req/min | 3000ms between uploads |
On HTTP 429: read x-ratelimit-reset header, wait that many seconds, retry.
Shared utility: For scripts, use import { createPrintfulClient } from './lib/printful-rate-limiter.mjs' — handles token bucket, 429 retry with jitter, proactive slowdown, and exponential backoff automatically.
File Upload Pattern
Printful requires files to be in its File Library before they can be used on products. Two upload methods:
Method 1: URL Upload (preferred for Supabase-hosted files)
curl -X POST https://api.printful.com/files \
-H "Authorization: Bearer ${PRINTFUL_API_TOKEN}" \
-H "X-PF-Store-Id: ${PRINTFUL_STORE_ID}" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-supabase.supabase.co/storage/v1/object/public/designs/my-design.png",
"filename": "my-design-front-1800x2400.png"
}'
Method 2: Multipart Upload (for local files)
curl -X POST https://api.printful.com/files \
-H "Authorization: Bearer ${PRINTFUL_API_TOKEN}" \
-H "X-PF-Store-Id: ${PRINTFUL_STORE_ID}" \
-F "file=@output/my-design-front-1800x2400.png" \
-F "type=default"
Response returns id (integer file_id) and url (CDN preview URL). Save both:
idis used in variant file placement updatesurlis used asimage_urlin mockup generation
Workflow 1: Create New M2480 Product
Step 1: Prepare Design Files
Design the main front canvas at 1800x2400px. Render branding assets per BRANDING.md.
Required files (v3 branding — no back):
- Front design — 1800x2400 PNG (main design)
- Sleeve left — 450x1800 PNG (SKAPARA wordmark vertical at 20% scale, file_id: 950653078)
CRITICAL: The sleeve_left branding asset for M2480 must be rendered at 450x1800 (vertical) with -resize 360x (20% width-based). See BRANDING.md for render commands. The MC1087 sleeve file (600x525) is INCOMPATIBLE.
v3 decision: No back branding (saves $5.25/unit). label_inside (S mark, file_id: 950649786) can only be added via Printful dashboard — API rejects it.
Step 2: Upload Design to Printful File Library
Upload the front design PNG via URL or multipart (see File Upload Pattern above). Save the returned file_id.
The sleeve_left branding file is already uploaded (20% scale wordmark):
sleeve_left: 950653078 (450x1800, SKAPARA wordmark vertical, verified 2026-03-03)
Step 3: Create Sync Product
curl -X POST "https://api.printful.com/store/products" \
-H "Authorization: Bearer ${TOKEN}" \
-H "X-PF-Store-Id: ${STORE}" \
-H "Content-Type: application/json" \
-d '{
"sync_product": {
"name": "Product Name — SKAPARA",
"thumbnail": "https://cdn.printful.com/.../design_preview.png"
},
"sync_variants": [
{
"variant_id": 11254,
"retail_price": "49.95",
"files": [
{"type": "default", "id": FRONT_DESIGN_FILE_ID},
{"type": "sleeve_left", "id": 950653078}
]
},
{
"variant_id": 11255,
"retail_price": "49.95",
"files": [
{"type": "default", "id": FRONT_DESIGN_FILE_ID},
{"type": "sleeve_left", "id": 950653078}
]
}
]
}'
IMPORTANT: Include ALL active variants for the colors selected via design-first analysis. Select 2-5 colors from the 6 DARK EU palette based on design contrast (see MOCKUPS.md — Design Palette Analysis). Each color x 6 sizes. See VARIANTS.md for complete variant ID table.
CRITICAL — Adding variants to existing products (POST /store/products/{id}/variants):
- Files MUST use
urlfield (NOTidalone) — using onlyidcauses"There can only be one file for each placement"error - Correct:
{ "type": "default", "url": "https://...design.png" } - Wrong:
{ "type": "default", "id": 950267047 }(fails with 400) - The initial product creation (
POST /store/products) can useid, but individual variant creation requiresurl
Pricing: Set retail_price on each variant. The margin fixer cron will overwrite if margin is below 35%, so set the correct retail price from the start. Recommended retail for PREMIUM crewneck: $49.95-$54.95.
Step 4: Generate Mockups
Follow MOCKUPS.md workflow. Generate Ghost mockups for all active dark colors with Front and Left views (v3 — no Back).
Step 5: Update Supabase
const ts = Math.floor(Date.now() / 1000)
// 1. Create or update product in Supabase
// CRITICAL: Set BOTH category (slug string) AND category_id (FK UUID)
// The breadcrumb uses `category` (string), the DB relations use `category_id`
await supabase.from('products').upsert({
id: productId,
title: 'Product Name',
description: 'Creative marketing description only. No specs here.',
translations: {
es: { title: 'Título en español', description: 'Descripción en español' },
de: { title: 'Titel auf Deutsch', description: 'Beschreibung auf Deutsch' }
},
category: 'crewnecks',
category_id: '3213a34b-0eeb-4195-8531-29e65f442384',
pod_provider: 'printful',
product_template_id: '411', // Cotton Heritage M2480 catalog ID
provider_product_id: String(pfProductId), // Printful sync product ID
base_price_cents: 4995,
compare_at_price_cents: 6499, // Original price (strikethrough) — must be > base_price_cents
images: [
// v3: Fronts first (hero), then sleeves. No backs.
{ src: `https://.../mockups/slug/black-front.png?v=${ts}`, alt: 'Product Name - Black' },
{ src: `https://.../mockups/slug/navy-blazer-front.png?v=${ts}`, alt: 'Product Name - Navy Blazer' },
{ src: `https://.../mockups/slug/black-sleeve.png?v=${ts}`, alt: 'Product Name - Black - Sleeve' },
{ src: `https://.../mockups/slug/navy-blazer-sleeve.png?v=${ts}`, alt: 'Product Name - Navy Blazer - Sleeve' },
],
product_details: {
safety_information: '<p><strong>Manufacturer:</strong> Printful Inc., Latvia</p>...',
material: '100% cotton face / 65% cotton, 35% polyester, 8.5 oz/yd²',
care_instructions: 'Machine wash cold, inside out. Tumble dry low. Do not bleach. Iron on low heat, avoid print area.',
print_technique: 'DTG (Direct-to-Garment)',
manufacturing_country: 'Latvia',
brand: 'SKAPARA',
model: 'Cotton Heritage M2480',
tier: 'PREMIUM',
fit: 'Classic / Ribbed Crew Neck'
},
status: 'active'
})
// 2. Select colors via DESIGN-FIRST ANALYSIS (see MOCKUPS.md)
// Analyze the design's color palette BEFORE choosing garment colors.
// DO NOT default to Black — e.g., a black penguin design is invisible on a black shirt.
// Select 2-5 colors from the DARK EU palette that maximize contrast with the design.
const DARK_EU_PALETTE = [
{ color: 'Black', hex: '#101010', L: 16 },
{ color: 'Navy Blazer', hex: '#171f2c', L: 30 },
{ color: 'Charcoal Heather', hex: '#3a3a38', L: 58 },
{ color: 'Team Royal', hex: '#2d407d', L: 65 },
{ color: 'Vintage Black', hex: '#43413D', L: 65 },
{ color: 'Forest Green', hex: '#335231', L: 69 },
]
// Example: Nihilist Penguin (black penguin silhouette + white text)
// Black → EXCLUDED (penguin invisible on #101010)
// Navy Blazer → OK (L=30, dark but penguin silhouette visible)
// Charcoal Heather → BEST (L=58, great contrast for black elements)
// Team Royal → GOOD (blue background, black penguin pops)
// Vintage Black → GOOD (L=65, olive-gray, decent contrast)
// Forest Green → GOOD (green, black penguin visible)
const selectedColors = DARK_EU_PALETTE.filter(c => /* design-first analysis */ true)
const sizes = ['S', 'M', 'L', 'XL', '2XL', '3XL']
for (const { color, hex, variantId } of activeVariants) {
for (const size of sizes) {
await supabase.from('product_variants').upsert({
product_id: productId,
color,
color_hex: hex,
size,
is_enabled: true,
external_variant_id: String(variantId), // Printful catalog variant ID
image_url: `https://.../mockups/slug/${colorSlug}-front.png?v=${ts}`
})
}
}
// 4. Disable light/no-EU color variants if they exist
await supabase
.from('product_variants')
.update({ is_enabled: false })
.eq('product_id', productId)
.in('color', ['White', 'Bone', 'Carbon Grey', 'Dusty Rose', 'Team Red',
'Cardinal', 'Adobe', 'Latte', 'Khaki', 'Agave',
'Light Pink', 'Lavender', 'Sky Blue'])
Step 6: GPSR Compliance
Every product MUST have GPSR data in product_details before going live. This is mandatory under EU Regulation 2023/988.
{
"safety_information": "<p><strong>Manufacturer:</strong> Printful Inc., Gandijas Dambis 15, Riga, Latvia LV-1045</p><p><strong>Material:</strong> 100% cotton face / 65% cotton, 35% polyester</p><p><strong>Weight:</strong> 8.5 oz/yd² (288.2 g/m²)</p><p><strong>Compliance:</strong> REACH, OEKO-TEX Standard 100</p>",
"material": "100% cotton face / 65% cotton, 35% polyester, 8.5 oz/yd² (288.2 g/m²)",
"care_instructions": "Machine wash cold, inside out. Tumble dry low. Do not bleach. Iron on low heat, avoid print area.",
"print_technique": "DTG (Direct-to-Garment)",
"manufacturing_country": "Latvia",
"brand": "SKAPARA"
}
Note: Charcoal Heather has a different composition (55% cotton, 45% polyester). If using that color, add a note in safety_information.
Workflow 2: Update Existing Product Branding
Use this when branding assets change or when adding branding to products that lack it.
Step 1: Get Current Product State
curl -s "https://api.printful.com/store/products/${SYNC_PRODUCT_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "X-PF-Store-Id: ${STORE}" | jq '.result.sync_variants[] | {id, name, files: [.files[].type]}'
Step 2: Render Updated Branding (if needed)
See BRANDING.md for ImageMagick render commands. Skip if using existing file IDs.
Step 3: Upload New Files (if needed)
Upload via POST /files. Save returned id.
Step 4: Update All Variants (Bulk)
curl -X PUT "https://api.printful.com/store/products/${SYNC_PRODUCT_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "X-PF-Store-Id: ${STORE}" \
-H "Content-Type: application/json" \
-d '{
"sync_variants": [
{
"id": SYNC_VARIANT_ID_1,
"files": [
{"type": "default", "id": FRONT_DESIGN_FILE_ID},
{"type": "sleeve_left", "id": 950653078}
]
},
...repeat for all active variants...
]
}'
Rate limit: delay(2000) between per-variant updates, delay(3000) between products.
Step 5: Regenerate Mockups
After branding update, regenerate mockups per MOCKUPS.md and update products.images[] in Supabase with fresh ?v=timestamp cache-busting.
Supabase Integration
Image URL Pattern
All mockup images stored in Supabase Storage under designs/mockups/{product-slug}/:
designs/mockups/{product-slug}/{color-slug}-front.png
designs/mockups/{product-slug}/{color-slug}-sleeve.png
Public URLs ALWAYS include cache-buster:
${SUPABASE_URL}/storage/v1/object/public/designs/mockups/{slug}/{color}-{placement}.png?v={timestamp}
Alt Text Convention (Critical for Frontend)
The buildVariantImageMap() function in product-detail-cache.ts parses alt text to map images to color variants:
| Placement | Alt Text Pattern | Example |
|---|---|---|
| Front | "Title - ColorName" |
"Nihilist Penguin - Black" |
| Sleeve | "Title - ColorName - Sleeve" |
"Nihilist Penguin - Black - Sleeve" |
Image order in products.images[]: Fronts (all colors) -> Sleeves (all colors). First image = hero for shop listing. No backs in v3 branding.
Known Issues & Gotchas
| Issue | Detail | Workaround |
|---|---|---|
| Temporary URLs | Mockup S3 URLs expire ~24h | Always download + re-upload to Supabase Storage |
| Python urllib blocked | Cloudflare rejects Python urllib against Printful | Use curl or Node.js fetch |
back vs label_outside |
Mutually exclusive — cannot use both | v3 does NOT use back (saves $5.25/unit) — label_outside available |
| Margin fixer | Cron sync overwrites prices if margin <35% | Set correct price in Printful FIRST |
| Cloudflare 403 | Missing User-Agent causes blocks | Always include User-Agent: POD-AI-Store/1.0 |
| Sleeve asset incompatible | MC1087 sleeve is 600x525, M2480 is 450x1800 | Render NEW vertical sleeve asset |
| Charcoal Heather material | 55% cotton / 45% polyester (different from other colors) | Note in GPSR if using |
| No UK region | M2480 ships EU + US but NOT UK | Acceptable for SKAPARA EU-first strategy |
| Cardinal no-EU | Cardinal red is NOT available in EU | Never include in EU products |
| Templates API | Returns NO explicit option_groups (all "default") | Extract views from URL paths |
| 2XL/3XL pricing | Base costs TBD for these sizes | Query API before production |
Variant Reference
See VARIANTS.md for the complete table of all 114 variants (19 colors x 6 sizes) with catalog variant IDs, hex codes, luminance values, EU availability, and status.
Branding Reference
See BRANDING.md for M2480-specific branding placements, vertical sleeve render commands, file IDs, and anti-patterns.
Mockup Reference
See MOCKUPS.md for M2480-specific mockup generation, template URL patterns, gallery structure, and rate limits.