Get 22 packs worth $978. Yours forever for $197 πŸ‘‰11Β days10Β hours56Β min53Β secClaim Deal - Just $197!
API

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.

DX
Developer Experience Team
Updated Aug 18, 2026 Β· 6 min read
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#

what-do-i-own.sh
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(","))"'
output
character	character-model-basic	source
icons	icons-everything	library_zip
pack	cozy-village	bundle,authoring

Three fields do the work:

  • product_type β€” what kind of thing it is. pack for an asset pack, character for a character set, icons for 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, or null when lifetime access or a free tier covers it instead. null means "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:

download-everything.sh
#!/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"
    done

Downloads 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:

character-source.sh
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_found for 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_unavailable means 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.

one-icon.sh
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 getIt means
404 not_foundNo such product, or one you cannot see: coming-soon, draft, archived, or a character set that is not on public sale.
403 not_entitledIt exists and is on sale β€” you have not bought it.
404 file_unavailableIt is yours, the file is not built yet.
403 insufficient_scopeThe 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#

Still stuck?

Ask us directly β€” we answer support mail ourselves.

Contact support
Share
Products and icons over the API β€” threejsassets.com