Customer API
The Customer API is how the backend of a Company works with stored records after verification — reading history, correcting data, driving state changes, and managing assignments. It is available on the Stored tier (there are no records to manage on the session-based tier).
API reference
The complete Customer API — Customers, Digital Identities, and Assignments — is published as an interactive reference with every endpoint, request and response schema, parameter, and status code, rendered directly from the OpenAPI specification of the service.
Open the Customer API reference
Resources
| Resource | Description |
|---|---|
| Customer | The master record for a person: personal data, address, custom fields, and links to Digital Identities. |
| Digital Identity | A single verification attempt and its result (read-only except for state transitions). |
| Assignment | A link between a Customer and something they were issued — see Assignments. |
Availability
This API belongs to the Stored tier. On a tenant whose record storage is switched off there is nothing to manage, and every endpoint here is refused at the gateway with 403. If you need it, ask Innovatrics to enable storage for your tenant.
Base URL and access token
Requests go through the API gateway, which validates your token, scopes the request to your tenant, and routes it on:
https://<gateway-host>/integrator/v1/...
For the Innovatrics-managed service the gateway host is api.idv.innovatrics.com; a self-hosted deployment uses your own. The interactive reference has a server selector with the same values.
Your access token must carry the audience onboarding-service. The gateway rejects a token without it before the request reaches the service, so a token that works against the Capture & Workflow API is not automatically accepted here.
The gateway allows 200 requests per minute per tenant, alongside a global limit shared across tenants. Exceeding either returns 429 Too Many Requests.
A 429 comes from the gateway rather than from the service, so — unlike every other error on this page — it does not use the standard error envelope. Do not try to parse it as one. Back off and retry.
Common operations
| Operation | Call |
|---|---|
| Read a Customer | GET /v1/customers/{customerId} |
| List / filter Customers | GET /v1/customers?… (non-PII filters, including external_id) |
| Search by personal data | POST /v1/customers/search (PII in the body) |
| Create a Customer | POST /v1/customers (supports a supplied external_id, for migrations) |
| Update fields | PATCH /v1/customers/{customerId} |
| Discover the schema | GET /v1/customers/schema |
| List the identities of a Customer | GET /v1/customers/{customerId}/identities |
| Read one identity | GET /v1/customers/{customerId}/identities/{identityId} |
| Transition an identity state | POST /v1/customers/{customerId}/identities/{identityId}/transition |
| Anonymize a Customer (GDPR) | POST /v1/customers/{customerId}/anonymize |
| Anonymize one identity | POST /v1/customers/{customerId}/identities/{identityId}/anonymize |
| Read the palm record of a Customer | GET /v1/customers/{customerId}/palm-record |
Search that includes personal data uses POST … /search with the query in the request body rather than in the URL, to avoid leaking PII into logs and proxies.
Reading an identity directly
An identity can also be reached without knowing its Customer, which matters for identities that never produced one — a verification that was rejected, or a session on the session-based tier:
| Operation | Call |
|---|---|
| Read an identity by its own id | GET /v1/identities/{identityId} |
| Read a captured image of an identity | GET /v1/identities/{identityId}/images/{slot} |
| Anonymize an identity by its own id | POST /v1/identities/{identityId}/anonymize |
Assignments
An Assignment records that a Customer was issued something — a SIM, a device, an account, a policy, a ticket. It is deliberately independent of verification: no workflow guard applies, and a running verification never modifies one.
| Operation | Call |
|---|---|
| List by the thing issued | GET /v1/assignments?external_identifier=… |
| List everything a Customer holds | GET /v1/customers/{customerId}/assignments |
| Create | POST /v1/assignments |
| Read | GET /v1/assignments/{assignmentId} |
| Update the free-form fields | PATCH /v1/assignments/{assignmentId} |
| Suspend / resume | POST /v1/assignments/{assignmentId}/suspend · …/resume |
| Transfer or unassign | POST /v1/assignments/transfer |
| Delete outright | DELETE /v1/assignments/{assignmentId} |
| Discover the schema | GET /v1/assignments/schema |
An Assignment is active, suspended or unassigned. The first two are open states and hold the identifier; only one open Assignment may exist for a given external_identifier at a time, so the API can answer "who holds this SIM right now" with a single record.
Three things are worth knowing before you build against it:
- Move it with
transfer, not delete-then-create.POST /v1/assignments/transfercloses the current holder's Assignment and opens the new one in a single transaction. Passingto_customer_id: nullunassigns instead, closing the Assignment without opening a replacement. Doing it in two calls risks leaving the identifier unheld, or held twice, if the second call fails. DELETEerases history. It is for correcting a mistaken entry. To record that an assignment legitimately ended, unassign it throughtransfer— that keeps the record and stampsunassigned_at.- Only
remote_referenceandcustom_fieldsare mutable. The link itself — which Customer, which identifier, which Digital Identity — is fixed once written.
POST /v1/assignments and POST /v1/assignments/transfer accept an Idempotency-Key header. Send one and a retry returns the original outcome instead of acting twice.
Custom fields
Beyond the typed base fields (name, date of birth, address, and standard identity fields), a Customer carries a free-form custom_fields object for Company-specific extension data. No schema declaration is required to write custom fields; keys a Company wants to search on are registered separately as indexed fields. GET …/schema reports the typed base fields and their flags.
External identifier
Every Customer has an external_id — the identifier under which your own systems know the person. It is unique within the tenant, limited to 64 characters from a–z A–Z 0–9 . _ -, and by default equals the Customer identifier.
There are three ways to work with it:
- Compose it in the workflow — the
create_new_customeroutcome action accepts anexternal_id_templatesuch as${document.personal_number}_${uuid}, so newly onboarded Customers follow your existing scheme from day one. See the manifest reference. - Supply it on create — when migrating an existing customer base,
POST /v1/customersaccepts the historicalexternal_id; uniqueness is validated. - Query by it —
external_idis a filter of the list endpoint and a field of the search body, so your backend can look up a Customer by its own key without storing the Customer identifier of the platform.
The external identifier is shown in the Customer detail and returned in webhook payloads. If your scheme embeds a personal number, treat the identifier as personal data: it is wiped by anonymization like the rest of the record.
Anonymization is terminal
Anonymization irreversibly erases the personal data, images and biometric templates of a record. It is the mechanism behind a GDPR erasure request, so there is no undo and no hard delete — the record remains as a shell for audit and counting, with the personal data gone.
The customer_id you supplied may itself carry personal data — many integrators compose it from a national identification number — so erasing the record also replaces the identifier with a freshly generated opaque one.
The anonymize response returns that new customer_id, and the id you called with stops resolving. Store the returned value if you ever need to reach the erased shell again, for an audit or a regulator request. An erasure job that discards the response loses the only handle on the record.
Once a record is anonymized, further writes to it are refused:
| Response | Meaning |
|---|---|
409 CUSTOMER_ANONYMIZED | The Customer was anonymized. Reads still work; writes and transitions do not. |
409 IDENTITY_ANONYMIZED | The identity was anonymized. |
Treat both as final rather than retrying. Anonymizing an already-anonymized record is a no-op, so your erasure job can safely run again.
Writing safely around active verifications
While a verification workflow is running for a Customer, it is authoritative over that Customer's identity data, and integrator writes that would race it are rejected with 409 WORKFLOW_RUN_ACTIVE. The response identifies the active run. Retry once it completes.
Three groups of writes behave differently:
| Write | While a workflow is running | If the guard itself is unreachable |
|---|---|---|
PATCH on a workflow-managed field — name, given_name, surname, date_of_birth, place_of_birth | 409 WORKFLOW_RUN_ACTIVE | Fails open — the write proceeds |
POST …/transition and every …/anonymize | 409 WORKFLOW_RUN_ACTIVE | Fails closed — 503 ACTIVE_RUN_GUARD_UNAVAILABLE |
PATCH on integrator-managed fields — custom fields, references, preferences | Always succeeds | Unaffected |
This keeps the workflow authoritative over identity data while leaving operational fields writable at any time.
Because anonymization fails closed, a GDPR erasure request can come back as 503 ACTIVE_RUN_GUARD_UNAVAILABLE rather than succeeding. Treat it as retryable and retry until it succeeds — do not record the erasure as done. Re-running anonymization on an already-anonymized record is a no-op, so retrying is always safe.
Who can write
The Customer API is one of three writers to the record store, alongside verification workflows and the back-office UI. All writes are authenticated (OAuth2) and audited.