Skip to content
LoyaltyImports and exports

Loyalty

Imports and exports

Import customers in batches and export them as CSV.

Pilotv1

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.

Check a batch of customers without importing it

POST/programs/{id}/customer-imports/validate

Scopecustomers: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[]Required
rows[].rowRefstringRequired
Your own reference for this row; unique within the import. Failures are reported by it.
rows[].externalCustomerIdstringRequired
rows[].namestring
rows[].phonestring
Bangladesh in any form, or +<country><number>. Never returned.
rows[].emailemail
rows[].localestring

Returns 200

invalidintegerRequired
totalintegerRequired
validintegerRequired
rowsobject[]Required
rows[].alreadyMemberbooleanRequired
rows[].codestring or nullRequired
rows[].fieldsstring[]Required
rows[].rowRefstringRequired
rows[].validbooleanRequired
Request
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"
      }
    ]
  }'
Response
{
  "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

POST/programs/{id}/customer-imports

Scopecustomers: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[]Required
rows[].rowRefstringRequired
Your own reference for this row; unique within the import. Failures are reported by it.
rows[].externalCustomerIdstringRequired
rows[].namestring
rows[].phonestring
Bangladesh in any form, or +<country><number>. Never returned.
rows[].emailemail
rows[].localestring

Returns 202

invalidintegerRequired
Rows already recorded as failed because they were not well formed.
jobIdstringRequired
programIdstringRequired
statusenumRequired
One of queued, running, completed.
totalintegerRequired

Errors

  • 429IMPORT_BACKLOGthe business already has the most customer imports waiting or running it may have. Nothing was queued; retry when one finishes.
Request
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"
      }
    ]
  }'
Response
{
  "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

GET/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 nullRequired
createdintegerRequired
createdAtstringRequired
existingintegerRequired
failedintegerRequired
jobIdstringRequired
pendingintegerRequired
programIdstringRequired
retryableintegerRequired
Failed rows a retry would try again.
startedAtstring or nullRequired
statusenumRequired
One of queued, running, completed.
totalintegerRequired
Request
curl "https://app.revisit.cards/v1/developer/customer-imports/2b472adc-6c4e-4b80-9f03-646ebc16b86e" \
  -H "Authorization: Bearer $DEVELOPER_API_KEY"
Response
{
  "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

GET/customer-imports/{jobId}/rows

Scopecustomers: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

hasMorebooleanRequired
itemsobject[]Required
items[].codestring or nullRequired
items[].customerIdstring or nullRequired
items[].externalCustomerIdstring or nullRequired
items[].fieldsstring[]Required
Names of refused fields, never values.
items[].membershipIdstring or nullRequired
items[].messagestring or nullRequired
items[].positionintegerRequired
items[].retryablebooleanRequired
items[].rowRefstringRequired
items[].statusenumRequired
One of pending, created, existing, failed.
limitintegerRequired
offsetintegerRequired
Request
curl "https://app.revisit.cards/v1/developer/customer-imports/2b472adc-6c4e-4b80-9f03-646ebc16b86e/rows" \
  -H "Authorization: Bearer $DEVELOPER_API_KEY"
Response
{
  "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

POST/customer-imports/{jobId}/retry

Scopecustomers: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

jobIdstringRequired
requeuedintegerRequired
statusenumRequired
One of queued, running, completed.

Errors

  • 409IMPORT_NOT_FINISHEDa retry was asked of an import that is still queued or running.
Request
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"
Response
{
  "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

GET/customer-exports

Scopecustomers:export

Requires customers:export. The newest 20 exports of this application, most recent first.

Returns 200

itemsobject[]Required
items[].exportIdstringRequired
items[].statusenumRequired
One of ready, expired.
items[].rowCountintegerRequired
Rows in the file; 0 once it is gone.
items[].createdAtdate-timeRequired
items[].expiresAtdate-timeRequired
items[].downloadPathstring or nullRequired
Set only while the file can be downloaded.
items[].expiredReasonenum or nullRequired
One of time, customer_erased.
items[].programIdstring or nullRequired
items[].branchIdstring or nullRequired
Request
curl "https://app.revisit.cards/v1/developer/customer-exports" \
  -H "Authorization: Bearer $DEVELOPER_API_KEY"
Response
{
  "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

POST/customer-exports

Scopecustomers: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

programIduuid
branchIduuid

Returns 200

exportIdstringRequired
statusenumRequired
One of ready, expired.
rowCountintegerRequired
Rows in the file; 0 once it is gone.
createdAtdate-timeRequired
expiresAtdate-timeRequired
downloadPathstring or nullRequired
Set only while the file can be downloaded.
expiredReasonenum or nullRequired
One of time, customer_erased.
programIdstring or nullRequired
branchIdstring or nullRequired

Errors

  • 422EXPORT_TOO_LARGEmore customers match than one export holds; narrow it by program or branch.
  • 429EXPORT_BACKLOGthe business already holds the most ready exports it may; wait for one to expire.
Request
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"
  }'
Response
{
  "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

GET/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

exportIdstringRequired
statusenumRequired
One of ready, expired.
rowCountintegerRequired
Rows in the file; 0 once it is gone.
createdAtdate-timeRequired
expiresAtdate-timeRequired
downloadPathstring or nullRequired
Set only while the file can be downloaded.
expiredReasonenum or nullRequired
One of time, customer_erased.
programIdstring or nullRequired
branchIdstring or nullRequired
Request
curl "https://app.revisit.cards/v1/developer/customer-exports/4b4520f4-2b56-4f23-97d1-996ddbadd5ba" \
  -H "Authorization: Bearer $DEVELOPER_API_KEY"
Response
{
  "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

GET/customer-exports/{exportId}/download

Scopecustomers: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

  • 410EXPORT_EXPIREDthe export file is gone (its day passed, or a customer in it was erased). Make a new export.
Request
curl "https://app.revisit.cards/v1/developer/customer-exports/4b4520f4-2b56-4f23-97d1-996ddbadd5ba/download" \
  -H "Authorization: Bearer $DEVELOPER_API_KEY" \
  -o download
Response
HTTP/1.1 200
Content-Type: text/csv

(file)