Contacts API

API

Contacts API

Create and update contacts from any system — a backend, a data warehouse, a partner integration — through one generic endpoint, single or batch. Field names are matched intelligently, unknown fields become custom properties, and consent is protected by design: syncing data can never resubscribe someone who opted out.

Which endpoint do I want?

  • POST /api/v1/contacts (this guide) — syncing contact data: bulk upserts, enrichment, migrations. Never triggers automations.
  • POST /api/leads/intake — a signup event: one person just opted in, welcome automations should fire.

1. Create an API key

  1. Go to Store settings → API keys and create a key. The narrow Contact intake only scope is enough for this endpoint; the optional crm block below needs a Full access key.
  2. Authenticate every call with Authorization: Bearer sc_live_… (or X-API-Key).

2. One contact

Shell
curl -X POST https://www.sendcore.me/api/v1/contacts \
  -H "Authorization: Bearer sc_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "anna@example.com",
    "first_name": "Anna",
    "phone": "+39 333 1234567",
    "birthday": "1990-07-15",
    "tags": ["VIP", "Fiera Milano"],
    "consent": "subscribed",
    "consent_source": "landing_signup",
    "piano_scelto": "premium"
  }'

Response — one row per contact, always telling the whole truth:

JSON
{
  "ok": true,
  "contact": {
    "email": "anna@example.com",
    "created": true,              // created vs updated
    "consent_state": "SUBSCRIBED",
    "consent_preserved": false,   // true = your "subscribed" did not apply
    "first_subscription": true,   // transitioned into SUBSCRIBED
    "suppressed": false           // true = on the unsubscribe list: accepted, but won't be mailed
  }
}

3. Batch

Send up to 500 contacts per request as { "contacts": [ … ] }. Rows that fail validation are reported by index in invalid without failing the rest; duplicate emails within one batch collapse to the last occurrence.

JSON
{
  "ok": true,
  "results": [ { "email": "…", "created": true, … } ],
  "invalid": [ { "index": 3, "error": "invalid_email" } ],
  "stats": { "received": 120, "created": 80, "updated": 39, "invalid": 1, "suppressed": 2 }
}

Field names

Keys go through the same matching engine as the CSV importer: email_address, firstName, phone_number, Date of birth, a single full_name (split into first/last) — including Italian names like telefono or cognome — all land on the right field. city, country and state are stored as canonical custom properties. Any other scalar key is kept as a custom property (at most 15 per contact); non-scalar values are refused and listed in ignored_keys rather than guessed at. Explicit custom_properties win over auto-detected keys.

Consent, precisely

  • Omit consent when syncing data. The presence of an email in your system is not marketing consent. A row without consent never changes an existing contact's state; a new contact lands as UNKNOWN.
  • "consent": "subscribed" is an assertion that this person actually opted in (form, checkbox, verbal consent you recorded). Send it only then — it updates existing contacts too.
  • "consent": "unsubscribed" always applies: downgrading is the safe direction. Accepted dialects: yes/no, true/false, subscribed/unsubscribed/non-subscribed, opted in/opted out…
  • People who clicked unsubscribe are on the suppression list, and this API never removes them from it, whatever you send. Their rows come back with suppressed: true: data accepted, no marketing email will go out.

CRM handoff (optional)

Add a crm block to any row to also create or update a CRM card, idempotently: same (source, external_id) → same card, forever (external_id defaults to the contact's email). Needs a Full access key and a plan with the CRM; if either is missing the contact is still saved and the row's crm result carries the error.

JSON
{
  "email": "anna@example.com",
  "consent": "subscribed",
  "crm": {
    "pipeline_id": "…",          // required — see /api/v1/crm/pipelines
    "stage_id": "…",             // optional: moves the card when it changes
    "external_id": "LEAD-12345", // optional: defaults to the email
    "source": "my_backend",      // optional label, default contacts_api
    "title": "Anna — premium",   // optional
    "amount": 1200               // optional
  }
}

Idempotency, limits, errors

  • Repeating a request converges to the same state — safe to retry on any failure.
  • Rate limits: 240 requests/min per IP, 300/min per account (HTTP 429, rate_limit_exceeded).
  • Errors are stable tokens with the usual statuses: missing_api_key / invalid_api_key (401), key_scope_insufficient (403), invalid_json / invalid_email / batch_too_large (400).

Next steps