LoyaltyImports and exports
Loyalty
Imports and exports
Import customers in batches and export them as CSV.
Send a batch of up to 100 customers. Revisit validates it, queues it and enrolls the rows in the background, then you read the outcome per row by your own rowRef. A dry run validates without writing anything.
An export is a CSV of customers and their memberships with masked phones, kept for 24 hours. Exporting and erasing need customers:export and customers:erase, which are in no preset: the owner ticks them on purpose.
Paths are relative to https://app.revisit.cards/v1/developer.
Endpoints
- POSTCheck a batch of customers without importing it
/programs/{id}/customer-imports/validate - POSTQueue a batch of customers to be enrolled in a program
/programs/{id}/customer-imports - GETRead an import's progress
/customer-imports/{jobId} - GETRead each row's outcome
/customer-imports/{jobId}/rows - POSTTry an import's retryable failed rows again
/customer-imports/{jobId}/retry - GETList this application's recent exports
/customer-exports - POSTExport customers to a CSV file
/customer-exports - GETRead an export's status
/customer-exports/{exportId} - GETDownload an export file
/customer-exports/{exportId}/download
Check a batch of customers without importing it
/programs/{id}/customer-imports/validateScopecustomers:write
Requires customers:write. Changes nothing and needs no Idempotency-Key. Answers, for each rowRef, whether the row is well formed (valid), which fields were refused (fields, names only) and whether the customer id is already a member of the program (alreadyMember, so importing it would be a no-op).
Path
iduuidRequired
Body
branchIduuid- The branch the customers join at; omitted, the program's own or first open one.
rowsobject[]Requiredrows[].rowRefstringRequired- Your own reference for this row; unique within the import. Failures are reported by it.
rows[].externalCustomerIdstringRequiredrows[].namestringrows[].phonestring- Bangladesh in any form, or +<country><number>. Never returned.
rows[].emailemailrows[].localestring
Returns 200
invalidintegerRequiredtotalintegerRequiredvalidintegerRequiredrowsobject[]Requiredrows[].alreadyMemberbooleanRequiredrows[].codestring or nullRequiredrows[].fieldsstring[]Requiredrows[].rowRefstringRequiredrows[].validbooleanRequired
curl -X POST "https://app.revisit.cards/v1/developer/programs/e01b010d-2c65-48e1-83a0-acad79f8d3b4/customer-imports/validate" \
-H "Authorization: Bearer $DEVELOPER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"rows": [
{
"externalCustomerId": "user-42",
"rowRef": "row-2"
}
]
}'{
"data": {
"invalid": 0,
"rows": [
{
"alreadyMember": false,
"code": "WELCOME10",
"fields": [
"phone"
],
"rowRef": "row-2",
"valid": false
}
],
"total": 12,
"valid": 1
},
"requestId": "ba6f48f5-dd7f-482f-b9bb-0102748ceb52",
"success": true
}Queue a batch of customers to be enrolled in a program
/programs/{id}/customer-importsScopecustomers:write
Requires customers:write and an Idempotency-Key. Up to 100 rows and 16 KiB of JSON per import (a larger migration is many imports; more than five waiting or running is 429 IMPORT_BACKLOG). Answers 202 with a jobId once the batch is recorded: queued means nothing has been enrolled yet. Rows with a bad field are accepted as failed rows that name their rowRef (INVALID_ROW); a bad envelope, a repeated rowRef or more than 100 rows is 422. Each row is enrolled exactly as POST /programs/{id}/memberships does it, by your own customer id: no consent is recorded, an existing opt-out is kept and a repeat is existing, never a second membership. Read the job and its rows to follow progress.
Send an Idempotency-Key header. Safe to retry.
Path
iduuidRequired
Body
branchIduuid- The branch the customers join at; omitted, the program's own or first open one.
rowsobject[]Requiredrows[].rowRefstringRequired- Your own reference for this row; unique within the import. Failures are reported by it.
rows[].externalCustomerIdstringRequiredrows[].namestringrows[].phonestring- Bangladesh in any form, or +<country><number>. Never returned.
rows[].emailemailrows[].localestring
Returns 202
invalidintegerRequired- Rows already recorded as failed because they were not well formed.
jobIdstringRequiredprogramIdstringRequiredstatusenumRequired- One of
queued,running,completed. totalintegerRequired
Errors
- 429
IMPORT_BACKLOGthe business already has the most customer imports waiting or running it may have. Nothing was queued; retry when one finishes.
curl -X POST "https://app.revisit.cards/v1/developer/programs/e01b010d-2c65-48e1-83a0-acad79f8d3b4/customer-imports" \
-H "Authorization: Bearer $DEVELOPER_API_KEY" \
-H "Idempotency-Key: 5f0c2a8e-7b1d-4c3e-9a6f-0d4b8e948034" \
-H "Content-Type: application/json" \
-d '{
"rows": [
{
"externalCustomerId": "user-42",
"rowRef": "row-2"
}
]
}'{
"data": {
"invalid": 0,
"jobId": "2b472adc-6c4e-4b80-9f03-646ebc16b86e",
"programId": "d7595bbd-4342-41b4-9037-2e61a9f8403f",
"status": "queued",
"total": 12
},
"requestId": "17f1ba31-0af2-406b-9fe5-eee7b681b64b",
"success": true
}Read an import's progress
/customer-imports/{jobId}Scopecustomers:write
Requires customers:write. Counts come from the rows. status is completed only when no row is pending; queued and running mean work is still to do. An import of another application, business or environment is 404.
Path
jobIduuidRequired
Returns 200
completedAtstring or nullRequiredcreatedintegerRequiredcreatedAtstringRequiredexistingintegerRequiredfailedintegerRequiredjobIdstringRequiredpendingintegerRequiredprogramIdstringRequiredretryableintegerRequired- Failed rows a retry would try again.
startedAtstring or nullRequiredstatusenumRequired- One of
queued,running,completed. totalintegerRequired
curl "https://app.revisit.cards/v1/developer/customer-imports/2b472adc-6c4e-4b80-9f03-646ebc16b86e" \
-H "Authorization: Bearer $DEVELOPER_API_KEY"{
"data": {
"completedAt": "2026-11-03T08:41:12.000Z",
"created": 1,
"createdAt": "2026-11-03T08:41:12.000Z",
"existing": 7,
"failed": 2,
"jobId": "2b472adc-6c4e-4b80-9f03-646ebc16b86e",
"pending": 4,
"programId": "d7595bbd-4342-41b4-9037-2e61a9f8403f",
"retryable": 1,
"startedAt": "2026-11-03T08:41:12.000Z",
"status": "queued",
"total": 12
},
"requestId": "18256ad6-b0a8-4607-bded-47c02e1b8b80",
"success": true
}Read each row's outcome
/customer-imports/{jobId}/rowsScopecustomers:write
Requires customers:write. Rows come in the order sent, limit 1 to 50 (default 25), optional status filter. A row is named by your rowRef; no name, phone or email is ever returned, and a failure carries a fixed code (INVALID_ROW, CUSTOMER_REFERENCE_CONFLICT, a plan refusal such as PLAN_READ_ONLY, EXPIRED, INTERNAL) and the names of refused fields.
Path
jobIduuidRequired
Query
limitinteger- 1 to 50. Default
25. offsetinteger- Default
0. statusenum- One of
pending,created,existing,failed.
Returns 200
hasMorebooleanRequireditemsobject[]Requireditems[].codestring or nullRequireditems[].customerIdstring or nullRequireditems[].externalCustomerIdstring or nullRequireditems[].fieldsstring[]Required- Names of refused fields, never values.
items[].membershipIdstring or nullRequireditems[].messagestring or nullRequireditems[].positionintegerRequireditems[].retryablebooleanRequireditems[].rowRefstringRequireditems[].statusenumRequired- One of
pending,created,existing,failed. limitintegerRequiredoffsetintegerRequired
curl "https://app.revisit.cards/v1/developer/customer-imports/2b472adc-6c4e-4b80-9f03-646ebc16b86e/rows" \
-H "Authorization: Bearer $DEVELOPER_API_KEY"{
"data": {
"hasMore": false,
"items": [
{
"code": "WELCOME10",
"customerId": "eb83b9c0-5619-432d-b857-cef7fcf84edb",
"externalCustomerId": "user-42",
"fields": [
"phone"
],
"membershipId": "b53ca741-1e76-4ddb-b6a5-998b65c4aff3",
"message": "Your 10th chai is on us this week.",
"position": 1,
"retryable": false,
"rowRef": "row-2",
"status": "pending"
}
],
"limit": 50,
"offset": 0
},
"requestId": "9f237955-9fc5-4e63-a971-262faec19f0c",
"success": true
}Try an import's retryable failed rows again
/customer-imports/{jobId}/retryScopecustomers:write
Requires customers:write and an Idempotency-Key. Only a completed import can be retried (409 IMPORT_NOT_FINISHED otherwise). Rows that failed for a reason that can pass later (a plan refusal, an unexpected error) and whose details Revisit still holds (seven days) go back to the queue; bad rows and conflicts do not (retryable: false). Rows already enrolled are never repeated.
Send an Idempotency-Key header. Safe to retry.
Path
jobIduuidRequired
Returns 200
jobIdstringRequiredrequeuedintegerRequiredstatusenumRequired- One of
queued,running,completed.
Errors
- 409
IMPORT_NOT_FINISHEDa retry was asked of an import that is still queued or running.
curl -X POST "https://app.revisit.cards/v1/developer/customer-imports/2b472adc-6c4e-4b80-9f03-646ebc16b86e/retry" \
-H "Authorization: Bearer $DEVELOPER_API_KEY" \
-H "Idempotency-Key: 5f0c2a8e-7b1d-4c3e-9a6f-0d4b8e1583ea"{
"data": {
"jobId": "2b472adc-6c4e-4b80-9f03-646ebc16b86e",
"requeued": 0,
"status": "queued"
},
"requestId": "b4fb04bf-b8ad-4a98-9e41-ed5184799eb4",
"success": true
}List this application's recent exports
/customer-exportsScopecustomers:export
Requires customers:export. The newest 20 exports of this application, most recent first.
Returns 200
itemsobject[]Requireditems[].exportIdstringRequireditems[].statusenumRequired- One of
ready,expired. items[].rowCountintegerRequired- Rows in the file; 0 once it is gone.
items[].createdAtdate-timeRequireditems[].expiresAtdate-timeRequireditems[].downloadPathstring or nullRequired- Set only while the file can be downloaded.
items[].expiredReasonenum or nullRequired- One of
time,customer_erased. items[].programIdstring or nullRequireditems[].branchIdstring or nullRequired
curl "https://app.revisit.cards/v1/developer/customer-exports" \
-H "Authorization: Bearer $DEVELOPER_API_KEY"{
"data": {
"items": [
{
"branchId": "5eff65d7-d01c-4bd3-b26a-3d06b21fd005",
"createdAt": "2026-11-03T08:41:12.000Z",
"downloadPath": "/v1/developer/displays/download",
"expiredReason": "time",
"expiresAt": "2026-11-03T08:41:12.000Z",
"exportId": "4b4520f4-2b56-4f23-97d1-996ddbadd5ba",
"programId": "d7595bbd-4342-41b4-9037-2e61a9f8403f",
"rowCount": 12,
"status": "ready"
}
]
},
"requestId": "12753b24-e805-455f-81e9-1315662e24d4",
"success": true
}Export customers to a CSV file
/customer-exportsScopecustomers:export
Requires customers:export and an Idempotency-Key. The scope is never part of the Read-only or Full access presets: an owner ticks it under "Choose permissions". The file holds the customers and memberships this key may already read (GET /customers), one row per membership, with the same masked phone (•••• 1234); a whole phone number and an email are never in it. Name cells that start with =, +, - or @ get a leading apostrophe so a spreadsheet never runs them. The file is built inside the request, so status is ready when this answers (a refusal answers an error, never a half file). At most 5,000 customers per file (422 EXPORT_TOO_LARGE: narrow by programId or branchId), 5 exports an hour per key (429), and 5 ready files per business (429 EXPORT_BACKLOG). A file can be downloaded for 24 hours and only by the application that made it. A repeat with the same key replays the same export (Idempotent-Replay).
Send an Idempotency-Key header. Safe to retry.
Body
programIduuidbranchIduuid
Returns 200
exportIdstringRequiredstatusenumRequired- One of
ready,expired. rowCountintegerRequired- Rows in the file; 0 once it is gone.
createdAtdate-timeRequiredexpiresAtdate-timeRequireddownloadPathstring or nullRequired- Set only while the file can be downloaded.
expiredReasonenum or nullRequired- One of
time,customer_erased. programIdstring or nullRequiredbranchIdstring or nullRequired
Errors
- 422
EXPORT_TOO_LARGEmore customers match than one export holds; narrow it by program or branch. - 429
EXPORT_BACKLOGthe business already holds the most ready exports it may; wait for one to expire.
curl -X POST "https://app.revisit.cards/v1/developer/customer-exports" \
-H "Authorization: Bearer $DEVELOPER_API_KEY" \
-H "Idempotency-Key: 5f0c2a8e-7b1d-4c3e-9a6f-0d4b8e5c203d" \
-H "Content-Type: application/json" \
-d '{
"branchId": "5eff65d7-d01c-4bd3-b26a-3d06b21fd005",
"programId": "d7595bbd-4342-41b4-9037-2e61a9f8403f"
}'{
"data": {
"branchId": "5eff65d7-d01c-4bd3-b26a-3d06b21fd005",
"createdAt": "2026-11-03T08:41:12.000Z",
"downloadPath": "/v1/developer/displays/download",
"expiredReason": "time",
"expiresAt": "2026-11-03T08:41:12.000Z",
"exportId": "4b4520f4-2b56-4f23-97d1-996ddbadd5ba",
"programId": "d7595bbd-4342-41b4-9037-2e61a9f8403f",
"rowCount": 12,
"status": "ready"
},
"requestId": "f280f756-05e8-4712-ac02-c6bd57ce3aa9",
"success": true
}Read an export's status
/customer-exports/{exportId}Scopecustomers:export
Requires customers:export. status is ready (with downloadPath) or expired. An export expires when its 24 hours pass (expiredReason: time) or when any customer in it is erased (customer_erased): the file is wiped at once. An export of another application, business or environment is 404.
Path
exportIduuidRequired
Returns 200
exportIdstringRequiredstatusenumRequired- One of
ready,expired. rowCountintegerRequired- Rows in the file; 0 once it is gone.
createdAtdate-timeRequiredexpiresAtdate-timeRequireddownloadPathstring or nullRequired- Set only while the file can be downloaded.
expiredReasonenum or nullRequired- One of
time,customer_erased. programIdstring or nullRequiredbranchIdstring or nullRequired
curl "https://app.revisit.cards/v1/developer/customer-exports/4b4520f4-2b56-4f23-97d1-996ddbadd5ba" \
-H "Authorization: Bearer $DEVELOPER_API_KEY"{
"data": {
"branchId": "5eff65d7-d01c-4bd3-b26a-3d06b21fd005",
"createdAt": "2026-11-03T08:41:12.000Z",
"downloadPath": "/v1/developer/displays/download",
"expiredReason": "time",
"expiresAt": "2026-11-03T08:41:12.000Z",
"exportId": "4b4520f4-2b56-4f23-97d1-996ddbadd5ba",
"programId": "d7595bbd-4342-41b4-9037-2e61a9f8403f",
"rowCount": 12,
"status": "ready"
},
"requestId": "1986f7a8-23f4-4ff5-8655-c6c399579d1f",
"success": true
}Download an export file
/customer-exports/{exportId}/downloadScopecustomers:export
Requires customers:export. Sends the CSV (text/csv, Content-Disposition: attachment, Cache-Control: no-store, X-Content-Type-Options: nosniff, and X-Export-Sha256 of the body). The first line is the header: customer_id, external_customer_id, member_code, name, masked_phone, locale, customer_created_at, membership_id, program_id, card_type, balance, lifetime_earned, marketing_opt_in, consent_source, consent_at, joined_at, joined_branch_id, last_activity_at. 410 EXPORT_EXPIRED once the file is gone. Each download is audited.
Path
exportIduuidRequired
Answers with a file: text/csv.
Errors
- 410
EXPORT_EXPIREDthe export file is gone (its day passed, or a customer in it was erased). Make a new export.
curl "https://app.revisit.cards/v1/developer/customer-exports/4b4520f4-2b56-4f23-97d1-996ddbadd5ba/download" \
-H "Authorization: Bearer $DEVELOPER_API_KEY" \
-o downloadHTTP/1.1 200
Content-Type: text/csv
(file)