Moving lots of customers at once
Sarah has 300 customers in a spreadsheet. Here's how to get them in, get them back out, and watch a long job finish.
Sarah is moving off her old system. Three hundred customers in a spreadsheet, and she isn't adding them one at a time.
Bulk work doesn't happen while you wait. You start it, get a receipt, and check back. Same in both directions.
It's also the pattern for any long-running job elsewhere in the API, so it's worth reading once even if you never touch a spreadsheet.
Get the template first
GET /api/modules/clients/import-export/templatecurl https://api.hyphabase.com/api/modules/clients/import-export/template \
-u sk_live_xxx: -H "X-Location-Id: $HYPHABASE_LOCATION_ID" \
-o customers-template.csvComes back as CSV with the header row we expect. Build your file from this rather than guessing column names — the structure is checked before the job even starts, so a wrong header fails immediately instead of halfway through.
Send it in
POST /api/modules/clients/import-export/importSent as multipart/form-data:
| Field | Notes |
|---|---|
file | CSV, 5 MB maximum. Split anything bigger. |
location_ids[] | Optional. Which sites these people belong to. Defaults to the site you're calling as. |
curl -X POST https://api.hyphabase.com/api/modules/clients/import-export/import \
-u sk_live_xxx: \
-H "X-Location-Id: $HYPHABASE_LOCATION_ID" \
-F "file=@customers.csv" \
-F "location_ids[]=$HYPHABASE_LOCATION_ID"202 Accepted and a receipt:
{
"data": {
"id": "019fb0d4-8a21-70bb-9e12-4c8ab3e91f70",
"type": "import",
"status": "pending",
"total_rows": 312,
"original_filename": "customers.csv",
"file_size": 38104
}
}202 means "we've taken it", not "it's done". Nothing exists yet.
A valid file doesn't mean valid rows
The CSV structure is checked before the job is created, so a malformed file is a 422
right here with the reason in message. A 202 therefore tells you the shape is right and
total_rows is a real count.
Individual rows can still fail while it runs — most often a duplicate email, since email is unique per site.
Get data back out
POST /api/modules/clients/import-export/exportJSON, and the filters mirror the normal list:
| Field | Notes |
|---|---|
search | Part of a name, email or phone |
date_from, date_to | YYYY-MM-DD. date_to can't be before date_from. |
client_status_id | Only people with that status |
location_id | Restrict to one site |
curl -X POST https://api.hyphabase.com/api/modules/clients/import-export/export \
-u sk_live_xxx: -H "X-Location-Id: $HYPHABASE_LOCATION_ID" \
-H "Content-Type: application/json" \
-d '{"date_from":"2026-01-01","client_status_id":"'"$STATUS_ID"'"}'Send {} for everything. total_rows on the receipt is how many matched, so you know the
size before you wait for it.
Exporting is the right tool for taking a whole copy. Don't page the normal list three hundred times to build one — see Long lists.
Check back
GET /api/modules/clients/import-export/operations/{id}status moves pending → processing → completed or failed.
Poll it so you can show a progress bar — that's the reason to do this from the browser rather than just waiting.
async function waitForJob(jobId, { intervalMs = 2000, timeoutMs = 300_000 } = {}) {
const deadline = Date.now() + timeoutMs;
for (;;) {
const response = await fetch(
`${BASE}/api/modules/clients/import-export/operations/${jobId}`,
{ credentials: 'include', headers: { 'X-Location-Id': locationId } },
);
const { data } = await response.json();
onProgress(data.processed_rows ?? 0, data.total_rows);
if (data.status === 'completed') return data;
if (data.status === 'failed') throw new Error(data.error_message ?? 'Import failed');
if (Date.now() > deadline) throw new Error('Timed out waiting for the import');
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}
}See everything with GET /api/modules/clients/import-export/operations, which is also how you find a job whose id you lost.
Download the export
GET /api/modules/clients/import-export/operations/{id}/downloadOnce it says completed:
curl https://api.hyphabase.com/api/modules/clients/import-export/operations/$JOB_ID/download \
-u sk_live_xxx: -H "X-Location-Id: $HYPHABASE_LOCATION_ID" \
-o customers-export.csvStopping and tidying up
Cancel something still running with
POST /api/modules/clients/import-export/operations/{id}/cancel.
Only pending and processing jobs can be cancelled.
Cancelling an import doesn't undo it
Rows already created stay created. Cancel means stop, not roll back. If you've imported half a bad file, you'll be deleting those people — which is recoverable, see Deleting and undeleting.
Receipts pile up. Clear finished ones with
DELETE /api/modules/clients/import-export/operations/clear,
which removes completed and failed and leaves anything in flight alone.
Sarah's actual first import
Get the template
Build the CSV from its headers. She discovers phone is required and fills the gaps with
unknown for the eleven customers she only has an email for.
Upload, and keep the receipt
A 202 means the file parsed. She stores data.id.
Poll until it settles
Every couple of seconds until completed.
Reconcile
Compare the resulting customer count against total_rows. Three didn't make it — duplicate
emails, two of them the same couple entered twice in her old system. She fixes those by
hand.
That last step is not optional. Rows fail individually, and nothing tells you unless you look.