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 economicsOn this page
The model
One CloudSitus tenant, many of your users.
your users→ your siteYou own auth, your billing, your packages, and your margin.
your site→ CloudSitusOne phi_live_ key. Read x-phi-cost-micro per call for COGS.
CloudSitus→ modelsRegion-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.
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 reselleroptionalYour CloudSitus account is funded with prepaid credits (auto-recharge optional); usage draws them down. You keep your margin over cost.
Unlimited / first-partysame-org connectoroptionalYour 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.
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./v1/subkeyssub)/v1/reseller/customers/v1/usage# 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_…"# 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.
<PhiUsageCard>customer-sideA customer's own credits used, calls, and cap progress — fetches /v1/usage with their sub-key (safe in the browser).
<PhiCustomerTable>reseller adminYour 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.
// 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.
/v1/mcp{ "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_modelfreeThe 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 · destructiveRevoke 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 pageA 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 tokenTyped 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 labelsrequiredcriteria 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 levelsrequiredcriteria 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?optionalNo criteria needed (optionally {"true": …, "false": …}). Returns the probability the statement is true — threshold it yourself.
# 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, RESTThe 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.
X-RegionCH | EU | US | …requiredData 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 | falseoptionaltrue 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.
# 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 bodyYour price
x-phi-cost-micro (your cost) → × your markup → end-user price. The header is your cost basis; you set the rest.
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.
x-phi-cost-micro bills on. Pull it to price packages and stay in sync when prices change./v1/pricing# 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 outHandy 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.
GET /v1/modelscatalogRegion/PHI-filtered. Each entry carries kind, phi_eligible, and supports_tools.
GET /v1/pricingratesThe per-model rate card (above).
x-phi-toolssupported | unsupportedOn 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.
Toolsfunction callingForward tools / tool_choice / response_format unchanged. Works across Claude / GPT / Mistral / Gemini / Qwen (Gemma has no tool template).
Streamingstream: trueSSE 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.
EmbeddingsdimensionsX-Region: EU (or CH) + dimensions: N to match your vector schema. Same canonical model everywhere, so vectors stay cross-region comparable.
# 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.
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 loggingprivacyCloudSitus 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 diagnosticUnmetered key + routing check (reports billing_active). Use it for setup without spending.
Residency-pinned modalitiesno cross-borderSTT / 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.