Bulk create or update contacts

POST /v1/contacts/import

Up to 500 per request, keyed on phone number, with the same merge rule as POST /contacts. Rows that cannot be imported come back in skipped; the rest still land. dry_run: true returns the summary (found, valid, malformed, duplicates, landlines, already opted out, will create / update, eligible) and writes nothing. A row with opt_in.status: true (or a date or source) stores opt-in evidence in the consent ledger; opt_in.status: false records the number as opted out. An import never opts anyone back in.

Send your API key as a bearer token: Authorization: Bearer ssms_sk_.... Test keys run this endpoint against the sandbox; see Authentication for key modes and scopes.

Request body

JSON (Content-Type: application/json).

FieldTypeRequiredDescription
contactsarray of ContactInputYesAt most 500 items.
dry_runbooleanNo
check_line_typesbooleanNodry_run only. Looks up line types to count landlines; live lookups are paid, so only the first 500 numbers are checked.
import_idstringNoFrom a previous response, to group several requests as one import.
namestringNoA label for the import (first request only).

Responses

StatusMeaningBody
200Import result, or the dry-run previewJSON object
400Invalid requestError
401Missing, malformed, or revoked API keyError
402Billable live calls only: the prepaid credit balance cannot cover the call (insufficient_credits); nothing was done or chargedError
429Rate limit or quota exceededError

200: response fields

FieldTypeDescription
objectcontact_import, contact_import_preview
createdinteger
updatedinteger
skippedarray of object
skipped[].indexintegerPosition of the row in the request
skipped[].valuestring
skipped[].reasonstring
import_idstringExample: imp_a1B2c3D4e5.
opted_outintegerRows recorded as opted out because they were marked unsubscribed.
summaryobjectdry_run only.
summary.foundinteger
summary.validinteger
summary.malformedinteger
summary.duplicatesinteger
summary.landlinesintegernull unless check_line_types was set Nullable.
summary.already_opted_outinteger
summary.marked_unsubscribedinteger
summary.will_createinteger
summary.will_updateinteger
summary.eligibleintegerWould receive a broadcast today
rejectedarray of objectdry_run only.
rejected[].indexinteger
rejected[].phone_numberstring
rejected[].reasonmalformed, duplicate, landline
rejected[].detailstring

Errors

StatusWhen
400Invalid request
401Missing, malformed, or revoked API key
402Billable live calls only: the prepaid credit balance cannot cover the call (insufficient_credits); nothing was done or charged
429Rate limit or quota exceeded

Every error has the same JSON shape, and request_id matches the X-Request-Id response header. Errors lists every code and what to do about it.

json
{
  "error": {
    "code": "invalid_request",
    "message": "What went wrong, in plain words.",
    "param": "the_field",
    "request_id": "req_a1B2c3D4e5F6g7H8"
  }
}

Examples

curl

bash
curl -X POST "https://api.joinsimplesms.com/v1/contacts/import" \
  -H "Authorization: Bearer $SIMPLESMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "contacts": [
    {
      "phone_number": "+14155550132"
    }
  ]
}'

Node.js

javascript
import { SimpleSMS } from 'joinsimplesms'; // npm install joinsimplesms

const sms = new SimpleSMS(process.env.SIMPLESMS_API_KEY);

const result = await sms.contacts.import([
  { phoneNumber: '+14155550132', name: 'Jane Doe', tags: ['customers'] },
  { phoneNumber: '+14155550133', name: 'Sam Lee' },
]);

Python

python
import os
from joinsimplesms import SimpleSMS  # pip install joinsimplesms

client = SimpleSMS(os.environ["SIMPLESMS_API_KEY"])

result = client.contacts.import_([
    {"phone_number": "+14155550132", "name": "Jane Doe", "tags": ["customers"]},
    {"phone_number": "+14155550133", "name": "Sam Lee"},
])