HyphaBase

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/template
curl https://api.hyphabase.com/api/modules/clients/import-export/template \
  -u sk_live_xxx: -H "X-Location-Id: $HYPHABASE_LOCATION_ID" \
  -o customers-template.csv

Comes 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/import

Sent as multipart/form-data:

FieldNotes
fileCSV, 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/export

JSON, and the filters mirror the normal list:

FieldNotes
searchPart of a name, email or phone
date_from, date_toYYYY-MM-DD. date_to can't be before date_from.
client_status_idOnly people with that status
location_idRestrict 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}/download

Once 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.csv

Stopping 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.

Next

On this page