Bulk import accounts + locations (CSV upload)

Overview

Bulk-imports accounts from a single CSV — one account + one location (store) per row — designed for up to ~100K rows. The endpoint validates the upload for shape/size, then stores and queues it. Processing runs asynchronously on the bulk queue (prepare, then parallel chunk jobs, then finalize). It returns an import_id immediately — poll Bulk import status for progress.

Per created row the system provisions the full cascade. This covers the account company (bundle_id=3), company_plans, company_options, owner user + person, location-account company (bundle_id=2), store, store_options, store_plans, and store_review_sites (one per the plan's review-site set). The endpoint suppresses welcome/notification emails for bulk imports. It creates every owner with a temporary password, and each owner must set their own at first login.

Prerequisites

  • A bearer token in the Authorization header.

Base URL

EnvironmentURL
Productionhttps://production-api.shoutaboutus.com
Developmenthttps://development-api.shoutaboutus.com

Endpoint

POST /api/v1/partner/account/bulk-import

Authentication

  • Requires a bearer token in the Authorization: Bearer <bearer-token> header.

  • Partner — authentication. Caller must be a partner company (company bundle_id = 1). The endpoint scopes the import to that partner. Non-partner callers get 403.

Rate limit

  • No rate limit (bulk import — the bulk worker controls back-pressure).

Request body

FieldTypeRequiredDescription
filefile (.csv)RequiredCSV only, max 100 MB. Header row must contain every required column below. The endpoint rejects the xlsx (streamed row-by-row for memory safety).

CSV columns — one account+location per row

ColumnRequiredDescription
account_nameRequiredAccount name (max 100, no <>).
client_account_idRequiredPartner-unique external account id (^[A-Za-z0-9_-]+$, max 100). Duplicate (in-file or existing) → skipped duplicate_client_account_id. Also the re-run idempotency key.
plan_idRequiredNumeric plan id assigned to the partner (company_plans). Unassigned → skipped plan_unavailable.
owner_first_nameRequiredAccount owner first name.
owner_emailRequiredGlobally unique on users. Duplicate → skipped duplicate_email.
location_nameRequiredLocation/store name (max 100, no <>).
client_location_idRequiredPartner-unique external location id (same charset). Duplicate → skipped duplicate_client_location_id.
addressRequiredmax 255, no <>.
countryRequiredCountry code or name; must already exist (unknown falls back to skipped unknown_country). Countries are never auto-created.
stateRequiredState code or name; created under the country when missing.
cityRequiredCity name; created under (country, state) when missing.
postal_codeRequired3–12 chars.
websiteOptionalURL — account website.
billing_idOptionalmax 100.
owner_last_nameOptionalAccount owner last name.
owner_phoneOptional9–15 digits, no spaces/symbols.
storeidOptionalStore label; defaults to the resolved city name when blank.
business_phoneOptional9–15 digits.
site_urlOptionalURL — location website.
companyindustry_idOptionalIndustry id; blank, then defaults to Home Services. Non-existent id, then skipped invalid_row. A partner-level industry still overrides the per-row value when set.

How it works

  • 202 Accepted{ data: { import_id, status: "queued" } } — the response returns immediately with an import_id. The import itself runs in the background, so poll the status endpoint rather than treating 202 as "import finished".
  • Validation, de-dupe (in-file + against the partner's existing accounts/locations) and geo resolve/create all happen in the single-threaded prepare stage. The endpoint records invalid/duplicate rows and skipped while the rest continue (skip-and-report).
  • Safe to re-run: re-uploading the same file only creates rows whose client_account_id / client_location_id don't already exist for the partner.
  • 403 if the caller is not a partner; 422 if the file is missing, not a .csv, or larger than 100 MB.

Response

202 Queued · 202

{
  "data": {
    "import_id": 2,
    "status": "queued",
    "message": "Import received. Processing has been queued."
  }
}

403 Not a partner · 403

{
  "message": "Only a partner account may run a bulk import.",
  "status": "error",
  "errors": []
}

422 Invalid file · 422

{
  "message": "The import file must be a .csv file.",
  "errors": {
    "file": [
      "The import file must be a .csv file."
    ]
  }
}

Errors

StatusMeaning
401The bearer token is missing, expired or invalid
403Not a partner
422Invalid file
500Unexpected server error

Example request

curl --request POST \
  --url "https://production-api.shoutaboutus.com/api/v1/partner/account/bulk-import" \
  --header 'Authorization: Bearer <bearer-token>' \
  --header 'Accept: application/json' \
  --form 'file=@account_bulk_import_sample.csv'

Did this page help you?