Guides
Multi-tenant face search: isolation, deletion, and cost attribution
The architecture that keeps one customer's faces un-searchable by another — and the bill attributable to each.
TL;DR
For a multi-tenant product, give each tenant its own collection (namespace) so one customer's faces can never match another's, authorize every call against the tenant that owns the collection, attribute cost per tenant by tagging calls, and propagate deletion to both the API and your database. This guide covers the collection-naming scheme, the authorization boundary, per-tenant cost attribution, noisy-neighbour control, and the deletion + audit trail you need before your first enterprise customer asks.
The moment you have two customers, face search becomes a tenancy problem, not just an API problem. If tenant A's selfie can match tenant B's photos, you have a data-isolation breach — the worst kind for biometric data. SightRadar isolates by collection: one account's face vectors are never cross-searchable. The rest is how you wire your own app around that guarantee.
The isolation boundary: one collection per tenant
A collection is a namespace faces live in. Searches only ever run inside one collection, so if each tenant's faces live in their own collection, cross-tenant matching is structurally impossible — not a filter you have to remember to apply. Choose a naming scheme that encodes the tenant and can never collide:
def collection_id(tenant_id: str, scope: str) -> str:
# e.g. "t_ac12" + "event_2026_gala" -> "t_ac12__event_2026_gala"
# tenant prefix first so every id is unambiguously owned by one tenant.
return f"{tenant_id}__{scope}"Note: Never derive a collection id from user-supplied input alone. Always prefix with a server-side tenant id you control, so tenant B can't name a collection that reads or writes into tenant A's namespace.
The authorization boundary
Isolation only holds if every request is checked against the tenant that owns the target collection. Do this in your backend before you ever call the face API — the API key is your service credential, not the tenant's.
def authorize(session, collection_id):
# collection ids are "<tenant>__<scope>"; the owning tenant is the prefix.
owner = collection_id.split("__", 1)[0]
if session.tenant_id != owner:
raise PermissionError("cross-tenant access denied")
return collection_idCost attribution per tenant
In a usage-billed product you need to know which tenant drove which spend. Because billing is per photo processed, attribution is a matter of logging each billable call against its tenant. Record the operation, tenant, and credit cost as you make the call — every billable response also returns an X-Credits-Remaining header you can snapshot.
| What to log | Why |
|---|---|
| tenant_id + operation (index/search) | Per-tenant usage + chargeback |
| photos processed | Billing is per photo — this is the billable unit |
| collection_id | Attribute to the right workspace/event |
| timestamp | Usage over time, rate-limit windows |
Noisy-neighbour control
One tenant backfilling ten million photos shouldn't starve another tenant's live searches. Two levers: run bulk indexing through the batch path (asynchronous, webhook results) so it doesn't contend with synchronous searches, and apply your own per-tenant rate limits in your backend so a single tenant can't monopolise throughput. SightRadar doesn't impose default TPS quota tickets, which means the throttling policy is yours to set deliberately rather than fight.
Deletion propagation
When a tenant offboards or a user requests erasure, deletion has to reach every place a face lives: the API collection, your database rows, and any derived data (search logs, caches). Delete the biometric data first and confirm success before dropping your local records — the reverse order can leave faces indexed while your DB says they're gone.
def offboard_tenant(tenant_id):
for cid in collections_for(tenant_id):
r = requests.delete(f"{BASE}/v1/collections/{cid}", headers=H)
r.raise_for_status() # abort + retry if the API delete fails
db.execute("delete from face_index where tenant_id = %s", (tenant_id,))
audit(tenant_id, "tenant_offboarded") # keep the audit recordAudit logging
Biometric processing is exactly the kind of activity a customer's compliance team will ask you to prove. Keep an append-only audit trail of who indexed, searched, and deleted what, per tenant. You don't store the faces in the audit log — you store the fact of the operation, its actor, and its scope.
Tip: Keep the audit log even after you delete the faces themselves. "We deleted tenant X's data on this date, on this request" is a record you want to retain; the biometric vectors are not.
See how per-tenant isolation and per-operation usage work in the console.
Read the API referenceFrequently asked questions
How do I stop one customer's faces from matching another's?
Give each tenant its own collection and only ever search inside a single collection. Because searches never cross collection boundaries, cross-tenant matching is structurally impossible rather than a filter you must remember to apply. Prefix every collection id with a server-side tenant id so tenants can't name into each other's namespaces, and authorize every call against the owning tenant before hitting the API.
How do I attribute face recognition cost to each tenant?
Billing is per photo processed, so log each billable call against its tenant id, operation, collection, and photo count. Every billable response also returns an X-Credits-Remaining header you can snapshot. Summing those logs per tenant gives you accurate usage and chargeback without guessing.
How do I handle a data-deletion request in a multi-tenant system?
Propagate the deletion to every place a face lives: delete the API collection first and confirm success, then remove your database rows and any derived data such as search logs or caches. Keep an audit record of the deletion itself — the fact and date of erasure — even though you no longer keep the biometric vectors.