Downloading 40 props one at a time from a marketplace tab, saving each to the right folder, renaming files that all arrive as `model.glb` — that's an hour of clicking for work a script does in under two minutes. At the default page size of 24 results per call, browsing the full catalog by hand means paging through 36 screens before you've even started clicking downloads. BitSoul3D's REST API does the same job as the download button: three `GET` endpoints, one `POST`, one `DELETE`, and a single header. No OAuth flow, no SDK to install. Send `X-API-Key` on every authenticated call and get JSON back, or a binary GLB stream for the download endpoint. The catalog itself — 846 models right now, 747 of them covered by your monthly download quota — is searchable with no key at all.
Get your API key
Your key lives on the account dashboard, shown once on signup and re-viewable there afterward. It's prefixed `bsm_`, and API access ships with the Studio tier. The same key authenticates every private endpoint below — there's no separate scope per call, so treat it like a password rather than a public identifier. If a key ends up committed to a public repo, hit Regenerate on the dashboard — the old key stops working the moment you do, no support ticket needed.
Search and filter models with the REST API
![]()
`GET /api/models` takes four query params: `q` for free text, `category` (animals, architecture, characters, electronics, fantasy, food, furniture, industrial, misc, nature, vehicles, or weapons), `page`, and `limit` (max 100, default 24). Neither this endpoint nor `GET /api/models/:id` needs a key.
The response also carries a `pagination` object (`page`, `limit`, `total`, `pages`). At the max `limit` of 100, listing an 846-model catalog takes nine requests, not one — a real search script loops on `page` until it passes `pagination.pages` instead of assuming everything fits on one screen.
Filter before you loop, not after. The response carries a `premium` field: `0` means the model draws from your monthly quota, `1` means it's priced individually (check `price_cents`) and won't come out of that quota the same way. A batch job that doesn't check this field will happily queue up paywalled models next to free ones:
```bash
curl -s "https://bitsoulhosting.com/marketplace/api/models?category=weapons&limit=50" \
| jq -r '.models[] | select(.premium==0) | "\(.id) \(.name)"'
```
That prints id/name pairs for every standard weapon model — Sci fi mercenary assault rifle among them, a 60,000-triangle mesh that's 9.3 MB as a GLB.
Download models and stay inside your quota
![]()
![]()
`GET /api/models/:id/download` is the authenticated call, and it counts against your plan's monthly credits. The mechanics are identical across plans — only the ceiling changes:
| Plan | Monthly download credits | Price |
|---|---|---|
| Free | 2 | $0 |
| Indie | 25 | $12/mo |
| Pro | 100 | $29/mo |
| Studio | Unlimited | $79/mo |
A free account's two monthly downloads cover evaluation; commercial use is included with paid memberships — see pricing for the full breakdown.
Every plan shares the same three outcomes on this endpoint — `200` with the binary GLB, `401` for auth problems, `429` for quota problems — so a script only needs to branch on two failure cases, not a dozen. A missing or wrong key returns `401` with `{"error":"API key required. Set X-API-Key header."}`. Go past your ceiling and you get `429` with a `quota` object (`used`, `limit`, `remaining`) instead of a file, so check the status code before you assume the response body is a GLB:
```python
import requests, pathlib
KEY, out = "YOUR_API_KEY", pathlib.Path("assets")
out.mkdir(exist_ok=True)
for mid, name in models: # (id, name) pairs from the search step above
r = requests.get(f"https://bitsoulhosting.com/marketplace/api/models/{mid}/download",
headers={"X-API-Key": KEY}, stream=True)
if r.status_code == 429:
print("quota hit:", r.json()["quota"]); break
(out / f"{mid}.glb").write_bytes(r.content)
```
Eleven lines, and it fills an `assets/` folder overnight instead of a browser tab you have to babysit through 40 clicks.
What breaks when you script this
The quota resets on the first of the month, not on a rolling 30-day window from your last download — a job that dies at credit 2 on the 28th runs clean again on the 1st, no waiting. `GET /api/downloads/history` (param `limit`, 1–200, default 50) is the fastest way to check what already came down before you re-run a job and burn credits on duplicates.
Favoriting has a similar trap: `POST /api/favorites/:modelId` caps the free tier at 10 saved models. Past that it returns `403` with `upgrade_required:true` in the body, not a silent no-op, so a script that ignores status codes will think every favorite succeeded when half of them didn't. `DELETE /api/favorites/:modelId` has no such cap.
Two more fields worth checking before you queue a batch: `has_rigging` and `has_animations`. Filtering on those client-side, alongside `premium`, keeps a batch job for a character-animation test from downloading a dozen static props when only three models in the results are actually rigged.
For the export side of this pipeline, batch-exporting GLBs from Blender and tracking the binaries in Git LFS cover what happens after the script writes a file to disk. If your engine needs a custom step on import, too, Godot's EditorImportPlugin picks up right where this leaves off.
---
*This post is part of the Ultimate Guide to Free 3D Game Assets — BitSoul's complete reference for formats, texturing, rigging, optimization, and engine integration.*