Products and icons over the API
List everything your account owns β packs, character sets, the icons library β from a script or an AI assistant, and pull icons and source bundles.
On this page (7)
We sell more than asset packs now, and /v1/packs only ever knew about packs. /v1/products is the endpoint that knows about everything: asset packs, character sets, the icons library, and whatever we add next. One call answers "what does this account own, and what can it download right now?" β whichever kinds of thing the answer contains.
Connected AI assistants get the same picture through list_purchased_products. Same inventory, same rules, different doorway.
One list, every kind of thing#
curl -s https://threejsassets.com/api/v1/products \
-H "Authorization: Bearer $TJA_API_KEY" \
| jq -r '.data[] | "\(.product_type)\t\(.slug)\t\(.deliverables | map(.edition) | join(","))"'character character-model-basic source
icons icons-everything library_zip
pack cozy-village bundle,authoringThree fields do the work:
product_typeβ what kind of thing it is.packfor an asset pack,characterfor a character set,iconsfor the icons library.deliverablesβ the download editions that exist right now, each with an absolute URL and a size. Fetch the URL as given rather than assembling one; that way the download path can change without breaking your script.purchased_atβ the date you bought it, ornullwhen lifetime access or a free tier covers it instead.nullmeans "no separate order", not "date missing". Do not sort on it without handling that.
Add ?type=character to narrow to one kind. It is matched exactly, so a typo returns an empty list rather than quietly handing back packs.
/v1/me gives you the same shape as a headline: product_count, plus products_by_type β {"pack": 3, "icons": 1}. That object is sparse, so a kind you own nothing of is simply absent rather than present with a 0. Read it with a default, not with a direct key access.
/v1/packs and /v1/assets have not changed and are not going anywhere. If a pack library is all you need, keep using them; /v1/products is what you reach for when the answer might include something that is not a pack.
An empty deliverables is not an error#
A product you own with no editions listed means the files are not built yet β a character set whose bundle is still in preparation, for example. The array is the honest answer to "what can I fetch", so branch on it:
#!/usr/bin/env bash
set -euo pipefail
API="https://threejsassets.com/api/v1"
AUTH="Authorization: Bearer ${TJA_API_KEY}"
curl -s "$API/products?limit=100" -H "$AUTH" \
| jq -r '.data[] | .slug as $s | .deliverables[] | "\($s)\t\(.edition)\t\(.url)"' \
| while IFS=$'\t' read -r slug edition url; do
echo "β ${slug} (${edition})"
curl -L --fail -C - -H "$AUTH" -o "${slug}-${edition}.zip" "$url"
doneDownloads are rate limited more tightly than metadata reads, so pull them one at a time as above.
Character sets: the source bundle#
A character set's deliverable is its editable source bundle, and it arrives as edition: "source" with a URL ending in ?edition=source. Use that URL and you are done:
curl -L --fail \
-H "Authorization: Bearer $TJA_API_KEY" \
-o character-model-basic-source.zip \
"https://threejsassets.com/api/v1/products/character-model-basic/download?edition=source"Two things behave differently from a pack, both deliberately:
- A character set that is not on public sale is a
404 not_foundfor everybody, including someone who bought it. If a set you own stops resolving, that is what happened; your purchase is intact and Account β Downloads is the place to check. 404 file_unavailablemeans the product is yours but its bundle is not built yet β retry later rather than changing your request.
Everything else is the same as a pack download: a Content-Disposition filename, a strong ETag, and Accept-Ranges: bytes so a half-finished transfer resumes with curl -C -.
Icons: search, then fetch#
Icons come in three sizes of request.
Browsing needs no key. /v1/catalog/icons takes q and category and answers with names, tags, page URLs and a 256px preview image. It never carries the artwork, for anyone, at any entitlement level β there is no account on an anonymous request to check.
One icon, with the artwork, needs downloads:read. /v1/icons/:name returns the metadata plus a body β the <path> markup β when your account is entitled to that icon. When it is not, the body key is simply absent. That presence is the entitlement answer; there is no separate flag to read.
curl -s "https://threejsassets.com/api/v1/icons/arrow-up" \
-H "Authorization: Bearer $TJA_API_KEY" | jq '.data | has("body")'A file comes from /v1/icons/:name/download, with format=svg (the default) or format=png, a size from the allowed list, and an optional color. Ask for something unsupported and you get the default rather than an error β see the endpoint reference for the exact values.
An assistant does the same two steps with search_icons and get_icon. One extra rule applies there: get_icon hands over the SVG only when your account is entitled to the icon and that connection was granted Create download links. An assistant that can browse but not download gets the preview URL and a reason instead β the artwork rides the download permission, because artwork is a download.
The library ZIP is gated on coverage, not ownership#
/v1/icons/library streams the whole library as a single ZIP, and its rule is worth knowing before it surprises you: you get it when your entitlements cover every icon in the file, because the ZIP is cut from the complete library.
So if a premium icon set lands that your purchase does not include, the ZIP starts answering 403 not_entitled β while every individual icon you own keeps downloading from /v1/icons/:name/download exactly as before. That is the safe direction rather than a bug: a bulk file that quietly included icons nobody bought would be worse.
If you want an offline copy that survives that, loop the icons you own instead of relying on the ZIP.
Refusals, and what each one means#
| You get | It means |
|---|---|
404 not_found | No such product, or one you cannot see: coming-soon, draft, archived, or a character set that is not on public sale. |
403 not_entitled | It exists and is on sale β you have not bought it. |
404 file_unavailable | It is yours, the file is not built yet. |
403 insufficient_scope | The key is missing downloads:read. Create a new key with it enabled. |
The download endpoint refuses in exactly the same words as the detail endpoint, so /v1/products/:slug is a safe way to check before you fetch. Full table in API errors and rate limits.
Next steps#
- API endpoint reference β every route, parameter, and field.
- Connect an AI assistant β the same inventory in a chat.
- Your account library β the browser version of all of this.
Ask us directly β we answer support mail ourselves.