CPL3D API
v1.1.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-10-10 · 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.0.0 can pull files. Downloads also only work inside approved apps, currently LayerMate and EdgeSlicer. 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://www.cpl3d.com/api/v1
Keep the www. cpl3d.com without it returns a 301 to www.cpl3d.com, and HTTP clients drop the Authorization header across a cross-host redirect rather than leak a credential to another origin. Your request then arrives with no key and we answer 401 Missing API key — which looks like a rejected key but is a lost header. If you are getting a 401 you cannot explain, check the host first.
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://www.cpl3d.com/api/v1/version"
Response
{
"version": "1.1.0",
"released": "2026-10-10"
}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://www.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://www.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://www.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://www.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://www.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, at least one owned model on the account, and an approved app.
Downloads only work in approved apps
403 with code: "app_not_approved". Browsing and metadata endpoints are not affected. To download your files outside an approved app, use the website.Approved apps
- LayerMate — add CPL3D as an Auto Download source in LayerMate and paste an API key with the
downloadsscope. - EdgeSlicer — add CPL3D as a custom model source and paste an API key with the
downloadsscope. See Using CPL3D in EdgeSlicer.
Neither needs an app key: the member's API key is enough.
Building an app and want downloads approved? Contact us with what your app does. Approved apps receive an app key to send in an X-App-Key header next to the member's Authorization header. Keep it server-side or otherwise private: a leaked app key is revoked, and your users lose downloads until a new one is issued.
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://www.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://www.cpl3d.com/api/v1/models/412/download?profile=AMS" \ -H "Authorization: Bearer pk_your_key_here" \ -H "X-App-Key: app_your_app_key_here" # 2. Fetch it. No auth header. Works once, for 5 minutes. curl -L -o snorlax-ams.3mf \ "https://www.cpl3d.com/api/v1/download/8Kx2...e2"
Treat the link as a credential
Errors
403— not entitled to this model, the key lacks thedownloadsscope, the account owns nothing, or the request is not from an approved app (code: "app_not_approved")404— 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.
The link only works from the IP address that requested it (for IPv6, the same /64). Request and fetch it from the same machine. A link requested on a server and opened somewhere else is refused and spent.
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, or the link was fetched from a different IP than the one that requested it (code: "ip_mismatch"). Mint a new link from the machine that will download the file.
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 | null | Estimated print time in seconds, or null when the profile has never been sliced (Snapmaker U1 files produced by conversion carry the project settings, not a slice). Treat null as unknown rather than zero. |
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.0.0 onward is exact.
Poll politely
Using CPL3D in EdgeSlicer
EdgeSlicer can sync your CPL3D library and download the files you own. Create an API key on your dashboard with the models and downloads scopes, then add a custom model source in EdgeSlicer and fill it in as below. Leave any field not listed empty. No app key is needed.
Basics and sign-in
| Field | Type | Description |
|---|---|---|
API address | Basics | https://www.cpl3d.com |
Type | Sign-in | Bearer token |
Header or parameter name | Sign-in | Authorization |
API key or token | Credentials | Your pk_ key, via Change |
Extra headers | Sign-in | None |
Model list and pages
| Field | Type | Description |
|---|---|---|
Path | Model list | /api/v1/library |
Items in the answer at | Model list | models |
Paging | Pages | Page number |
Page, offset or cursor parameter | Pages | page |
First page | Pages | 1 |
Page size parameter | Pages | limit |
Page size | Pages | 100 |
"More pages" flag at | Pages | has_next |
Total count at | Pages | total |
Only changes since: parameter | Pages | updated_since |
Compared with item field | Pages | files_updated_at |
Item fields
| Field | Type | Description |
|---|---|---|
Id | Item fields | id |
Name | Item fields | name |
Thumbnail URL | Item fields | thumbnail |
Page URL | Item fields | https://www.cpl3d.com/dashboard/models/poke-ball/{slug} |
Tags or category | Item fields | type |
Updated | Item fields | files_updated_at |
Description | Item fields | description |
Files of an item
| Field | Type | Description |
|---|---|---|
Files at | Files | print_profiles |
Id | Files | id |
Name | Files | name |
Variant | Files | variant |
Size (bytes) | Files | fileSize |
Plates | Files | numberOfPlates |
Print time (s) | Files | estimatedPrintTime |
Colours list | Files | filamentColors |
Colour in each | Files | hex |
Download and quota
| Field | Type | Description |
|---|---|---|
Endpoint path | Download | /api/v1/models/{id}/download |
File link in its answer at | Download | download_url |
Downloads count against a limit | Download | Ticked |
Used | Quota headers | X-Api-Calls-Used |
Limit | Quota headers | X-Api-Calls-Limit |
Resets | Quota headers | X-Api-Period-Resets |
Keep the key in the credential store
pk_ key only into Credentials → Change, never into the header name or an extra header. That keeps it in your system's credential store rather than in EdgeSlicer's settings file. If it ever ends up somewhere else, revoke it on your dashboard and make a new one.Downloads count toward the 100-distinct-models-per-24-hours cap, and each link works once, from the machine that asked for it.
Changelog
- EdgeSlicer is supported. Add CPL3D as a custom model source and paste an API key with the downloads scope; EdgeSlicer syncs your library and downloads your files with no app key. See "Using CPL3D in EdgeSlicer".
- Supported clients that cannot send an app key, currently LayerMate and EdgeSlicer, can call GET /v1/models/{id}/download with just the member's API key. Any other program still needs an approved app's X-App-Key; nothing changes for apps that already send one.
- Breaking: GET /v1/models/{id}/download now needs an approved app. Send the app key CPL3D issued you in an X-App-Key header next to the member's Authorization header. Without one the endpoint returns 403 with code "app_not_approved". A member's API key alone can no longer request download links. Every other endpoint is unchanged and needs no app key.
- LayerMate is an approved app: downloads through its CPL3D Auto Download source keep working with just a member's API key.
- Breaking: a download link from /v1/download/{token} now only works from the IP address that requested it (for IPv6, the same /64). A link fetched from anywhere else returns 403 with code "ip_mismatch" and is spent. Request and fetch links from the same machine.
- 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.