Trust Signals: Authorization and Proxy
This page covers the backend side of an integration: the credentials Futurae provisions, the tokens your backend obtains with them, how the APIs validate requests, and the collection proxy that forwards observations from the SDKs.
All Trust Signals APIs require an OAuth2 Bearer token, obtained through the Client Credentials flow. Both kinds of token stay on your backend: neither ever reaches a device or a browser.
Endpoints
All three services are public, and your backend calls them directly:
| Service | Host | Called by |
|---|---|---|
| Authorization server | auth.futurae.com | Your backend, to obtain tokens |
| Collection API | ts-collection-public.futurae.com | Your collection proxy, to forward observations |
| Signals API | ts-signals-public.futurae.com | Your backend, to query signals |
Provisioned credentials
Futurae provisions two OAuth2 clients per integration, and your Solutions Engineer supplies them. Each has its own client_id and client_secret, and each is scoped to a single role:
| Role | Used for | Token audience | OAuth2 scope |
|---|---|---|---|
| Collection | Forwarding observations to the Collection API, from your collection proxy | ts-collection-public.futurae.com | observations:write |
| Retrieval | Querying the Signals API | ts-signals-public.futurae.com | signals:read |
Each client_id is a UUID assigned by Futurae; it is not your serviceId. Because each client is scoped to one role, the collection client cannot obtain a signals token, and the retrieval client cannot obtain a collection token.
Requesting a token
Your backend exchanges a client’s credentials for an access token at the token endpoint of the authorization server, https://auth.futurae.com/oauth/v2/token, passing them with HTTP Basic authentication:
curl --location 'https://auth.futurae.com/oauth/v2/token?grant_type=client_credentials&scope=urn'\
'%3Azitadel%3Aiam%3Aorg%3Aproject%3Aid%3A<audience>%3Aaud' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic <base64(client_id:client_secret)>'
| Field | Value |
|---|---|
audience | The host of the API the token is for: ts-collection-public.futurae.com for a collection token, ts-signals-public.futurae.com for a signals token. This selects the role. |
client_id | The UUID of the client for that role. |
client_secret | The secret provisioned with that client_id. |
Token scope
Every token is valid for all accounts in your tenant. A collection token in an app or a web page would therefore let anyone who extracts it submit observations for any of your users. This is why the SDKs never receive it: your collection proxy holds it and attaches it on the way through.
The boundary between tenants is enforced by serviceId on every request. See Tenant isolation.
Token claims
The access token is a signed JWT. Alongside the standard claims (iss, aud, exp, nbf, iat, jti), it carries one Trust Signals claim, set by the Authorization Server:
| Claim | Description |
|---|---|
service_id | Your tenant identifier: the UUID Futurae provisions for your tenant. The serviceId on every request must equal this claim. |
An example decoded payload of a signals token:
{
"iss": "https://auth.futurae.com",
"aud": ["ts-signals-public.futurae.com"],
"sub": "374789412942250792",
"iat": 1781013712,
"nbf": 1781013712,
"exp": 1781056912,
"jti": "V2_376678216330379570-at_376678216330445106",
"scope": "signals:read",
"client_id": "9c1e5a84-2f73-4b16-9a0d-6e4f8b2c7d35",
"service_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"permissions": []
}
Token lifecycle
Your backend obtains tokens, caches them, and refreshes them before they expire, using the exp claim. No part of the token lifecycle involves the app or the browser. If an API rejects a token with 401 before its expiry, request a new token and retry once.
Request validation
The Collection and Signals APIs apply the same rules. They differ only in the token they accept, the place the identifiers arrive in, and which identifiers are required:
| Requirement | Collection API | Signals API |
|---|---|---|
| Token audience | ts-collection-public.futurae.com | ts-signals-public.futurae.com |
| Required scope | observations:write | signals:read |
| Where identifiers arrive | Request body | Query parameters |
serviceId | Required, and must equal the service_id claim | Same |
accountId | Required. Not constrained by the token, which is valid for all accounts in your tenant | Same |
unitId | Required | Optional, except for Remote Access Tool |
verificationStatus | Required | Optional, except for Remote Access Tool. Requires unitId |
interactionId | Required | Optional, except for Remote Access Tool. Requires verificationStatus |
| Identifier format | As defined in Identifiers | Same |
A request that fails validation is rejected with one of three codes:
| Code | Meaning |
|---|---|
400 Bad Request | A required parameter is missing, an identifier breaks the format rules, or the optional query identifiers are supplied out of order. |
401 Unauthorized | The token itself is unusable: missing, malformed, expired, carrying an invalid signature, or presented with a scheme other than Bearer. |
403 Forbidden | The token is valid, but its claims do not allow this request: the wrong scope or audience, or a serviceId that does not match the service_id claim. |
A Signals API request that passes validation returns 200 OK, even when the signal could not be computed. See Reading the response. The per-endpoint parameters and responses are in the Trust Signals API Reference.
Collection proxy
The Sensor SDKs never send observations to Futurae directly. They send them to an endpoint on your backend, the collection proxy, which forwards them to the Collection API. This keeps the collection token and the user’s real identifiers on your backend: the app and the page never hold the collection token, and the identity fields they send are overwritten.
The proxy contract is the same for the Mobile and the Browser Sensor SDK.
What the SDK sends
The SDK sends POST requests to the collectionUrl you configure. Each request has a JSON body carrying serviceId, unitId, accountId, interactionId, verificationStatus and the collected observation. Its Authorization header carries Bearer <accessToken>, where <accessToken> is whatever value you passed to the SDK.
Your proxy has to know which authenticated user each request belongs to. Use your existing session mechanism for this. Because the SDK sends the accessToken value in the Authorization header, a mobile app can pass its own session credential for your backend there. In the browser, page JavaScript can read anything you pass to the SDK, so pass placeholders and identify the user from your own session. Never pass the Futurae collection token to either SDK.
What your proxy does
For every request it receives from a Sensor SDK, your proxy must:
- Identify the user. Use your own session mechanism to establish which authenticated user and session the request belongs to. Reject the request if it cannot.
- Set the identity fields. Overwrite
accountId,interactionIdandverificationStatusin the request body with the values your backend holds for that user and session, whatever the SDK sent. - Set the collection token. Replace the
Authorizationheader withBearer <collection_token>, using the token your backend holds. - Add the client IP address. Set the
X-Forwarded-Forheader to the IP address of the device or browser that sent the request. - Forward the request to the Collection API at
https://ts-collection-public.futurae.com/api/v1/collections.
The forwarded request takes this form, whichever SDK sent the original:
POST /api/v1/collections HTTP/1.1
Host: ts-collection-public.futurae.com
Authorization: Bearer <collection_token>
X-Forwarded-For: <original_client_ip>
Content-Type: application/json
{
"serviceId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"unitId": "<unit_id>",
"accountId": "<real_account_id>",
"interactionId": "<real_interaction_id>",
"verificationStatus": "<real_verification_status>",
"observation": { ... }
}
Responding to the SDK
The Collection API replies 202 Accepted. Return its status code to the SDK, because the SDK interprets it:
401or403: the SDK reports an authentication error. Return one of these when your proxy cannot identify the user.- Any other status outside the
2xxrange: the SDK reports the upload as rejected.
If the Collection API rejects your collection token with 401, refresh the token on your backend and retry before you answer the SDK. The device cannot fix a token it never had. How each SDK surfaces errors is described in Mobile SDK error handling and in Browser Sensor SDK.
Example implementation
A minimal proxy in Node.js with Express. It uses three functions of your own: authenticate() resolves the request to one of your user sessions, identityFor() returns that session’s Trust Signals identifiers, and getCollectionToken() returns a cached collection token.
import express from 'express';
const COLLECTION_URL = 'https://ts-collection-public.futurae.com/api/v1/collections';
const app = express();
app.set('trust proxy', 1); // the number of proxies in front of this service, so req.ip is the client's address
app.use(express.json({ limit: '1mb' }));
app.post('/trust-signals/collections', async (req, res) => {
// 1. Identify the user.
const session = await authenticate(req);
if (!session) return res.sendStatus(401);
// 2. Set the identity fields from your own session state.
const { accountId, interactionId, verificationStatus } = identityFor(session);
const body = JSON.stringify({ ...req.body, accountId, interactionId, verificationStatus });
// 3 to 5. Attach the collection token and the client IP address, and forward.
const forward = async (token) => fetch(COLLECTION_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'X-Forwarded-For': req.ip,
},
body,
});
let upstream = await forward(await getCollectionToken());
if (upstream.status === 401) {
upstream = await forward(await getCollectionToken({ refresh: true }));
}
res.sendStatus(upstream.status);
});
The example leaves out logging, timeouts and error handling for the upstream call.