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
| Program | Export to use | vendor |
|---|---|---|
| Smile | Customers export (CSV) | smile |
| LoyaltyLion | Customers export (CSV) | loyaltylion |
| Yotpo | Loyalty customers export (CSV) | yotpo |
| Anything | The generic format below | generic |
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:
| Field | Smile | LoyaltyLion | Yotpo |
|---|---|---|---|
| Customer id | customer_id, shopify_customer_id | merchant_id, customer_id | third_party_id, customer_id |
email, customer_email | email | email, customer_email | |
| Balance | points_balance, points | points_approved, approved_points | points_balance, point_balance |
| Lifetime total | points_earned, total_points_earned | lifetime_points, total_points_earned | points_earned, total_points_earned |
| Spent | points_spent | points_spent, spent_points | points_redeemed, points_spent |
| Tier | vip_tier, vip_tier_name | loyalty_tier, tier, tier_name | vip_tier_name, vip_tier, tier |
| Birthday | birthday, date_of_birth | birthday, date_of_birth | birthday, 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-04external_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-DDorMM-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(defaulttrue): customers that do not exist yet are created with the file's external id. The file's email is stored as theemailattribute of a customer who has none; an email a customer already has is never changed.allow_existing(defaultfalse): a customer who already holds points in the currency, or whom an earlier import already imported, is skipped unless this istrue, 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) oremail_attribute, which finds customers by theiremailattribute. 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.
balancedistrueonly when every line of the file was imported and the ledger holds exactly thesourcetotals. An excluded, skipped or failed line, or a dry run alone, is never balanced.eligible_reconciledis the same check overeligible: every line your options allowed was imported and the ledger holds exactly theeligibletotals. Use it when you left customers out on purpose.imported_subset_reconciledis 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.
Klaviyo connector
Keep every customer's Klaviyo profile current with points, VIP tier, birthday, referral code and store credit, and send loyalty activity to Klaviyo as metrics for flows.
Partner rewards and gift cards
Supply catalogue rewards through a gift-card supplier or a brand partner, share the cost with the partner, and settle per period.