CPL3D API
v1.0.0Read-only access to the CPL3D design catalogue: model metadata, print profiles, filament requirements and per-account entitlement. Built for filament planners, library managers and anything else that wants the catalogue without scraping it.
Released 2026-09-04 · See changelog
Overview
Everything here is a plain JSON API over HTTPS. There is no SDK to install and no scraping involved — nothing about the site's HTML matters to any of these endpoints.
- Model metadata — title, slug, description, dimensions, thumbnail
- Print profiles per model — file size, plate count, estimated print time
- Filament requirements — colour name, hex and weight in grams
- Pokemon attributes — Pokedex number, generation, region, tags
- Entitlement — the
ownedfield tells you whether the key's owner can download each model
Downloads are opt-in per key
downloads scope, fetch the actual .3mf files you own. Keys are metadata-only unless you ask for that scope when generating one, so no key issued before v1.1.0 can pull files. See the download endpoint.Authentication
Every request except /version carries your key as a bearer token:
Authorization: Bearer pk_your_key_here
Keys look like pk_ followed by 32 hex characters. They are long-lived and revocable, stored only as a SHA-256 hash on our side — which means we cannot show you a key again after you generate it. Copy it once, store it somewhere safe, and rotate it if you lose it.
Getting a key
Log in, then go to Dashboard → Tools → Models API. Generating a key requires an active subscription.
Never ship a key to a browser
Base URL & versioning
https://cpl3d.com/api/v1
The version is in the path. Within a major version we only ever add fields — we will not remove or rename a documented field, because integrations poll these endpoints unattended and a silent rename breaks them with no error anyone sees. Treat unknown fields as forward compatibility, not as a bug.
Check GET /version to confirm compatibility before you ask a user for a key — it needs no authentication for exactly that reason.
Access, tiers & limits
| Tier | Requests | Window |
|---|---|---|
| Member (no subscription) | 100 | Rolling 30 days |
| Subscriber | Unlimited | — |
Each account may hold 3 active keys at a time. Members without a subscription may additionally create at most 3 keys per rolling 30 days. Rotating a key does not count against that creation limit.
Usage headers
Every successful response reports your current usage, so you can back off before you hit a limit rather than discovering it through a 429:
X-Api-Calls-Used: 12 X-Api-Calls-Limit: 100 X-Api-Period-Resets: 2026-10-04T00:00:00.000Z
X-Api-Calls-Limit reads Unlimited for subscribers.
Scopes
A key carries one or more scopes, chosen when you create it. A request to an endpoint your key lacks the scope for returns 403.
models— included on every key. Catalogue metadata and/me.poke-balls— opt-in. Pokemon attributes and their filters.downloads— opt-in. Request download links for models you own.
Download limits
Separate from the request limits above: 100 distinct models per rolling 24 hours per account. Re-downloading a model you already pulled inside that window does not count, so a retry, a resume, or re-fetching a file you already have is free. The cap is per account, not per key, so two keys share one budget. GET /me reports what is left.
Endpoints
GET/version
Returns the current API version. No authentication required, and it does not count against any limit.
Example
curl "https://cpl3d.com/api/v1/version"
Response
{
"version": "1.0.0",
"released": "2026-09-04"
}GET/categories
The categories present across the approved catalogue, each with a display label. No authentication required — this is catalogue vocabulary, not account data, so a client can build its filter row before anyone has pasted a key.
Response
{
"categories": [
{ "id": "poke-ball", "label": "Pokéballs" }
]
}Pass an id as category on /library, or as type on /models. The list is derived from the catalogue rather than hardcoded, so a new product line appears here on its own. An unknown category returns an empty page rather than an error.
One category today
poke-ball. Read the list rather than assuming it — that is the whole reason this endpoint exists.GET/me
Confirms the presented key works and reports who it belongs to, what it may do, and live usage against both limits. Requires only the models scope, so a metadata-only key can still verify itself — it reports downloads_enabled: false rather than refusing.
Built for connection screens: show connected as X, 87 downloads left today instead of letting a user find the limit by hitting it. Never cached.
Example
curl "https://cpl3d.com/api/v1/me" \ -H "Authorization: Bearer pk_your_key_here"
Response
{
"display_name": "Chris",
"tier": "subscriber",
"key": {
"name": "LayerMate",
"prefix": "pk_1a2b3c4...",
"created_at": "2026-09-01T10:00:00.000Z",
"last_used_at": "2026-09-04T17:58:11.000Z",
"scopes": ["models", "downloads"],
"downloads_enabled": true
},
"usage": {
"requests_this_period": 342,
"request_limit": null,
"period_resets_at": "2026-10-01T10:00:00.000Z",
"downloads_today": 13,
"daily_download_limit": 100,
"downloads_remaining_today": 87
}
}request_limit is null when the account has no request cap. The three download fields are null when the key lacks the downloads scope.
GET/library
Everything your account can download — purchases, subscription grants, gifts and qualifying early access. Never the whole catalogue.
This is the endpoint a sync client wants
/library is open to any member who owns at least one model. /models browses the entire catalogue and needs a subscription. Getting the files you already bought is not the same request as browsing the shop, so they are not the same endpoint.Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | number | 1 | 1-indexed page number. |
limit | number | 50 | Items per page. Clamped to a maximum of 100. |
updated_since | string | — | ISO 8601 timestamp. Returns only models whose files_updated_at is later. An unparseable value is ignored rather than rejected. |
category | string | — | An id from /categories. type is accepted as an alias. |
Example
curl "https://cpl3d.com/api/v1/library?limit=50" \ -H "Authorization: Bearer pk_your_key_here"
Response
{
"models": [
{
"id": 412,
"name": "0143 Snorlax",
"slug": "0143-snorlax",
"thumbnail": "https://...supabase.co/.../0143-snorlax-thumbnail.webp",
"files_updated_at": "2026-08-20T11:02:31.000Z",
"owned": true,
"downloadable": true,
"estimated_print_time_seconds": 15200,
"total_weight_grams": 53.5,
"print_profiles": [
{
"id": 1841,
"name": "0143 Snorlax AMS.3mf",
"variant": "AMS",
"isCommunity": false,
"numberOfPlates": 2,
"estimatedPrintTime": 13500,
"totalWeightGrams": 53.5,
"filamentColors": [
{ "hex": "#1F3A5F", "name": "Navy Blue", "weight": 42.5 },
{ "hex": "#F2F2F2", "name": "Jade White", "weight": 11 }
]
}
]
}
],
"total": 145,
"page": 1,
"limit": 50,
"total_pages": 3,
"has_next": true
}Ordered by files_updated_at descending, so the most recently changed models come first. Admins and mods get their own real grants here, not the full catalogue — their blanket access still applies to individual downloads.
GET/models
A paginated list of approved models, each with its print profiles and an owned flag. Requires the models scope.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | number | 1 | 1-indexed page number. |
limit | number | 20 | Items per page. Clamped to a maximum of 100. |
search | string | — | Substring match on name and slug. Ignored below 2 characters. |
sort | string | name | name or created_at. |
sortDir | string | asc | asc or desc. |
type | string | — | Filter by model type. Currently only poke-ball. |
updated_since | string | — | ISO 8601 timestamp. Returns only models whose files_updated_at is later than this. An unparseable value is ignored rather than rejected. |
Example
curl "https://cpl3d.com/api/v1/models?limit=50&sort=created_at&sortDir=desc" \ -H "Authorization: Bearer pk_your_key_here"
Response
{
"models": [
{
"id": 412,
"name": "0143 Snorlax",
"slug": "0143-snorlax",
"description": "Print-tested Snorlax ball. AMS and SPLIT profiles included.",
"type": "poke-ball",
"thumbnail": "https://...supabase.co/.../0143-snorlax-thumbnail.webp",
"width_mm": 74.5,
"depth_mm": 74.5,
"height_mm": 78.2,
"created_at": "2026-08-14T03:21:55.000Z",
"files_updated_at": "2026-08-20T11:02:31.000Z",
"owned": true,
"print_profiles": [
{
"id": 1841,
"name": "0143 Snorlax AMS.3mf",
"variant": "AMS",
"isCommunity": false,
"fileSize": 18234491,
"numberOfPlates": 2,
"estimatedPrintTime": 13500,
"filamentColors": [
{ "hex": "#1F3A5F", "name": "Navy Blue", "weight": 42.5 },
{ "hex": "#F2F2F2", "name": "Jade White", "weight": 11.0 }
]
}
]
}
],
"total": 214,
"page": 1,
"limit": 50
}Only approved models are returned
GET/models/{id}
Full detail for one model: every approved profile with its id, variant, plate count, print time and per-filament breakdown. Open to any member who owns at least one model — detail for a single model is not the bulk-catalogue feature that /models is.
Example
curl "https://cpl3d.com/api/v1/models/412" \ -H "Authorization: Bearer pk_your_key_here"
Returns the same model object shape as /library, unwrapped. A model that does not exist and one that is not currently approved both return the same 404 body, so an id sweep cannot be used to discover models the listing endpoints hide.
GET/poke-balls
The same catalogue with Pokemon attributes attached — Pokedex number, generation, region and tags — and filters for them. Requires the poke-balls scope.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | number | 1 | 1-indexed page number. |
limit | number | 20 | Items per page. Clamped to a maximum of 100. |
tags | string | — | Comma-separated tag list. Matches models carrying all of them. |
generation | string | — | Filter by Pokemon generation. |
region | string | — | Filter by region. |
hasExtraBlack | boolean | — | Models needing black filament beyond the core. |
hasExtraWhite | boolean | — | Models needing white filament beyond the button. |
sort | string | name | name or pokedex_num. |
Example
curl "https://cpl3d.com/api/v1/poke-balls?generation=1&sort=pokedex_num" \ -H "Authorization: Bearer pk_your_key_here"
GET/models/{id}/download
Requests a one-time download link for one print profile. Returns JSON, not bytes. Requires the downloads scope and at least one owned model on the account.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
profile | number | string | first | The profile id, or its exact name (case-insensitive), from a model's print_profiles. Omit for the first profile in display order, which is the AMS variant where one exists. |
Response
{
"download_url": "https://cpl3d.com/api/v1/download/8Kx2...e2",
"expires_in": 300,
"expires_at": "2026-09-04T18:05:00.000Z",
"one_time_use": true,
"profile": { "id": 1841, "name": "0143 Snorlax AMS.3mf" }
}Example
# 1. Ask for a link (returns JSON, not bytes) curl "https://cpl3d.com/api/v1/models/412/download?profile=AMS" \ -H "Authorization: Bearer pk_your_key_here" # 2. Fetch it. No auth header. Works once, for 5 minutes. curl -L -o snorlax-ams.3mf \ "https://cpl3d.com/api/v1/download/8Kx2...e2"
Treat the link as a credential
Errors
403— not entitled to this model, the key lacks thedownloadsscope, or the account owns nothing404— model or profile not found, or the model has no approved profiles429— the 100-distinct-models-per-24h cap503— API downloads temporarily disabled
GET/download/{token}
Serves the watermarked .3mf. You do not construct this URL — use the download_url from the endpoint above verbatim. No Authorization header; follow redirects and read the filename from Content-Disposition.
The token names exactly one file and is spent the moment it is used, before any bytes are sent. Entitlement is checked again here, not just when the link was minted, so a refund or a cancelled subscription in the intervening minutes still denies the download. Query parameters are ignored entirely: nothing you append can change which file comes back.
Errors
410— already used, expired, or never valid. Mint a new link; do not retry this one.403— entitlement changed since the link was created
Every download is watermarked
Field reference
Model object
| Field | Type | Description |
|---|---|---|
id | number | Stable numeric identifier. Use this in later calls. |
name | string | Display name. |
slug | string | URL-friendly identifier. |
description | string | null | Long-form description. |
type | string | null | Model type, e.g. poke-ball. |
thumbnail | string | null | Publicly readable cover image URL (webp). |
width_mm | number | null | Bounding width in millimetres. |
depth_mm | number | null | Bounding depth in millimetres. |
height_mm | number | null | Bounding height in millimetres. |
created_at | string | ISO 8601 timestamp of first publish. |
files_updated_at | string | When this model's print profile files last changed. Does not move for metadata-only edits. Pair it with updated_since to poll for changes — see Syncing a library. |
owned | boolean | Whether the key's owner may download this model — a purchase, a subscription grant, or a free model. Reflects download access only, not commercial licensing. |
print_profiles | array | Every profile for this model, in display order. |
Print Profile object
| Field | Type | Description |
|---|---|---|
name | string | Filename including extension. The variant is encoded here — AMS, SPLIT, MC or U1 as a standalone token. |
id | number | Pass this as profile to the download endpoint. Stable, unlike the name. |
variant | string | null | AMS, SPLIT, MC, U1, or null. Derived from the name, matched on a whole token, so you do not have to parse filenames. |
isCommunity | boolean | A member-uploaded profile we reviewed and approved but did not produce or test ourselves. |
fileSize | number | null | Size in bytes. Null if not yet recorded. |
numberOfPlates | number | Build plates required. |
estimatedPrintTime | number | Estimated print time in seconds. |
filamentColors | array | Filament breakdown for this profile. |
Filament Color object
| Field | Type | Description |
|---|---|---|
hex | string | Hex colour, e.g. #1F3A5F. Draw a swatch without loading an image. |
name | string | Colour name as labelled in the profile. |
weight | number | Grams of this colour required. |
A model's total weight is not returned as its own field — sum weight across a profile's filamentColors.
Errors
Every error response on every endpoint is JSON with a single error string, safe to show a user. Never an HTML error page.
{ "error": "Invalid or revoked API key." }| Status | Meaning |
|---|---|
| 400 | Invalid parameters. |
| 401 | Missing, invalid or revoked API key. |
| 403 | No active subscription, missing scope, or account suspended. |
| 429 | Rate limit exceeded. Includes a Retry-After header. |
| 500 | Server error. Safe to retry once. |
A 429 carries Retry-After in seconds. Respect it and stop the in-flight batch rather than continuing to retry.
Syncing a library
If you are building something that keeps a local copy of a member's files up to date, this is the loop to write.
- Call
GET /meonce to confirm the key works and read the download allowance. Show the member which account they connected. - Page
GET /modelsto build the initial list. Keep the largestfiles_updated_atyou saw. - Download what the member wants: one
GET /models/{id}/downloadper profile, then follow the returned link. Process serially, and stop the batch on a429rather than continuing. - To poll later, call
GET /models?updated_since=<that timestamp>. An emptymodelsarray means nothing changed. Do not re-list the catalogue.
On the update signal
files_updated_at moves only when a model's actual files change — a profile added, replaced, renamed or removed. Editing a description, a filament colour or a thumbnail does not move it, and a member-submitted profile does not move it until it has been reviewed and is actually downloadable. So if it changed, there is genuinely something new to fetch.Backfill caveat, stated plainly: models published before this field existed carry their first-publish date. We did not invent a history we do not have, so a model last re-uploaded in 2025 reports 2025's publish date rather than the re-upload. Every change from v1.2.0 onward is exact.
Poll politely
Changelog
- First public release. GET /v1/models and GET /v1/poke-balls were already live; this is the first version to document them, pin a version to them, and commit to not breaking them without a major bump.
- GET /v1/version - unauthenticated, so a client can check compatibility before it has a key.
- GET /v1/me - key introspection. Confirms a pasted key works and reports live request and download usage, so an app can show what is left instead of discovering limits through errors.
- GET /v1/library - everything your account can download, paginated, with updated_since cursoring. Open to any member who owns at least one model; browsing the full catalogue via /v1/models remains a subscriber feature.
- GET /v1/models/{id} - full detail for one model: every approved profile with its id, variant, plate count, print time and per-filament breakdown.
- GET /v1/categories - the categories present across the approved catalogue, each with a display label. Derived from the catalogue, so a new product line appears automatically.
- File downloads. GET /v1/models/{id}/download returns a single-use link that expires in 5 minutes; fetch it with a plain unauthenticated GET to receive the watermarked .3mf. Requires the new downloads scope, which is opt-in per key - a key is metadata-only unless you ask for it.
- Per-account download cap of 100 distinct models per rolling 24 hours. Re-downloading a model you already pulled in that window does not count.
- files_updated_at on every model object - when the model's print profile FILES last changed. A description or filament edit does not move it, so an update signal built on this field does not cry wolf. Pair it with updated_since to poll for changes.
- variant, id and isCommunity on each print profile, plus total_weight_grams and estimated_print_time_seconds on model objects - previously every client had to parse filenames and sum filaments itself.
- API keys are no longer subscriber-only: any member who owns at least one model can create one. Request caps still differ by tier; download access follows entitlement.
- LayerMate compatibility endpoints under /v1/compat/layermate/ implementing the LayerMate Auto-Download Source API v1.0. A translation over the endpoints above; the native API remains canonical.