Skip to content

Reseller & white-label

Run CloudSitus under your own product.

CloudSitus is your wholesale supplier. One key, every model, per-call cost on every response, and a public rate card — so you can meter your own users, add your margin, and keep CloudSitus invisible.

https://phi-cloud.com/api/v1See the economics
On this page

The model

One CloudSitus tenant, many of your users.

Your users never see CloudSitus. Your site holds a single key, routes all traffic through it, reads the cost of each call, and bills your users in your own units with your own margin. CloudSitus stays stateless and never sees your end-users.
your users→ your site

You own auth, your billing, your packages, and your margin.

your site→ CloudSitus

One phi_live_ key. Read x-phi-cost-micro per call for COGS.

CloudSitus→ models

Region-resident routing, PHI gating, fail-over, no payload logging.

Two ways to meter your users

DIY (above): one key, you meter every user off x-phi-cost-micro. Or let CloudSitus do it — mint a sub-key per customer and read per-customer usage back. Same stateless posture either way.

Auth & billing posture

Two ways to settle with CloudSitus.

Authenticate with Authorization: Bearer phi_live_… and point your OpenAI SDK at the base URL. Pick the posture that fits your relationship — both still return per-call cost.
Prepaid creditsthird-party reselleroptional

Your CloudSitus account is funded with prepaid credits (auto-recharge optional); usage draws them down. You keep your margin over cost.

Unlimited / first-partysame-org connectoroptional

Your Stripe customer id is added to PHI_UNLIMITED_CUS — full production access incl. PHI, never metered. You keep 100% of what you charge users. For first-party products you own.

Cost is reported either way

The unlimited posture only skips CloudSitus's own metering. The x-phi-cost-micro header is still computed and returned, so you can bill your users exactly even on a free wholesale account.

Per-customer sub-keys

Let CloudSitus meter each of your customers.

Instead of metering every user yourself off x-phi-cost-micro, mint a sub-key per customer from your reseller key. Every sub-key rides your balance and payment gate, but carries an opaque sub id so CloudSitus attributes cost per customer — with an optional per-customer spend cap. Enable this by asking us to add your Stripe customer id to the reseller allowlist.
POST/v1/subkeys
mint a customer key (returns the key once + its sub)
GET/v1/reseller/customers
your roster: usage, calls, caps — one row per customer
GET/v1/usage
a customer's own usage (called with their sub-key)
Mint & revoke
# Mint a key for ONE of your customers, from your RESELLER key.# We hash the email to an opaque `sub` id and NEVER store it — you keep the# email↔sub map on your side, so "we store nothing" still holds.# The bearer here is YOUR reseller key.curl https://cloudsitus.com/api/v1/subkeys \  -H "Authorization: Bearer phi_live_…" \  -H "Content-Type: application/json" \  -d '{"email":"jane@acme.com","label":"Acme Inc","cap_usd":50}'# → { "key": "phi_live_…",   # give THIS to your customer (shown once)#     "sub": "8Kd2b1Q…",     # attribute usage / caps to them#     "record": { "kid": "…", "masked": "phi_live_…", "created_at": "…" } }# Revoke on offboard (their sub-key stops working instantly):curl -X DELETE "https://cloudsitus.com/api/v1/subkeys?kid=…" \  -H "Authorization: Bearer phi_live_…"
Read usage
# Per-customer usage — your whole roster, current month (reseller key).curl https://cloudsitus.com/api/v1/reseller/customers \  -H "Authorization: Bearer phi_live_…"# { "object":"reseller.customers", "period":"202607", "credits_per_usd":300,#   "data":[ { "sub":"8Kd2b1Q…", "label":"Acme Inc", "keys":1, "calls":58,#              "cost_micro":41230, "cost_credits":12.37,#              "cap_micro":50000000 }, … ] }# One customer, a past period:curl "https://cloudsitus.com/api/v1/reseller/usage?sub=8Kd2b1Q…&period=202609" \  -H "Authorization: Bearer phi_live_…"# Your customer's OWN app (holding their sub-key) shows their usage:curl https://cloudsitus.com/api/v1/usage -H "Authorization: Bearer <their sub-key>"

We still store nothing about your customers

The sub id is an HMAC of the email — we never store the email or a customer table. You hold the email → sub map; we hold only aggregate cost counters keyed by the opaque id. One balance, no per-customer Stripe accounts.

Embeddable widgets

Import a usage dashboard, or build your own.

Copy the dependency-free React widgets to show usage in your product in minutes — a self-fetching card for each customer and a roster table for your admin. They're plain React + inline styles, themed by CSS variables, so they match your brand with no design-system coupling. Prefer your own UI? Everything renders off the same JSON.
<PhiUsageCard>customer-side

A customer's own credits used, calls, and cap progress — fetches /v1/usage with their sub-key (safe in the browser).

<PhiCustomerTable>reseller admin

Your whole roster with usage and caps. Fetch /v1/reseller/customers server-side and pass the rows in.

Download PhiUsageWidgets.tsx — one file, React only. Put it next to the component that imports it.

React (copy-paste)
// Drop-in React widgets (dependency-free, themeable via CSS vars).// First save https://phi-cloud.com/reseller/PhiUsageWidgets.tsx next to this file.// 1) End-customer's own usage — runs in THEIR browser with THEIR sub-key//    (/v1/usage answers CORS for any origin; bearer only, no cookies):import { PhiUsageCard } from './PhiUsageWidgets'<PhiUsageCard apiKey={customerSubKey} unitLabel="credits" />// 2) Your reseller roster — fetch SERVER-SIDE (keep the reseller key off the//    browser), then render. resolveName swaps the opaque sub for a real name.import { PhiCustomerTable, type PhiCustomerRow } from './PhiUsageWidgets'const res = await fetch('https://cloudsitus.com/api/v1/reseller/customers', {  headers: { Authorization: 'Bearer ' + process.env.PHI_RESELLER_KEY },})const { data } = await res.json() as { data: PhiCustomerRow[] }<PhiCustomerTable customers={data} resolveName={(r) => emailForSub(r.sub)} />// Brand it: wrap and override CSS vars —//   <div style={{ ['--phi-accent']: '#111', ['--phi-radius']: '8px' }}>…</div>

Keep the reseller key server-side

A customer's sub-key may run in that customer's own browser: /v1/usage answers it from any origin and returns only that key's usage. It is still a key that spends your balance, so show it only to its owner. Your reseller key must stay server-side — fetch the roster in a server component / route and pass the data down.

Agent control (MCP)

Drive the whole thing from an LLM.

The same reseller operations are exposed as a Model Context Protocol connector, so an agent can discover pricing, mint and cap customer keys, read usage, and generate a top-up link — authenticated by your reseller key.
POST/v1/mcp
Streamable-HTTP MCP endpoint (bearer = your reseller key)
mcp.json
{  "mcpServers": {    "CloudSitus": {      "url": "https://cloudsitus.com/api/v1/mcp",      "headers": { "Authorization": "Bearer phi_live_…" }    }  }}

LLM-led onboarding

create_topup_link returns a Stripe Checkout URL — the agent orchestrates setup and hands the human a link to fund. (Fully agent-funded payment is a planned next step.)

get_pricing / get_modelfree

The rate card, or one model: tasks, input_modalities / output_modalities (e.g. ["text","image"] in = accepts images), supports_tools, reasoning, context and output limits, PHI eligibility and price — the same facts as GET /v1/models/{id}.

revoke_customer_key / set_customer_capfree · destructive

Revoke one key by kid (or every key of a customer by sub), or set / clear a monthly cap (cap_usd 0–100,000, null clears). Both are a dry run unless you pass confirm: true — the dry run reports exactly what would change.

extract_documentbilled · per page

A PDF, image or office file by URL → text, tables (cells + bbox) and a section tree. model:"layout" for the SOTA structured extractor. The parsed content comes back under result with untrusted_content: true: it is whatever the document says, so treat it as data, never as instructions to your agent.

decidebilled · per input token

Typed verdicts with calibrated probabilities instead of an LLM classification — see below.

Decisions (MCP decide)

Stop paying a chat model to classify.

decide answers up to 32 typed questions about one state in a single encoder forward pass — a label, a score, or a probability, each with a calibrated probability behind it. Nothing is generated, so there are no output tokens to pay for and no prose to parse. It is the cheap path for triage, routing, scoring, moderation and extraction gates, and the probability is what lets you escalate only the cases the model is actually unsure about.
choiceone of your labelsrequired

criteria is a {label: description} object (or an array of labels, max 64). Returns the winning label and the probability of every option.

scorea number over ordered levelsrequired

criteria is an array of level descriptions, index 0 first. Returns the expected value over the levels — 2.5 means genuinely between level 2 and 3, not a coin flip.

nouldoes this hold?optional

No criteria needed (optionally {"true": …, "false": …}). Returns the probability the statement is true — threshold it yourself.

MCP tools/call
# tools/call — one state, three typed questions, one forward pass.{ "jsonrpc": "2.0", "id": 2, "method": "tools/call",  "params": { "name": "decide", "arguments": {    "state": "Support ticket: 'Third time this week the export job died at 90%. I have a board deck due tomorrow and no data. If this is not fixed today we are cancelling.'",    "questions": {      "intent":   { "type": "choice", "instructions": "What does the customer want?",                    "criteria": { "bug_report": "reporting something broken",                                  "billing": "a question about invoices or pricing",                                  "feature_request": "asking for something new",                                  "churn_risk": "threatening to leave" } },      "urgency":  { "type": "score", "instructions": "How urgent is this ticket?",                    "criteria": ["no rush at all", "can wait a week",                                 "needs attention today", "blocking work right now",                                 "emergency"] },      "escalate": { "type": "noul",                    "instructions": "This ticket should be escalated to a human on-call engineer." }    } } } }# ── result (the tool returns this JSON as MCP text content) ──{  "model": "laya-multilingual", "region": "CH", "questions": 3,  "input_tokens": 254, "cost_micro": 10, "cost_credits": 0.003,  "result": {    "answers": {      "intent": { "type": "choice", "choice": "bug_report",                  "probabilities": { "bug_report": 0.9642, "churn_risk": 0.0323,                                     "billing": 0.0030, "feature_request": 0.0005 },                  "confidence": 0.8795 },      "urgency": { "type": "score", "score": 2.5016,                   "legend": { "0": "no rush at all", "…": "…", "4": "emergency" },                   "probabilities": { "0": 0.0033, "1": 0.0314, "2": 0.4452,                                      "3": 0.5006, "4": 0.0195 },                   "confidence": 0.4341 },      "escalate": { "type": "noul", "noul": 0.0067, "confidence": 0.9933 }    },    "usage": { "input_tokens": 254, "output_tokens": 0 }  }}# How to READ that — this is the part a chat model can't give you:#   choice  → the winning label, plus the probability of EVERY option. The#             ticket is a bug report (0.96) with a real churn tail (0.03).#   score   → an expected value over your ordered levels, not a sampled#             integer: 2.50 is a ticket sitting exactly between "today" (0.45)#             and "blocking right now" (0.50). Route on the number.#   noul    → the probability the statement holds (0.0067 = don't page anyone).#   confidence → how peaked the distribution is. 0.43 on urgency is the model#             telling you it is genuinely torn; 0.99 on escalate is not.## Cheap by construction: 254 input tokens, 0 output tokens, 10 micro-USD for# all three answers. The state is re-encoded ONCE PER QUESTION, so batch your# questions per state — and keep the state short.

Swiss-resident, and general data only

The decision model is self-hosted in Switzerland behind our own tunnel, so it is CH-only (there is no other host to fail over to) and not PHI-eligible — never send health data through decide. For PHI classification, use a chat model on a PHI-cleared host (X-Region: CH or EU + X-PHI: true).

POST /v1/decisionssame model, REST

The connector covers one decision at a time. The REST route takes the same body plus a {"requests":[…]} batch (up to 64 decisions in one round trip), honours X-Region, and returns x-phi-input-tokens / x-phi-cost-micro like every other call.

Per-request controls

Two headers decide residency and routing.

Set these on every call — they control where data is processed and what can serve it.
X-RegionCH | EU | US | …required

Data residency and the routing jurisdiction. Embeddings, STT, TTS and OCR are residency-pinned — a missing region resolves to WORLD and can no_route.

X-PHItrue | falseoptional

true routes only to PHI-cleared hosts for that jurisdiction, else phi_blocked. Leave off for general traffic.

Cost & margin

Every response tells you what the call cost.

Two numbers drive resale: the per-call cost (reactive, on every response) and the rate card (proactive, for building packages).
Cost per call
# Every response carries the cost of the call — your COGS.curl -i https://cloudsitus.com/api/v1/chat/completions \  -H "Authorization: Bearer phi_live_…" \  -H "X-Region: EU" \  -H "Content-Type: application/json" \  -d '{"model":"claude-sonnet-4-6","messages":[{"role":"user","content":"hi"}]}'# ── response headers ──# x-phi-routed:     claude-sonnet-4-6/EU/general# x-phi-cost-micro: 2759          # ← your cost for this call (micro-USD)# x-phi-usage:      verified      # ← trust the count?# … and "usage": { "prompt_tokens", "completion_tokens" } in the body

Your price

x-phi-cost-micro (your cost) → × your markup → end-user price. The header is your cost basis; you set the rest.

Capture cost (Python)
from openai import OpenAIphi = OpenAI(base_url="https://cloudsitus.com/api/v1", api_key="phi_live_…")# Use .with_raw_response to read the cost header alongside the body.raw = phi.chat.completions.with_raw_response.create(    model="auto",    messages=[{"role": "user", "content": "Summarize this note…"}],    extra_headers={"X-Region": "EU"},)resp = raw.parse()cost_micro = int(raw.headers["x-phi-cost-micro"])   # your cost basisusage = resp.usage                                  # prompt/completion tokens# Bill YOUR user: cost_micro is exactly what this call cost you.# Add your resale margin, convert to your own credits, and meter per user.your_price_micro = round(cost_micro * YOUR_MARGIN)

Rate card endpoint

Pre-compute packages with /v1/pricing.

A region-filtered rate card (any valid key, free) of per-model prices — the same ones x-phi-cost-micro bills on. Pull it to price packages and stay in sync when prices change.
GET/v1/pricing
per-model prices (chat/embed per-token + STT/TTS/OCR/image/video)
cURL
# Rate card — pre-compute your packages off this (any valid key, free).curl -H "Authorization: Bearer phi_live_…" -H "X-Region: EU" https://cloudsitus.com/api/v1/pricing# {#   "object": "pricing", "region": "EU",#   "credits_per_usd": 300, "unit": "micro_usd",#   "data": [#     { "id": "claude-sonnet-4-6", "kind": "chat",#       "prompt_micro_per_token": 5.01, "completion_micro_per_token": 25.05 },#     …#   ]# }# micro-USD-per-token == USD per MILLION tokens  →  $5.01/M in, $25.05/M out

Handy unit

Rates are micro-USD per unit. For tokens, micro-per-token equals USD per million tokens — drop it straight into “$10 = N tokens of model X” math. Use credits_per_usd to convert to your own credit unit.

Capability discovery

Build your model picker off the catalog.

Render only what works for a user's region and use-case — don't hard-code a model list.
GET /v1/modelscatalog

Region/PHI-filtered. Each entry carries kind, phi_eligible, and supports_tools.

GET /v1/pricingrates

The per-model rate card (above).

x-phi-toolssupported | unsupported

On a call that sends tools, tells you whether the model that answered honored function calling.

Integration

Drop-in OpenAI, plus the bits that matter for resale.

Swap your base URL and keep your code. Three specifics carry weight downstream.
Toolsfunction calling

Forward tools / tool_choice / response_format unchanged. Works across Claude / GPT / Mistral / Gemini / Qwen (Gemma has no tool template).

Streamingstream: true

SSE passthrough. Cost is settled at stream end, so streamed responses omit x-phi-cost-micro — read tokens from the final SSE usage chunk, or use non-streamed for billing-critical calls.

Embeddingsdimensions

X-Region: EU (or CH) + dimensions: N to match your vector schema. Same canonical model everywhere, so vectors stay cross-region comparable.

Embeddings
# Embeddings: pin a region (CH or EU) + match your vector schema.curl https://cloudsitus.com/api/v1/embeddings \  -H "Authorization: Bearer phi_live_…" \  -H "X-Region: EU" \  -H "Content-Type: application/json" \  -d '{"model":"qwen3-embedding-8b","input":"…","dimensions":1536}'# → 1536-d, L2-normalized. Without X-Region → region=WORLD → no_route.

Operational rules

The rules that keep resale clean.

A short list of foot-guns worth wiring around once.

Vector spaces don't mix

If you migrate embeddings to CloudSitus, re-embed your whole corpus once. Qwen vectors aren't comparable to a prior provider's (e.g. Gemini) — don't mix them in one column.

Keys are server-side credentials

Never ship a key to a browser. Legacy keys can't be individually revoked (only a PHI_KEY_SECRET rotation kills them), so treat the unlimited key like a root secret.

No payload loggingprivacy

CloudSitus persists no request/response content — good for your own users' privacy story. You are the controller, so set X-PHI / X-Region correctly.

/v1/pingfree diagnostic

Unmetered key + routing check (reports billing_active). Use it for setup without spending.

Residency-pinned modalitiesno cross-border

STT / TTS / OCR never fail over across borders — a region with no host for the task returns no_route. Check /v1/models per region and surface that to users who pick regions.