API Reference
/
External Users API
External Users API
Resellers embed Moshpit and serve their own customers. Each of those customers is an external user — a tenant you provision through your integration, give a per-customer storage and scene allocation, and bill yourself. These four endpoints manage that lifecycle. For the conceptual model and a worked walkthrough, start with the reseller / multi-tenant guide.
REST API access
REST API access is included on Free, Pro, and Enterprise. Storage, Scene counts, and integration limits still apply.
Auth
Every external-user endpoint requires all three headers. The
X-Moshpit-External-User-Id header is mandatory here — it names the customer
the request operates on, and it is your own opaque identifier for that customer,
not a Moshpit login:
Authorization: Bearer msk_YOUR_SECRET_KEY
X-Moshpit-Public-Key: mpk_YOUR_PUBLIC_KEY
X-Moshpit-External-User-Id: YOUR_CUSTOMER_IDRequests missing X-Moshpit-External-User-Id return 400. The value is your own
string (up to 256 characters, no line breaks); Moshpit treats it as opaque and
scopes the customer record to { integration, externalUserId }.
The secret stays on your backend
These endpoints authenticate with the msk_ secret. Call them from your
server — provisioning, raising allocations, and reading per-customer usage are
back-office operations. Never expose the secret to a browser.
The externalUser object
PUT and GET return the customer record under externalUser. DELETE returns
the final state of the record (or null).
| Field | Type | Description |
|---|---|---|
id | string | The Moshpit record id (MongoDB ObjectId) for this customer. |
integrationId | string | The embed integration this customer belongs to. |
externalUserId | string | Your own identifier for the customer — the value you sent in `X-Moshpit-External-User-Id`. |
productSlug | string | The integration's product boundary (its saved Site domain). |
displayName | string | null | Optional human label you set for the customer. |
avatarUrl | string | null | Optional avatar URL you set for the customer. |
emailHash | string | null | Optional opaque hash you set (Moshpit never stores customer emails in clear text). |
metadata | object | null | Arbitrary key/value bag you control (≤ 20 keys, ≤ 4 KB serialized). |
status | "active" | "inactive" | Inactive customers cannot mint scoped sessions or upload. Soft-deleted customers report `deleted` internally. |
billingState | "active" | "suspended" | "canceled" | `active` enforces the stored caps. `suspended` or `canceled` makes the customer inactive and moves scenes outside the supplied keeper/cap set into retained archive. |
usedBytes | number | The customer's current storage consumption, in bytes. |
limitBytes | number | The customer's storage allocation, in bytes. This is the hard cap enforced on upload. |
maxSplats | number | null | The customer's scene cap. `null` means no per-customer cap. |
remainingBytes | number | `max(0, limitBytes − usedBytes)`. |
percentUsed | number | `usedBytes / limitBytes`, clamped to 0–100. `0` when the limit is 0. |
deletedAt | string | null | ISO 8601 timestamp set when the customer is soft-deleted. |
createdAt | string | null | ISO 8601 creation timestamp. |
updatedAt | string | null | ISO 8601 last-update timestamp. |
Over-allocation is allowed by design
You can hand out more storage and scenes across your customers than your
account bundle contains. Per-customer limitBytes and maxSplats are hard
caps on each customer; Moshpit bills your account for the aggregate usage
and applies your plan's overage rules. See Billing.
PUT /api/v1/external-user
Provision a new customer or update an existing one. This is an upsert keyed on the
integration plus X-Moshpit-External-User-Id.
PUT
https://moshpit.studio/api/v1/external-user
Body
Every field is optional. Omitted fields are left unchanged on an existing
customer; on a brand-new customer, omitting limitBytes defaults the allocation
to your Studio account storage bundle.
| Field | Type | Description |
|---|---|---|
limitBytes | number (int ≥ 0) | The customer's storage allocation, in bytes. Omit to leave unchanged, or — on a new customer — to default to the account bundle. Raise this to unblock a customer that hit their cap. |
maxSplats | number (int ≥ 0) | null | The customer's scene cap. `null` removes the per-customer cap. Omit to leave unchanged. |
status | "active" | "inactive" default unchanged (`active` on create) | Set `inactive` to suspend a customer without deleting it. Omit to leave an existing customer unchanged. |
billingState | "active" | "suspended" | "canceled" default unchanged (`active` on create) | Use this to mirror your product billing state. `suspended` and `canceled` force the customer inactive; omitted caps default to `0` for those states. Omit to leave an existing customer unchanged. |
keepSplatIds | string[] | Optional scene ids to keep active when lowering caps or changing billing state. Selected scenes must fit `maxSplats` and `limitBytes`; all other scenes become private, non-embeddable retained archive. |
displayName | string | Optional label, 1–120 characters. |
avatarUrl | string (URL) | Optional avatar URL, up to 2048 characters. |
emailHash | string | Optional opaque hash, 8–128 characters. |
metadata | object | Arbitrary key/value bag. String, number, boolean, or null values. ≤ 20 keys and ≤ 4 KB serialized. |
Example request
Bash
curl -X PUT https://moshpit.studio/api/v1/external-user \
-H "Authorization: Bearer msk_YOUR_SECRET_KEY" \
-H "X-Moshpit-Public-Key: mpk_YOUR_PUBLIC_KEY" \
-H "X-Moshpit-External-User-Id: customer_42" \
-H "Content-Type: application/json" \
-d '{
"limitBytes": 5368709120,
"maxSplats": 10,
"displayName": "Acme Robotics",
"status": "active"
}'Success response — 200 OK
JSON
{
"status": "success",
"externalUser": {
"id": "65f1a2b3c4d5e6f7a8b9c0d1",
"integrationId": "65e0b1c2d3e4f5a6b7c8d9e0",
"externalUserId": "customer_42",
"productSlug": "app.example.com",
"displayName": "Acme Robotics",
"avatarUrl": null,
"emailHash": null,
"metadata": null,
"status": "active",
"billingState": "active",
"usedBytes": 0,
"limitBytes": 5368709120,
"maxSplats": 10,
"remainingBytes": 5368709120,
"percentUsed": 0,
"deletedAt": null,
"createdAt": "2026-06-18T10:00:00.000Z",
"updatedAt": "2026-06-18T10:00:00.000Z"
}
}GET /api/v1/external-user
Fetch one customer's record.
GET
https://moshpit.studio/api/v1/external-user
Example request
Bash
curl "https://moshpit.studio/api/v1/external-user" \
-H "Authorization: Bearer msk_YOUR_SECRET_KEY" \
-H "X-Moshpit-Public-Key: mpk_YOUR_PUBLIC_KEY" \
-H "X-Moshpit-External-User-Id: customer_42"Success response — 200 OK
Returns { status: "success", externalUser: { ... } } with the
externalUser object. Returns 404 if no customer
exists for that id under the integration.
GET /api/v1/external-user/usage
Read a single customer's usage and allocation — handy for rendering a usage meter inside your own dashboard.
GET
https://moshpit.studio/api/v1/external-user/usage
Example request
Bash
curl "https://moshpit.studio/api/v1/external-user/usage" \
-H "Authorization: Bearer msk_YOUR_SECRET_KEY" \
-H "X-Moshpit-Public-Key: mpk_YOUR_PUBLIC_KEY" \
-H "X-Moshpit-External-User-Id: customer_42"Success response — 200 OK
JSON
{
"status": "success",
"usage": {
"storage": {
"usedBytes": 4294967296,
"limitBytes": 5368709120,
"remainingBytes": 1073741824,
"percentUsed": 80
},
"splats": {
"used": 7,
"limit": 10
}
}
}| Field | Type | Description |
|---|---|---|
usage.storage | object | The customer's `usedBytes`, `limitBytes`, `remainingBytes`, and `percentUsed`. `limitBytes` is the customer's allocation, not the account bundle. |
usage.splats.used | number | Live count of scenes the customer currently owns. |
usage.splats.limit | number | null | The customer's `maxSplats`. `null` means the customer has no per-customer scene cap. |
Returns 404 if the customer does not exist.
Billing state and retained archive
Use PUT /api/v1/external-user rather than DELETE when your customer cancels,
downgrades, or temporarily loses access in your own product. Studio owns the
hosted assets and applies the retention lifecycle.
billingState: "active"keeps the customer usable and applies the suppliedlimitBytes/maxSplats.billingState: "suspended"makes the customer inactive and locks scenes outside the supplied keeper/cap set.billingState: "canceled"makes the customer inactive and schedules retained archive.keepSplatIdscan name specific scenes to keep active, as long as they fit the supplied caps.protectedSplatIdscan name scenes to protect from the retention purge. Protected scenes are still archived like any other overflow, but they are never auto-removed and stay restorable indefinitely.
Retained scenes become private and non-embeddable, disappear from normal product
surfaces, and are released from user-facing storage counters. Unprotected
retained scenes are permanently removed 44 days after they were archived
(14-day grace + 30-day dormancy); until then, raising the customer's caps or
setting billingState: "active" restores them automatically up to the caps.
Scenes named in protectedSplatIds are exempt from the 44-day removal.
A PUT that only upserts identity fields (no billingState, limitBytes,
maxSplats, keepSplatIds, or protectedSplatIds) never re-applies the
retention lifecycle — it is safe to call before every session mint.
Bash
curl -X PUT https://moshpit.studio/api/v1/external-user \
-H "Authorization: Bearer msk_YOUR_SECRET_KEY" \
-H "X-Moshpit-Public-Key: mpk_YOUR_PUBLIC_KEY" \
-H "X-Moshpit-External-User-Id: customer_42" \
-H "Content-Type: application/json" \
-d '{
"billingState": "canceled",
"limitBytes": 0,
"maxSplats": 0
}'DELETE /api/v1/external-user
Soft-delete a customer and cascade-delete every scene they own (including the underlying assets and view/comment records). The customer's storage is released. This is the explicit destructive path; do not use it for ordinary subscription cancellation or downgrade.
DELETE
https://moshpit.studio/api/v1/external-user
Example request
Bash
curl -X DELETE "https://moshpit.studio/api/v1/external-user" \
-H "Authorization: Bearer msk_YOUR_SECRET_KEY" \
-H "X-Moshpit-Public-Key: mpk_YOUR_PUBLIC_KEY" \
-H "X-Moshpit-External-User-Id: customer_42"Success response — 200 OK
JSON
{
"status": "success",
"deletedSplatCount": 7,
"externalUser": {
"id": "65f1a2b3c4d5e6f7a8b9c0d1",
"externalUserId": "customer_42",
"status": "deleted",
"billingState": "canceled",
"usedBytes": 0,
"deletedAt": "2026-06-18T11:30:00.000Z"
}
}| Field | Type | Description |
|---|---|---|
deletedSplatCount | number | How many of the customer's scenes were removed. |
externalUser | object | null | The soft-deleted customer record (`status: "deleted"`, `usedBytes: 0`), or `null`. |
Enforcement
Per-customer caps are hard and independent of your account pool —
a customer can be blocked even while your account still has room. You unblock a
customer by raising their allocation with PUT.
Storage — 413
An upload that would push the customer past their limitBytes is rejected, even
if your account still has storage:
JSON
{
"status": "error",
"code": "STORAGE_QUOTA_EXCEEDED",
"scope": "customer",
"usedBytes": 5368709120,
"limitBytes": 5368709120,
"remainingBytes": 0,
"requestedBytes": 134217728
}Raise the customer's limitBytes with PUT /api/v1/external-user to unblock the
upload.
scope distinguishes the two storage blocks:
"customer"— the customer's per-customer allocation (above). Raise theirlimitBytes."account"— the upload would exceed your whole Moshpit account and overage isn't configured. With overage on, the account accrues overage and this never fires; with overage off, customer uploads are blocked until you configure overage or free account-wide space.usedBytes/limitByteshere are the account aggregate, not the customer's.
Scenes — 403
Creating a scene beyond the customer's maxSplats is rejected with the shared
plan-limit envelope:
JSON
{
"status": "error",
"code": "plan_limit",
"dimension": "splatCount",
"limit": 10,
"current": 10,
"message": "This customer's scene limit (10) has been reached.",
"upgradeUrl": "/account/billing?upgrade=plan"
}This is the shared plan-limit envelope, so it also carries upgradeUrl. That
deep link targets your own plan upgrade flow and is not meaningful to your
customers — for a per-customer cap, ignore it and raise the customer's
maxSplats (or set it to null) with PUT to unblock.
Capabilities stay per-integration
Whether a session can view or edit is decided per integration, not per customer. Per-customer allocation governs storage and scene counts only.
Billing
Moshpit bills your Moshpit account for the aggregate of every customer's
storage and scene usage, plus any overage allowed by your plan. Per-customer
limitBytes and maxSplats are allocation knobs you control for your own
tenants — Moshpit does not bill your customers. You price and bill them yourself,
externally.
Because the account is billed on the total, you may over-allocate across
customers: the sum of every customer's limitBytes can exceed your included
bundle.
Error responses
| Status | Cause |
|---|---|
| 400 | Invalid JSON body, schema validation failure, or missing external user header |
| 401 | Missing or invalid Bearer / public-key combination |
| 403 | REST access disabled for the account, or a per-customer Scene cap was reached |
| 404 | The customer does not exist under this integration |
| 410 | The integration has been revoked |
| 413 | The customer's storage allocation (limitBytes) would be exceeded by an upload |
What's next
- Reseller / multi-tenant guide — the end-to-end flow and a worked example.
- POST embed-sessions — mint a session scoped to a customer with
X-Moshpit-External-User-Id. - POST /v1/splats — commit a scene against a customer's allocation.