Customer identity
Customer Data
Access and manage customer-related data including e-wallet balances, bulk imports, PDF tickets, and custom metadata.
E-Wallet & Balance
Retrieve e-wallet automatic reload configuration and balance history for a customer. Balance history tracks all e-wallet transactions (credits, debits, refunds).
GET/customers/{customerId}/ewallet_automatic_reloadGet E-Wallet Auto-Reload Config
Retrieve the automatic e-wallet reload configuration for a customer.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| customerId | integer | Yes | The unique identifier of the customer. |
Response
{
"status": 200,
"code": "customer_ewallet_automatic_reload_succeed",
"message": "Configuration retrieved successfully.",
"data": {
"automaticEwalletReloadActive": true,
"automaticEwalletReloadMinBalance": 500,
"automaticEwalletReloadAmount": 2000,
"automaticEwalletReloadCardId": 12345
}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Example | Description |
|---|---|---|---|
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "customer_ewallet_automatic_reload_succeed" | Machine-readable application code for the result. |
| message | string | "Configuration retrieved successfully." | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.automaticEwalletReloadActive | boolean | true | The automatic ewallet reload active value. |
| data.automaticEwalletReloadMinBalance | integer | 500 | The automatic ewallet reload min balance value. |
| data.automaticEwalletReloadAmount | integer | 2000 | The automatic ewallet reload amount value. |
| data.automaticEwalletReloadCardId | integer | 12345 | Identifier of the associated automatic ewallet reload card. |
Balance History
The full reference for GET /customers/{customerId}/balance_history (parameters, includes, and response schema) lives in the E-Wallet Balance & Transactions section.
In addition to the parameters documented there, the endpoint supports include=order and a date-range filter using unix timestamps: applyDate[gte]=1700000000&applyDate[lte]=1710000000.
GET/customers/{customerId}/balance_history/countGet Balance History Count
Retrieve the total number of balance history entries for a customer. Useful for pagination.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| customerId | integer | Yes | The unique identifier of the customer. |
Response
{
"status": 200,
"code": "balance_count_succeed",
"message": "Balance count retrieved.",
"data": {
"total": 142
}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Example | Description |
|---|---|---|---|
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "balance_count_succeed" | Machine-readable application code for the result. |
| message | string | "Balance count retrieved." | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.total | integer | 142 | Total number of matching records. |
Customer Import
Bulk import customers from a file. The endpoint accepts a file upload and processes it asynchronously, streaming progress updates back to the client via chunked transfer encoding.
Streaming Response
This endpoint uses Transfer-Encoding: chunked to stream progress updates during the import. The final chunk contains the standard JSON response with import results (customers created, updated, and unchanged).
POST/customers/importImport Customers from File
Upload a CSV/Excel file to bulk import customers for a brand. Requires a file in multipart/form-data format.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| brandId | integer | Yes | The brand ID to import customers into. |
| isMultiBadge | boolean | No | If true, the file format supports multiple badges per customer. |
| autoImportGuestHierarchy | boolean | No | If true, automatically creates guest group hierarchy from the file data. |
Request Body
multipart/form-data with a 'file' field containing the CSV or Excel file.Response
{
"status": 200,
"code": "customers_import_create_succeed",
"message": "Import completed successfully.",
"data": {
"customersCreated": 15,
"customersUpdated": 3,
"customersUnchanged": 82
}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Example | Description |
|---|---|---|---|
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "customers_import_create_succeed" | Machine-readable application code for the result. |
| message | string | "Import completed successfully." | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.customersCreated | integer | 15 | The customers created value. |
| data.customersUpdated | integer | 3 | The customers updated value. |
| data.customersUnchanged | integer | 82 | The customers unchanged value. |
POST/customers/import_phoenixImport Phoenix Customer File
Upload a Phoenix-formatted customer file for a brand. The endpoint streams text progress chunks and ends with the standard JSON result payload.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| brandId | integer | Yes | Brand that owns the imported customers. |
Request Body
file=[binary]Request Body Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| file | binary | Yes | [binary] | Phoenix-formatted customer file uploaded as multipart/form-data. |
Response
{
"status": 200,
"code": "customers_import_create_succeed",
"message": "Customers have been imported successfully.",
"data": {
"customersCreated": 15,
"customersUpdated": 3,
"customersUnchanged": 82,
"customersError": [
{
"line": 7,
"email": "invalid@example.com",
"invalidFields": [
"email"
]
}
],
"customersWarn": [
{
"line": 8,
"email": "duplicate@example.com",
"invalidFields": [
"email"
]
}
],
"otherWarnings": [
{
"type": "pricingRule",
"list": [
"UNKNOWN-RULE"
]
}
],
"validationErrors": []
}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Example | Description |
|---|---|---|---|
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "customers_import_create_succeed" | Machine-readable application code for the result. |
| message | string | "Customers have been imported successfully." | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.customersCreated | integer | 15 | The customers created value. |
| data.customersUpdated | integer | 3 | The customers updated value. |
| data.customersUnchanged | integer | 82 | The customers unchanged value. |
| data.customersError | array | […] | List of customers error entries. |
| data.customersError[] | object | {…} | Object containing customers error fields. |
| data.customersError[].line | integer | 7 | The line value. |
| data.customersError[].email | string | "invalid@example.com" | Email address. |
| data.customersError[].invalidFields | array | […] | List of invalid fields entries. |
| data.customersError[].invalidFields[] | string | "email" | The invalid fields value. |
| data.customersWarn | array | […] | List of customers warn entries. |
| data.customersWarn[] | object | {…} | Object containing customers warn fields. |
| data.customersWarn[].line | integer | 8 | The line value. |
| data.customersWarn[].email | string | "duplicate@example.com" | Email address. |
| data.customersWarn[].invalidFields | array | […] | List of invalid fields entries. |
| data.customersWarn[].invalidFields[] | string | "email" | The invalid fields value. |
| data.otherWarnings | array | […] | List of other warnings entries. |
| data.otherWarnings[] | object | {…} | Object containing other warnings fields. |
| data.otherWarnings[].type | string | "pricingRule" | The type value. |
| data.otherWarnings[].list | array | […] | List of list entries. |
| data.otherWarnings[].list[] | string | "UNKNOWN-RULE" | The list value. |
| data.validationErrors | array | [] | List of validation errors entries. |
Customer Base Export
Export a filtered customer base directly as CSV or XLSX. If a large export cannot complete within the synchronous request window, request the asynchronous report instead and retrieve it from report generation.
GET/customers/exportExport Customer Base File
Returns a CSV or XLSX attachment. The export is scoped to the requested brand and supports the same filters as the customer list.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| brandId | integer | Yes | Brand whose customers are exported. |
| fileType | string | Yes | Output format: csv or xlsx. |
| restaurantIds | string | No | Comma-separated restaurant IDs used to filter the export. |
| search | string | No | Free-text customer search filter. |
| groups | string | No | Comma-separated customer-group filters. |
| mustDisplayArchivedCustomers | boolean | No | Include archived customers when true. |
Response
Binary attachment. Content-Type is text/csv for csv or application/vnd.openxmlformats-officedocument.spreadsheetml.sheet for xlsx; Content-Disposition contains the filename.GET/customers/export/asyncRequest Asynchronous Customer Base Export
Creates a report-generation request for the same export filters. Use this when the synchronous export times out or for large customer bases.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| brandId | integer | Yes | Brand whose customers are exported. |
| fileType | string | Yes | Requested output format: csv or xlsx. |
| restaurantIds | string | No | Comma-separated restaurant IDs used to filter the export. |
| search | string | No | Free-text customer search filter. |
| groups | string | No | Comma-separated customer-group filters. |
| mustDisplayArchivedCustomers | boolean | No | Include archived customers when true. |
Response
{
"status": 200,
"code": "customer_base_export_request_created",
"message": "Customer base export request is created successfully."
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Example | Description |
|---|---|---|---|
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "customer_base_export_request_created" | Machine-readable application code for the result. |
| message | string | "Customer base export request is created successfully." | Human-readable result message. Do not use this value for program logic. |
Retrieve Generated Reports
An asynchronous export is made available through report generation. Brand and restaurant users see only reports they created. Admin and brand-group users must provide brandId when they do not have a brand context, and can include impersonatedUserId to see reports created by that user.
GET/report_generationList Generated Reports
Lists generated report files in the caller scope. Customer-base export reports become expired after their retention window; use expired to avoid requesting an unavailable download.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| brandId | integer | No | Brand context required for an admin or brand-group user who has no own brand. |
| impersonatedUserId | integer | No | Optional impersonated user whose reports are included for an authorized admin or brand-group caller. |
Response
{
"status": 200,
"code": "report_generations_succeed",
"message": "The list of report generations have been found.",
"data": [
{
"reportGenerationId": 812,
"name": "customer-base-export-3325-2026-07-11.xlsx",
"createdAt": "Fri Jul 11 2026 09:15:00 GMT+0000 (Coordinated Universal Time)",
"reportType": "customer_base_export",
"status": "succeed",
"expired": false
}
]
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Example | Description |
|---|---|---|---|
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "report_generations_succeed" | Machine-readable application code for the result. |
| message | string | "The list of report generations have been found." | Human-readable result message. Do not use this value for program logic. |
| data | array | […] | Endpoint-specific response payload. |
| data[] | object | {…} | Endpoint-specific response payload. |
| data[].reportGenerationId | integer | 812 | Identifier of the associated report generation. |
| data[].name | string | "customer-base-export-3325-2026-07-11.xlsx" | The name value. |
| data[].createdAt | string | "Fri Jul 11 2026 09:15:00 GMT+0000 (Coordinated Universal Time)" | Timestamp when this resource was created. |
| data[].reportType | string | "customer_base_export" | The report type value. |
| data[].status | string | "succeed" | HTTP status code returned by the API. |
| data[].expired | boolean | false | The expired value. |
GET/report_generation/{reportGenerationId}/signed-urlGet Generated Report Download URL
Returns a time-limited signed URL for a generated report after verifying that the report belongs to the requested brand and to the caller (or authorized impersonated user). Expired or missing files cannot be downloaded.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| reportGenerationId | integer | Yes | Generated report identifier returned by the list endpoint. |
| brandId | integer | No | Brand context required for an admin or brand-group user who has no own brand. |
| impersonatedUserId | integer | No | Optional impersonated user whose report is being downloaded by an authorized caller. |
Response
{
"status": 200,
"code": "report_generations_succeed",
"message": "The list of report generations have been found.",
"data": {
"url": "https://storage.googleapis.com/<signed-report-url>"
}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Example | Description |
|---|---|---|---|
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "report_generations_succeed" | Machine-readable application code for the result. |
| message | string | "The list of report generations have been found." | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.url | string | "https://storage.googleapis.com/<signed-report-url>" | The url value. |
POST/customers/{customerId}/unarchiveUnarchive Customer
Restores an archived customer. Brand and brand-group users may only restore a customer in a brand they are authorized to manage.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| customerId | integer | Yes | Archived customer to restore. |
Response
{
"status": 200,
"code": "customer_unarchive_succeed",
"message": "Customer has been successfully unarchived.",
"data": {
"success": true
}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Example | Description |
|---|---|---|---|
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "customer_unarchive_succeed" | Machine-readable application code for the result. |
| message | string | "Customer has been successfully unarchived." | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.success | boolean | true | The success value. |
Customer Tickets
Generate PDF receipts for individual orders or export a customer's receipt history as a PDF. Bulk exports can be counted and paginated before downloading to keep large histories manageable.
Single Ticket PDF
GET/orders/tickets/{orderId}Get Ticket PDF
Generate and download a PDF receipt for a specific order. Returns the file directly as application/pdf.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| orderId | string | Yes | The unique identifier of the order. |
Response
Binary PDF file (Content-Type: application/pdf). The response is an attachment; read the filename from the Content-Disposition response header.Bulk PDF Export
First count the selected customer history, then request a PDF. For very large histories, uselimit and offset to retrieve bounded batches. A date range must include bothstartDate and endDate.
GET/orders/tickets/export/countCount Tickets Available for Export
Returns the number of customer tickets matching the optional inclusive ISO-date range.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| customerId | integer | Yes | Customer whose tickets are counted. |
| startDate | string | No | ISO 8601 start of the export range. Required together with endDate. |
| endDate | string | No | ISO 8601 end of the export range. Required together with startDate. |
Response
{
"count": 142
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Example | Description |
|---|---|---|---|
| count | integer | 142 | Total number of matching records. |
POST/orders/tickets/exportDownload Customer Ticket History PDF
Generates a PDF attachment for the selected customer history. The X-Total-Count response header contains the total matching records before pagination; Content-Disposition contains the filename.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| customerId | integer | Yes | Customer whose tickets are exported. |
| startDate | string | No | ISO 8601 start of the export range. Required together with endDate. |
| endDate | string | No | ISO 8601 end of the export range. Required together with startDate. |
| limit | integer | No | Positive maximum number of tickets included in this PDF batch. |
| offset | integer | No | Zero-based number of matching tickets to skip before this batch. |
Response
Binary PDF file (Content-Type: application/pdf). Read the total matching ticket count from X-Total-Count and the filename from Content-Disposition.Customer Metadata
Metadata entries belong to a customer and a brand. Each entry exposes customerMetadataId, customerId, brandId, namespace, data, createdAt and updatedAt. Older entries can have a null namespace. Use the explicit entry endpoints below for new integrations; the historical upsert and delete contracts remain available.
Authentication and scope
Send an authenticated bearer token. Every operation checks access to the brand and verifies that the customer belongs to it. Entry operations also verify that the entry belongs to that same customer and brand. The new /entries mutations accept Admin and Brand accounts only. Restaurant accounts have read-only metadata controls in the backoffice. The historical metadata routes retain their Admin, Brand, Restaurant and POS role allowlist for compatibility, subject to the brand-access checks; this does not grant those roles access to the new mutations.
Text, JSON and concurrency
modeis required on entry creation and update:textorjson. The requestdatais always a string, including an empty string in text mode.- Text mode preserves the value literally. JSON mode parses the string once and stores its native JSON type: object, array, number, boolean, string or JSON null. Without a business schema, validation means valid JSON syntax.
- For a JSON object, use JSON mode and serialize the object once into the request's string-valued
datafield, as in the creation example below. For the literal text{"enabled":true}, use text mode. JSON null is sent as{"mode":"json","data":"null"}. - Existing JSON-encoded strings remain readable. Depending on the storage/transport layer, returned data may be JSON text; do not recursively parse arbitrary strings or infer the write mode from a JSON-looking value. Preserve text values as text when editing; the backoffice can format legacy JSON strings for display.
- Read the entry first. Send its exact
updatedAtin the JSON body of PUT or DELETE as the expected version. On409, reload, reconcile the changes and submit the fresh version; do not blindly overwrite. - The namespace is required, trimmed, non-empty and at most 255 characters on creation. It cannot be renamed by PUT. Updates/deletions use the returned
customerMetadataIdas the pathmetadataId, including for old entries whose namespace is null.
GET/brands/{brandId}/customers/{customerId}/metadataGet Customer Metadata
Read all entries, including automatically managed provenance. Use updatedAt from this response as the expected version for entry updates and deletions.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| brandId | integer | Yes | Brand owning the customer. |
| customerId | integer | Yes | Customer in that brand. |
Response
{
"status": 200,
"code": "customer_metadata_fetched",
"message": "The customer metadata has been fetched.",
"data": [
{
"customerMetadataId": 1234,
"customerId": 5337578,
"brandId": 100,
"namespace": "preferences",
"data": {
"preferredLanguage": "fr",
"enabled": true
},
"createdAt": "2026-09-21T09:00:00.000Z",
"updatedAt": "2026-09-21T09:00:00.000Z"
}
]
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Example | Description |
|---|---|---|---|
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "customer_metadata_fetched" | Machine-readable application code for the result. |
| message | string | "The customer metadata has been fetched." | Human-readable result message. Do not use this value for program logic. |
| data | array | […] | Endpoint-specific response payload. |
| data[] | object | {…} | Endpoint-specific response payload. |
| data[].customerMetadataId | integer | 1234 | Identifier of the associated customer metadata. |
| data[].customerId | integer | 5337578 | Identifier of the customer. |
| data[].brandId | integer | 100 | Identifier of the brand. |
| data[].namespace | string | "preferences" | The namespace value. |
| data[].data | object | {…} | Endpoint-specific response payload. |
| data[].data.preferredLanguage | string | "fr" | The preferred language value. |
| data[].data.enabled | boolean | true | The enabled value. |
| data[].createdAt | string | "2026-09-21T09:00:00.000Z" | Timestamp when this resource was created. |
| data[].updatedAt | string | "2026-09-21T09:00:00.000Z" | Timestamp when this resource was last updated. |
POST/brands/{brandId}/customers/{customerId}/metadata/entriesCreate a Metadata Entry
Admin/Brand only. Explicit creation, not an upsert: an existing namespace returns 409 customer_metadata_conflict. The reserved provenance namespace cannot be created manually. Success returns 200.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| brandId | integer | Yes | Brand owning the customer. |
| customerId | integer | Yes | Customer in that brand. |
| namespace | string | Yes | JSON body: unique non-empty namespace, trimmed, maximum 255 characters. |
| mode | string | Yes | JSON body: text or json. |
| data | string | Yes | JSON body: literal text or serialized JSON according to mode. |
Request Body
{
"namespace": "preferences",
"mode": "json",
"data": "{\"preferredLanguage\":\"fr\",\"enabled\":true}"
}Request Body Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Example | Description |
|---|---|---|---|
| namespace | string | "preferences" | The namespace value. |
| mode | string | "json" | The mode value. |
| data | string | "{\"preferredLanguage\":\"fr\",\"enabled\":true}" | Endpoint-specific response payload. |
Response
{
"status": 200,
"code": "customer_metadata_saved",
"message": "The customer metadata has been saved.",
"data": {
"customerMetadataId": 1234,
"customerId": 5337578,
"brandId": 100,
"namespace": "preferences",
"data": {
"preferredLanguage": "fr",
"enabled": true
},
"createdAt": "2026-09-21T09:00:00.000Z",
"updatedAt": "2026-09-21T09:00:00.000Z"
}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Example | Description |
|---|---|---|---|
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "customer_metadata_saved" | Machine-readable application code for the result. |
| message | string | "The customer metadata has been saved." | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.customerMetadataId | integer | 1234 | Identifier of the associated customer metadata. |
| data.customerId | integer | 5337578 | Identifier of the customer. |
| data.brandId | integer | 100 | Identifier of the brand. |
| data.namespace | string | "preferences" | The namespace value. |
| data.data | object | {…} | Endpoint-specific response payload. |
| data.data.preferredLanguage | string | "fr" | The preferred language value. |
| data.data.enabled | boolean | true | The enabled value. |
| data.createdAt | string | "2026-09-21T09:00:00.000Z" | Timestamp when this resource was created. |
| data.updatedAt | string | "2026-09-21T09:00:00.000Z" | Timestamp when this resource was last updated. |
PUT/brands/{brandId}/customers/{customerId}/metadata/entries/{metadataId}Update a Metadata Entry
Admin/Brand only. Replace the content without renaming the namespace. A stale updatedAt returns 409. An unchanged value is a no-op.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| brandId | integer | Yes | Brand owning the customer. |
| customerId | integer | Yes | Customer in that brand. |
| metadataId | integer | Yes | Positive customerMetadataId returned by GET. |
| mode | string | Yes | JSON body: text or json. |
| data | string | Yes | JSON body: replacement content. |
| updatedAt | string (ISO 8601) | Yes | JSON body: exact version from the latest read. |
Request Body
{
"mode": "json",
"data": "{\"preferredLanguage\":\"en\",\"enabled\":true}",
"updatedAt": "2026-09-21T09:00:00.000Z"
}Request Body Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Example | Description |
|---|---|---|---|
| mode | string | "json" | The mode value. |
| data | string | "{\"preferredLanguage\":\"en\",\"enabled\":true}" | Endpoint-specific response payload. |
| updatedAt | string | "2026-09-21T09:00:00.000Z" | Timestamp when this resource was last updated. |
Response
{
"status": 200,
"code": "customer_metadata_saved",
"message": "The customer metadata has been saved.",
"data": {
"customerMetadataId": 1234,
"customerId": 5337578,
"brandId": 100,
"namespace": "preferences",
"data": {
"preferredLanguage": "en",
"enabled": true
},
"createdAt": "2026-09-21T09:00:00.000Z",
"updatedAt": "2026-09-21T09:05:00.000Z"
}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Example | Description |
|---|---|---|---|
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "customer_metadata_saved" | Machine-readable application code for the result. |
| message | string | "The customer metadata has been saved." | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.customerMetadataId | integer | 1234 | Identifier of the associated customer metadata. |
| data.customerId | integer | 5337578 | Identifier of the customer. |
| data.brandId | integer | 100 | Identifier of the brand. |
| data.namespace | string | "preferences" | The namespace value. |
| data.data | object | {…} | Endpoint-specific response payload. |
| data.data.preferredLanguage | string | "en" | The preferred language value. |
| data.data.enabled | boolean | true | The enabled value. |
| data.createdAt | string | "2026-09-21T09:00:00.000Z" | Timestamp when this resource was created. |
| data.updatedAt | string | "2026-09-21T09:05:00.000Z" | Timestamp when this resource was last updated. |
DELETE/brands/{brandId}/customers/{customerId}/metadata/entries/{metadataId}Delete a Metadata Entry
Admin/Brand only. Delete exactly one entry after checking its version. Send updatedAt in a JSON request body, not in the query string. The provenance entry cannot be deleted.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| brandId | integer | Yes | Brand owning the customer. |
| customerId | integer | Yes | Customer in that brand. |
| metadataId | integer | Yes | Positive customerMetadataId returned by GET. |
| updatedAt | string (ISO 8601) | Yes | JSON body: exact version from the latest read. |
Request Body
{
"updatedAt": "2026-09-21T09:05:00.000Z"
}Request Body Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Example | Description |
|---|---|---|---|
| updatedAt | string | "2026-09-21T09:05:00.000Z" | Timestamp when this resource was last updated. |
Response
{
"status": 200,
"code": "customer_metadata_deleted",
"message": "The customer metadata has been deleted.",
"data": {}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Example | Description |
|---|---|---|---|
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "customer_metadata_deleted" | Machine-readable application code for the result. |
| message | string | "The customer metadata has been deleted." | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
Entry error responses
400 customer_metadata_invalid: invalid JSON content or invalid provenance context. Missing or malformed request fields can instead fail schema validation with HTTP 400.409 customer_metadata_conflict: duplicate namespace on POST, or stale version on PUT/DELETE.403 customer_metadata_forbidden: manual mutation of the reserved namespace. Authentication, role and brand authorization failures also apply and may use the API's standard permission errors.404 customer_metadata_missing: customer or entry not found in the requested brand/customer scope.
Historical integration contracts
These routes keep their string payloads and do not accept the new mode/version fields. Prefer the entry endpoints when you need explicit creation or conflict detection.
POST/brands/{brandId}/customers/{customerId}/metadataSave Customer Metadata (Historical Upsert)
Create or overwrite the entry in the same namespace. data is a required string; namespace is optional or null, maximum 255 characters. No updatedAt conflict check. innovorder.provenance is protected.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| brandId | integer | Yes | Brand owning the customer. |
| customerId | integer | Yes | Customer in that brand. |
Request Body
{
"namespace": "external_ids",
"data": "{\"externalId\":\"EXT-99887\"}"
}Request Body Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Example | Description |
|---|---|---|---|
| namespace | string | "external_ids" | The namespace value. |
| data | string | "{\"externalId\":\"EXT-99887\"}" | Endpoint-specific response payload. |
Response
{
"status": 200,
"code": "customer_metadata_saved",
"message": "The customer metadata has been saved.",
"data": {
"customerMetadataId": 1235,
"customerId": 5337578,
"brandId": 100,
"namespace": "external_ids",
"data": "{\"externalId\":\"EXT-99887\"}",
"createdAt": "2026-09-21T09:00:00.000Z",
"updatedAt": "2026-09-21T09:00:00.000Z"
}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Example | Description |
|---|---|---|---|
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "customer_metadata_saved" | Machine-readable application code for the result. |
| message | string | "The customer metadata has been saved." | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.customerMetadataId | integer | 1235 | Identifier of the associated customer metadata. |
| data.customerId | integer | 5337578 | Identifier of the customer. |
| data.brandId | integer | 100 | Identifier of the brand. |
| data.namespace | string | "external_ids" | The namespace value. |
| data.data | string | "{\"externalId\":\"EXT-99887\"}" | Endpoint-specific response payload. |
| data.createdAt | string | "2026-09-21T09:00:00.000Z" | Timestamp when this resource was created. |
| data.updatedAt | string | "2026-09-21T09:00:00.000Z" | Timestamp when this resource was last updated. |
DELETE/brands/{brandId}/customers/{customerId}/metadataDelete Customer Metadata (Historical Contract)
Delete entries in the optional namespace query filter. Without that filter, delete all non-reserved entries and retain innovorder.provenance. Explicitly targeting the reserved namespace is forbidden. No updatedAt conflict check.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| brandId | integer | Yes | Brand owning the customer. |
| customerId | integer | Yes | Customer in that brand. |
| namespace | string | No | Query parameter, maximum 255 characters. Omit to delete non-reserved entries; provenance is always retained. |
Response
{
"status": 200,
"code": "customer_metadata_deleted",
"message": "The customer metadata has been deleted.",
"data": {}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Example | Description |
|---|---|---|---|
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "customer_metadata_deleted" | Machine-readable application code for the result. |
| message | string | "The customer metadata has been deleted." | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
Optional Customer Provenance
innovorder.provenance is reserved and managed by the API. Do not write it through any metadata endpoint. Instead, authenticated staff/integration callers may include an optional provenance object in POST /brands/{brandId}/customers, PUT /customers/{customerId} or PUT /brands/{brandId}/customers/loader. The loader accepts Admin/Brand accounts. Customer self-service callers cannot submit an external provenance context.
Backward compatibility: existing creation, update and loader payloads remain valid without provenance. Omitting it does not activate tracking, erase an existing origin or stop tracking a customer that already has valid provenance. For HTTP, omit the field when unused; its nested fields are required only when it is supplied.
Provenance request fields
source(required):idandlabel, non-empty strings up to 255 characters;channel, non-empty string up to 64 characters. Supply a stable integration ID and a readable source label.entryMode(required): non-empty string up to 64 characters, for exampleform,importormanual. Channel and mode are generic strings, not enumerations.reference(optional): external request/import reference, up to 255 characters, or null.actors(required): array of up to 10 declared actors; it may be empty. Each actor requires non-emptyidanddisplayName(up to 255 characters), androle(up to 64). Roles such asrequesterandapproverdistinguish the participants. Names are snapshots, not live lookups.at,authenticatedActorand the stored document'sversionare assigned by the API, not accepted as provenance request fields.
Example fragment to add to an otherwise valid customer creation/update/loader body:
{
"provenance": {
"source": {
"id": "client-portal",
"label": "Client portal",
"channel": "portal"
},
"entryMode": "form",
"reference": "request-123",
"actors": [
{
"id": "portal:user:42",
"displayName": "Alice Dupont",
"role": "requester"
},
{
"id": "portal:user:84",
"displayName": "Theo Martin",
"role": "approver"
}
]
}
}An EDI connector supplies its own source, for example { id: "edi-connector", label: "EDI import", channel: "edi" }, with entryMode: "import". For manual entry by an approver, use entryMode: "manual" and an actor with role: "approver"; this contract does not introduce a new portal workflow. The authenticated account is recorded separately from these declared participants.
Stored document and lifecycle
The metadata entry contains a versioned document with optional creation and lastModification events. Example decoded data:
{
"version": 1,
"creation": {
"at": "2026-09-21T09:00:00.000Z",
"source": {
"id": "client-portal",
"label": "Client portal",
"channel": "portal"
},
"entryMode": "form",
"reference": "request-123",
"actors": [
{
"id": "portal:user:42",
"displayName": "Alice Dupont",
"role": "requester"
},
{
"id": "portal:user:84",
"displayName": "Theo Martin",
"role": "approver"
}
],
"authenticatedActor": {
"id": "innovorder:user:12",
"displayName": "Portal integration",
"role": "operator"
}
}
}- Only an explicit provenance context initializes tracking. A new customer gets a creation event. For an existing customer, the first effective change with context records only lastModification; the API never reconstructs its origin.
- After activation, effective profile, student/pricing/validity/status, badge and non-reserved metadata changes update lastModification, even without a new external context. The creation event stays unchanged. Identical reimports and unchanged metadata values do not replace the last trace.
- The API supplies the server timestamp and available authenticated identity. Generic changes use an Innovorder/API source; staff and self-service customers have distinct actor identities. Declared requester/approver actors are claims from the authenticated integration, not independently authenticated accounts.
- Changing another metadata entry does not activate provenance. Reserved internal writes do not recursively create a new trace. Data and provenance are written in the same transaction, with writes serialized per customer.
- Credentials/authentication, orders and wallet movements are excluded. This is creation plus latest modification, not a complete audit history. Direct maintenance SQL/model writes are outside application tracking.
- The backoffice displays Traceability only when the namespace contains a valid version-1 document with at least one event. Missing, empty or invalid provenance produces no section. Its metadata row is read-only and marked as automatically managed.
HTTP, Pub/Sub and rollout
Customer commands can carry the same optional provenance context through HTTP and the import Pub/Sub pipeline. The Avro customer command adds a nullable provenance field with default null; the consumer supports existing serialized commands as well as new ones. Old producers do not need to populate it. This is a command context, not a direct write to the reserved metadata namespace.
Deploy the compatible API and import consumer before enabling provenance in producers, then deploy the backoffice. No new table, historical backfill or full event history is required.