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
- Create the import: upload, paste or Google Sheets. You get the columns, a sample and a
suggested_mapping. - Process it with your column mapping. This runs in the background; poll
GET /v1/jobs/{job_id}orGET /v1/imports/{id}untilstatusisready. - 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
.xlsxand.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
}| Field | Description |
|---|---|
mapping.phone | Column with the phone numbers (required). |
mapping.name | Optional 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_country | ISO country for local numbers like 0772.... |
dedupe | Default true: keep the first row of each number. |
skip_opted_out | Default 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.csvEach 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
| Method | Path | Description |
|---|---|---|
GET | /v1/imports/{id} | Status, counts, mapping, and the processing job. |
GET | /v1/imports/{id}/recipients | Valid 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)
}