# Platform: tenants, your artifact domain, statements

The Platform plan ($199 a month) is for products that run their own
customers' code on Shotcup. Each of your customers can be a **tenant**: runs,
sandboxes and uploads belong to it, it can have its own quotas within your
plan, and you can read its usage for your own billing.

## Create a tenant

```sh
curl https://shotcup.dev/api/v1/tenants \
  -H "Authorization: Bearer $SHOTCUP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"externalId": "cus_123", "name": "Acme Inc.", "metadata": {"crm": "4711"},
       "quotas": {"maxConcurrent": 5, "runsPerMonth": 100000, "computeHoursPerMonth": 10}}'
```

`externalId` is your own id for the customer (1-64 letters, digits and
`. _ : @ -`), unique among your tenants. An account holds up to
1,000 tenants; more is a contract term. Metadata is up to 20 string
entries for your own use.

**Quotas** are optional and each at most your plan's number:
`maxConcurrent` (1-100), `runsPerMonth` (0-5,000,000),
`computeHoursPerMonth` (0-300), `memory` (`"standard"` refuses the
tenant's `memory: "large"` runs) and `retentionDays` (1-90; how long its
runs' history and artifact links last, 90 days by default). A tenant without
a quota shares your account's limit. A tenant's run always counts against
your account's allowance too.

## Run as a tenant

Two ways, and you can mix them:

- **The tenant header.** Your account key with
  `Shotcup-Tenant: cus_123` (the external id, or the tenant's id) acts
  for that tenant on the run, sandbox, upload and artifact routes. One key
  serves all your customers from your backend.
- **A tenant key.** `POST /api/v1/tenants/cus_123/keys` with
  `{"name": "production"}` returns a key (shown once) that acts only for
  that tenant. Hand it to an environment you do not fully trust, such as
  your customer's own deployment. A tenant holds up to 10 live keys;
  an account creates up to 600 tenant keys an hour.

A tenant key, or a request naming a tenant, can run code, manage the
tenant's sandboxes and workspace files, upload files and mint links to the
tenant's own artifacts. It cannot read another tenant's runs, sandboxes,
uploads or artifacts (they answer 404), nor the account: `/api/v1/me`,
API keys and tenant management answer `403 tenant-forbidden`. The MCP
server takes account keys only. Your account key without the header sees
every tenant's runs and sandboxes; each run and sandbox carries its
`tenantId`.

## Quota denials

A run past a tenant's own quota is a `429` whose `error.kind` names it
and whose `error.tenant` is the external id, so you can tell your
customer's limit from your plan's:

| kind | meaning |
|---|---|
| `tenant-concurrency-limit` | the tenant's `maxConcurrent` runs are in flight |
| `tenant-allowance-runs` | the tenant used its `runsPerMonth` this month |
| `tenant-allowance-compute` | the tenant used its `computeHoursPerMonth` this month |
| `tenant-large-memory-not-allowed` | the tenant is limited to `memory: "standard"` |

Your plan's own denials (`concurrency-limit`, `allowance-runs`, ...)
still apply to every tenant run. Months are UTC calendar months.

## Usage, suspension and deletion

- `GET /api/v1/tenants` lists tenants with this month's runs and
  compute (`?limit=` up to 100, `?cursor=` for the next page).
- `GET /api/v1/tenants/cus_123/usage?month=2026-10` returns one tenant's
  runs and compute-hours in a month, for billing your customer.
- `PATCH /api/v1/tenants/cus_123` changes `name`, `metadata`
  (replaced whole), `quotas` (merged; `null` removes one) and
  `suspended`. A suspended tenant's keys and header answer
  `403 tenant-suspended` and its artifact links 404 until you resume it.
- `DELETE /api/v1/tenants/cus_123` revokes its keys, deletes its saved
  sandboxes and frees the external id; its run history stays until it
  expires and still counts in your usage.

The dashboard's Platform section does all of this too.

## Artifact links on your own domain

Artifact links normally live on `shotcup.page`. A Platform account can
serve them from one host of its own, such as `artifacts.example.com` (a
subdomain; more hosts are a contract term):

1. `POST /api/v1/domains` with `{"host": "artifacts.example.com"}`. The
   answer lists the DNS records to set: a TXT record at
   `_shotcup-challenge.artifacts.example.com` that proves the host is
   yours, and a CNAME that points the host at Shotcup.
2. `POST /api/v1/domains/artifacts.example.com/verify` once the records
   are live. The domain becomes `verified`; Shotcup then attaches it to its
   hosting, and it becomes `active`.
3. From then on, artifact links of your runs and your tenants' runs use
   `https://artifacts.example.com/a/...`. Links issued before keep working.

Your host serves only your account's artifacts, with the same protections
as `shotcup.page`: no cookies, `X-Content-Type-Options: nosniff`, no
referrer, and HTML and SVG in a sandbox without your origin. Everything
else on it answers 404. `DELETE /api/v1/domains/artifacts.example.com`
removes it.

## Monthly statements and invoices

- `GET /api/v1/statements` lists months; `GET /api/v1/statements/2026-10`
  returns the month as JSON with your plan fee, usage, overage and a line
  per tenant plus your own usage; `?format=csv` and `?format=html` (a
  printable statement) too, and `?tenant=cus_123` for one tenant's usage.
  The current month is a running statement; a month is final from 01:00 UTC
  on the 1st of the next.
- `PUT /api/v1/billing/details` sets the legal name, billing email,
  address and VAT or tax ID printed on your invoices.
- Platform is invoiced monthly in arrears (the plan fee plus the month's
  overage, net 30) from the final statement, once invoicing is switched on
  for your account.
