Orishare
Loyalty guides

Import from another loyalty program

Move customers, point balances, lifetime totals, VIP tiers and birthdays from Smile, LoyaltyLion, Yotpo or any CSV file, check the result before anything is written, and reconcile it against the ledger afterwards.

An import takes the customer export of your current loyalty program and gives every customer the same balance, the same lifetime total and, if you want, the same tier and birthday. Nothing is written until you have seen a dry run of the whole file, and every import ends with a report that compares the file with what the ledger holds.

All calls use a secret key with ledger:write (reads need ledger:read) and an Idempotency-Key on every POST.

1. Export from your current program

ProgramExport to usevendor
SmileCustomers export (CSV)smile
LoyaltyLionCustomers export (CSV)loyaltylion
YotpoLoyalty customers export (CSV)yotpo
AnythingThe generic format belowgeneric

Headers are matched without regard to case, spaces or punctuation, so Points Balance, points_balance and pointsBalance are the same column. The columns read for each vendor:

FieldSmileLoyaltyLionYotpo
Customer idcustomer_id, shopify_customer_idmerchant_id, customer_idthird_party_id, customer_id
Emailemail, customer_emailemailemail, customer_email
Balancepoints_balance, pointspoints_approved, approved_pointspoints_balance, point_balance
Lifetime totalpoints_earned, total_points_earnedlifetime_points, total_points_earnedpoints_earned, total_points_earned
Spentpoints_spentpoints_spent, spent_pointspoints_redeemed, points_spent
Tiervip_tier, vip_tier_nameloyalty_tier, tier, tier_namevip_tier_name, vip_tier, tier
Birthdaybirthday, date_of_birthbirthday, date_of_birthbirthday, or birthday_month and birthday_day

When a file has no lifetime column but has a spent column, the lifetime total is the balance plus what was spent; with neither, it equals the balance. Pending points (LoyaltyLion points_pending) are not imported. If your export uses other headers, rename them or use the generic format.

The generic format

external_id,email,points_balance,lifetime_points,tier,birthday
cust_1001,[email protected],120,500,Gold,1990-04-12
cust_1002,[email protected],75,,,--07-04
  • external_id: your id for the customer (required unless you match by email).
  • points_balance: whole points, at least 0. Thousands separators are fine; decimals are refused.
  • lifetime_points: optional, never below the balance.
  • tier: optional, a level name of the tier program you name on the import.
  • birthday: optional, YYYY-MM-DD or MM-DD (also --MM-DD). Other spellings are left out with a warning.

Files are read as standard CSV: quoted fields, quotes written twice inside them, a byte order mark and Windows line endings are all fine. A quote in the wrong place (text after a closing quote, as in "1"2, or a quote inside an unquoted value) makes that line an error rather than guessing what it meant.

2. Create the import and upload the file

curl -X POST https://api.orishare.com/v1/imports \
  -H "Authorization: Bearer $ORISHARE_SECRET_KEY" \
  -H "Idempotency-Key: import-smile-2026-10" \
  -H "Content-Type: application/json" \
  -d '{ "vendor": "smile", "currency": "points", "tier_program": "tp_..." }'

Options:

  • create_customers (default true): customers that do not exist yet are created with the file's external id. The file's email is stored as the email attribute of a customer who has none; an email a customer already has is never changed.
  • allow_existing (default false): a customer who already holds points in the currency, or whom an earlier import already imported, is skipped unless this is true, in which case the imported amounts are added to what they hold. Merged customers count as one: points held by any profile of the customer count, and two lines naming profiles of the same customer are an error.
  • match_by: external_id (default) or email_attribute, which finds customers by their email attribute. When a line's external id belongs to a customer with another email, the line is an error (customer_conflict) and nothing is imported for it.

Send the CSV text in parts of up to 400,000 characters, numbered from 0. A part may end anywhere, even in the middle of a line:

curl -X POST https://api.orishare.com/v1/imports/imp_.../chunks \
  -H "Authorization: Bearer $ORISHARE_SECRET_KEY" \
  -H "Idempotency-Key: import-smile-2026-10-part-0" \
  -H "Content-Type: application/json" \
  -d '{ "sequence": 0, "content": "Email,Customer ID,Points Balance,..." }'

Sending the same part again changes nothing; different text for a part already sent is refused. One import holds up to 20 MB and 100,000 customers; split larger files into several imports.

3. Dry run

curl -X POST https://api.orishare.com/v1/imports/imp_.../dry_run \
  -H "Authorization: Bearer $ORISHARE_SECRET_KEY" \
  -H "Idempotency-Key: import-smile-2026-10-dry-run" \
  -H "Content-Type: application/json" -d '{}'

The dry run reads the whole file, checks every line, matches customers and adds up the totals. It writes no points, customers or tiers. The report on the import tells you:

  • which header each field was read from (columns), so a wrong mapping shows at once;
  • how many customers will be imported, created, or skipped because they already hold points or do not exist;
  • the total balance and lifetime points of the file;
  • every line that cannot be imported and why: a negative or fractional amount, a lifetime below the balance, a customer named twice, an unknown tier, or a lifetime above the balance in a currency that cannot be spent.

If any line has an error the import is blocked; fix the file, create a new import and dry run again. Otherwise it is ready. Adding a part after a dry run returns the import to draft: it only runs after a dry run of its current content.

4. Run

curl -X POST https://api.orishare.com/v1/imports/imp_.../run \
  -H "Authorization: Bearer $ORISHARE_SECRET_KEY" \
  -H "Idempotency-Key: import-smile-2026-10-run-1" \
  -H "Content-Type: application/json" -d '{ "limit": 200 }'

Each call imports up to limit customers (500 at most) and answers with the progress and has_more. Call it again, with a new Idempotency-Key, until has_more is false; the import is then completed and can no longer change. Calls may overlap or be repeated after a timeout: every customer is imported exactly once.

What each customer gets:

  • an opening entry of their lifetime total, and, when the lifetime total is above the balance, a second entry that takes the difference back off. Both are marked as a migration in the ledger, so their balance and lifetime earned match the file exactly;
  • their tier, set as a manual tier assignment, when the import names a tier program;
  • their birthday, unless they already have one (the date they chose is kept).

If someone changes a customer's tier while the import is running, that change is kept and the line gets a tier_changed warning.

5. Reconcile

GET /v1/imports/{id}/report compares the file with the ledger, line by line: every imported line must have its entries, on its customer, in the import's currency, with exactly its amounts. It gives the totals (as exact numbers in strings) of the imported, skipped and failed lines, the ledger totals, any line that does not match and the first errors.

source is the whole file. Lines left out by an option you chose (a customer who already holds points or was imported before, without allow_existing; a customer who does not exist, with create_customers: false) stay in source and are also listed under excluded with their reasons; eligible is source without them.

  • balanced is true only when every line of the file was imported and the ledger holds exactly the source totals. An excluded, skipped or failed line, or a dry run alone, is never balanced.
  • eligible_reconciled is the same check over eligible: every line your options allowed was imported and the ledger holds exactly the eligible totals. Use it when you left customers out on purpose.
  • imported_subset_reconciled is the weakest check: every imported line matches the ledger.

GET /v1/imports/{id}/rows?status=skipped (or failed, invalid) lists the customers to look at, each with its reason.

On this page