Publishable key
Identifies your integration to the browser SDK and loads the public configuration: your branding, and which components are available here.
Developers
Everything a customer sees in Proxy is built on the same REST API, server and browser SDKs, embeddable components and webhooks your team can use. The organisation, the environment, the granted scopes and the enabled services are all resolved from the credential you present: no organisation id to send, and no second authorisation model to learn.
Each product is delivered by one or more services, and every service does these six things the same way. Independence with six different opinions about authentication would be worse than a suite.
A publishable key for the browser, a secret key for your server, and short-lived client sessions your backend mints for a single capability.
Three types, one prefix convention, and the environment in the prefix.
The same policies serve the API and the web interface. A permission written for a person is the permission an integration inherits, narrowed by its scopes.
There is no second authorisation system to keep in step with the first.
The credential decides the organisation, the environment and the enabled services. There is no organisation id to send and no header to set.
A cross-organisation identifier is refused during route resolution.
Every operation writes a usage record carrying its operation key, quantity, billing unit and cost, so "what did Processor cost this month" is a query rather than an allocation exercise.
Free operations are recorded in full, because the count still matters.
A versioned path, responses under a data key, stable error codes, idempotency keys on anything that costs money, and rate limits applied per credential and per organisation.
Additive changes ship inside v1. Breaking ones get a new version.
Every mutating operation has a named actor, and that actor is written to the trail and onto the record. Services do not keep separate histories that have to be reconciled.
The actor is required, not derived, and never optional.
API documentation (opens a different site) Service reference
Browser code cannot hide a secret. Everything about the credential model follows from accepting that rather than working around it.
Identifies your integration to the browser SDK and loads the public configuration: your branding, and which components are available here.
The credential every server-to-server call carries. It resolves the organisation, the environment, the scopes and the enabled services.
Your backend trades its secret key for a token good for one capability, one resource and a few minutes, then hands that to the page.
The complete server story in two calls, the browser half in one, and the two artefacts they leave behind. The token is read from configuration, never written into code, and never sent to a page.
The one request that answers most questions
curl https://api.proxy.example/api/v1/me \
-H "Authorization: Bearer sk_test_..."
{
"data": {
"credential_type": "secret",
"environment": "test",
"organisation": { "name": "Northgate Adjusters" },
"integration": { "name": "Claims portal", "status": "active" },
"scopes": ["storage:read", "storage:write", "processor:run"],
"permissions": [
"storage.files.view",
"storage.files.upload",
"storage.files.delete"
]
}
}
scopes is what was granted. permissions is what those scopes confer after narrowing. They are shown separately because a scope whose service is switched off appears in the first and is still refused, and hiding that would make a misconfigured integration look correct.
Every credential is issued for one environment and the prefix says which. Test traffic never reaches a real customer, and the two sets of records do not mix.
A new secret key works before the old one stops, so a deployment does not have to be simultaneous. Revoking one key does not disturb the others.
A new optional field, a new response field, a new endpoint or a new enum case in an open field are not breaking changes. Tolerate unknown fields and unknown enum values rather than failing on them.
Your backend trades its secret key for a client session: one capability, one resource, a few minutes, and the constraints you set. The page receives that and nothing else.
Your backend, before the page renders
// Your backend decides what the page may do, and for how long.
$session = $proxy->clientSessions()->create(
capability: 'storage.upload',
resource: ['type' => 'claim', 'id' => $claim->reference],
constraints: [
'max_files' => 5,
'max_file_size' => 20_000_000,
'mime_types' => ['application/pdf', 'image/jpeg'],
],
);
return view('claim.evidence', ['clientToken' => $session->token]);
The capability comes from a closed list. The resource is required, so a token minted to upload against one claim cannot be replayed against another.
Your page, after it has the token
import { PoweredByProxy } from '@proxy/browser';
const proxy = new PoweredByProxy({
publishableKey: 'pk_live_...',
baseUrl: 'https://api.proxy.example',
});
// The uploader takes no maxFiles option. Its limits are read off the
// token your backend minted, so the page cannot widen them.
await proxy.storage.mountUploader({
token: clientToken,
target: '#evidence',
});
There is no maxFiles option, deliberately. The limits are read off the session your backend set, so the number shown to the person is the number Proxy will enforce.
proxy.capabilities() returns what will actually work: the integration holds the scope and the organisation is entitled to the service. A list built from scopes alone would render an uploader for a service nobody bought, and the refusal would arrive after somebody had already dragged a file onto it.
mount() refuses an unavailable component before it renders.
Each integration names the origins its publishable key may be used from. There is no wildcard, and the signing host keeps cross-origin access closed rather than configured.
The parts of an integration that only matter once it is under load, documented before you meet them rather than after.
Handling a completed job
// Delivery is a record. Every attempt carries a status, and a replay
// arrives with the same event id rather than a new one.
export function handle(event) {
if (event.type !== 'processor.job.completed') return;
const { id, result, usage } = event.data;
claims.attachExtraction(id, result.fields);
ledger.note(usage.operation, usage.quantity, usage.unit);
}
A webhook event carries a delivery status through pending, processed, failed or ignored, and every attempt is recorded. Handlers should be idempotent: a replay arrives with the same event id rather than a new one.
An organisation, an integration and a pair of test keys, before anything is enabled or metered. The reference documentation is generated from the API itself.