eSMS AfricaeSMS Africa
API ReferenceSMS API

Imports

Upload CSV or Excel files, paste numbers or link a Google Sheet; map columns; validate, normalise to E.164, dedupe and drop opted-out numbers in the background.

An import turns a file into a clean recipient list you can use as a campaign audience ({"type": "import", "id": "imp_..."}) or save into a contact group. Keys need the send scope to create and process imports, query to read them.

Flow

  1. Create the import: upload, paste or Google Sheets. You get the columns, a sample and a suggested_mapping.
  2. Process it with your column mapping. This runs in the background; poll GET /v1/jobs/{job_id} or GET /v1/imports/{id} until status is ready.
  3. Use it in a campaign, download the error file, or save it to a group.

Imports are kept for 3 days unless a launched campaign uses them.

Create

POST https://sms.esmsafrica.io/v1/imports            multipart: file, default_country?
POST https://sms.esmsafrica.io/v1/imports/paste      {"text": "...", "default_country": "UG"}
POST https://sms.esmsafrica.io/v1/imports/gsheet     {"url": "https://docs.google.com/spreadsheets/d/...", "default_country": "UG"}
  • Files: CSV / TXT / TSV (delimiter and encoding are detected), Excel .xlsx and .xls (first sheet). Max 50 MB and 500,000 rows. Upload limit: 30 a minute.
  • Paste: numbers one per line or separated by commas or spaces, or tab-separated rows with a header row.
  • Google Sheets: share the sheet as "Anyone with the link can view". Only Google hosts are contacted. 10 a minute.

201 response:

{
  "id": "imp_3f9c1a2b4d5e6f708192",
  "source": "xlsx",
  "filename": "debtors.xlsx",
  "status": "uploaded",
  "columns": ["Full Name", "Mobile", "Amount Due", "Branch"],
  "sample": [["Ada Obi", "0772100001", "1500", "Kampala"]],
  "has_header": true,
  "suggested_mapping": {
    "phone": "Mobile",
    "name": "Full Name",
    "fields": { "Amount Due": "amount_due", "Branch": "branch" }
  },
  "row_estimate": 12850,
  "counts": { "total_rows": 0, "valid": 0, "invalid": 0, "duplicates": 0, "opted_out": 0, "unavailable": 0 }
}

Process

POST https://sms.esmsafrica.io/v1/imports/{id}/process
{
  "mapping": { "phone": "Mobile", "name": "Full Name", "fields": { "Amount Due": "amount", "Branch": "branch" } },
  "default_country": "UG",
  "dedupe": true,
  "skip_opted_out": true
}
FieldDescription
mapping.phoneColumn with the phone numbers (required).
mapping.nameOptional name column (gives {name}, {first_name}, {last_name}).
mapping.fields{column: merge_key} - columns to keep as merge fields. Omit to use the suggestion.
default_countryISO country for local numbers like 0772....
dedupeDefault true: keep the first row of each number.
skip_opted_outDefault true: drop numbers on your opt-out list.

Returns {"import": {...}, "job_id": 91}. When status is ready:

{
  "status": "ready",
  "counts": { "total_rows": 12850, "valid": 12702, "invalid": 96, "duplicates": 40, "opted_out": 12, "unavailable": 3 },
  "countries": { "UG": 12650, "KE": 49, "NG": 3 },
  "reasons": { "invalid_number": 61, "missing_country_code": 20, "scientific_notation": 15, "duplicate": 40, "opted_out": 12, "unavailable_country": 3 },
  "fields": ["amount", "branch"],
  "field_stats": { "amount": { "max": 7, "unicode": false, "filled": 12702 } }
}

unavailable numbers are kept but are skipped (and never charged) when a campaign sends. A file with no valid numbers ends failed with an error.

Rows that were not imported

GET https://sms.esmsafrica.io/v1/imports/{id}/errors?reason=&page=0&limit=50
GET https://sms.esmsafrica.io/v1/imports/{id}/errors.csv

Each row: row (line in your file), value, reason, message. Reasons: empty, not_a_number, scientific_notation, too_short, too_long, missing_country_code, invalid_number, landline, duplicate (the message names the original row), opted_out, limit. The CSV has the columns row, value, reason, explanation. Up to 50,000 error rows are stored per import.

Other endpoints

MethodPathDescription
GET/v1/imports/{id}Status, counts, mapping, and the processing job.
GET/v1/imports/{id}/recipientsValid recipients (row, phone, country, name, vars), paginated.
POST/v1/imports/{id}/save-to-group{group_id} or {name, tags?} (creates a group). Returns {group_id, job_id}; numbers already in the group are skipped.
GET/v1/jobs/{job_id}{id, kind, status, progress: {done, total}, error, result}.

Example

curl -X POST https://sms.esmsafrica.io/v1/imports \
  -H "Authorization: Bearer $ESMS_API_KEY" \
  -F "[email protected]" -F "default_country=UG"

curl -X POST https://sms.esmsafrica.io/v1/imports/imp_3f9c1a2b4d5e6f708192/process \
  -H "Authorization: Bearer $ESMS_API_KEY" -H "Content-Type: application/json" \
  -d '{"mapping":{"phone":"Mobile","name":"Full Name","fields":{"Amount Due":"amount"}}}'

curl https://sms.esmsafrica.io/v1/jobs/91 -H "Authorization: Bearer $ESMS_API_KEY"
// SDK 1.2.0 or later
import { readFile } from "node:fs/promises";
const imp = await esms.imports.upload(await readFile("debtors.xlsx"), { filename: "debtors.xlsx", defaultCountry: "UG" });
const { job_id } = await esms.imports.process(imp.id, {
  mapping: { phone: "Mobile", name: "Full Name", fields: { "Amount Due": "amount" } },
});
let job;
do { await new Promise((r) => setTimeout(r, 1000)); job = await esms.jobs.get(job_id); }
while (job.status === "queued" || job.status === "running");
const done = await esms.imports.get(imp.id);
console.log(done.counts);
# SDK 1.2.0 or later
import time
imp = esms.imports.upload("debtors.xlsx", default_country="UG")
job = esms.imports.process(imp["id"], mapping={"phone": "Mobile", "name": "Full Name", "fields": {"Amount Due": "amount"}})
while esms.jobs.get(job["job_id"])["status"] in ("queued", "running"):
    time.sleep(1)
print(esms.imports.get(imp["id"])["counts"])
// SDK 1.2.0 or later
$imp = $esms->imports->upload('debtors.xlsx', null, 'UG');
$job = $esms->imports->process($imp['id'], ['phone' => 'Mobile', 'name' => 'Full Name', 'fields' => ['Amount Due' => 'amount']]);
do { sleep(1); $j = $esms->jobs->get($job['job_id']); } while (in_array($j['status'], ['queued', 'running'], true));
print_r($esms->imports->get($imp['id'])['counts']);
// SDK 1.2.0 or later
data, _ := os.ReadFile("debtors.xlsx")
imp, _ := client.Imports.Upload(ctx, "debtors.xlsx", data, "UG")
job, _ := client.Imports.Process(ctx, imp["id"].(string), esms.ProcessParams{
	Mapping: esms.ImportMapping{Phone: "Mobile", Name: "Full Name", Fields: map[string]string{"Amount Due": "amount"}},
})
jobID := int(job["job_id"].(float64))
for {
	j, _ := client.Jobs.Get(ctx, jobID)
	if s := j["status"]; s != "queued" && s != "running" {
		break
	}
	time.Sleep(time.Second)
}

On this page