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

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.

curl -X POST https://www.sendcore.me/api/v1/contacts \
  -H "Authorization: Bearer sc_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"contacts": [
    {"email": "anna@example.com",  "first_name": "Anna"},
    {"email": "marco@example.com", "first_name": "Marco", "consent": "subscribed"}
  ]}'

Response:

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

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.
  • Mirroring another system? A full export or a nightly sync carries the consent you recorded back then, not what the person chose since. Add "consent_policy": "preserve" at the top level of the request (next to contacts, or beside the fields of a single contact): subscribed then applies to new contacts only, an existing contact keeps the choice made in Sendcore, and every other field still updates. Rows where that protection applied come back with consent_preserved: true. The default, "assert", is the behaviour above.
  • "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 and are never marked SUBSCRIBED: data accepted, no marketing email will go out.

A nightly mirror of your own database, then, looks like this — new people land subscribed, everyone else keeps their choice, and every address, phone and property still updates:

JSON
{
  "consent_policy": "preserve",
  "contacts": [
    { "email": "anna@example.com",  "first_name": "Anna",  "city": "Firenze", "consent": "subscribed" },
    { "email": "marco@example.com", "first_name": "Marco", "city": "Prato",   "consent": "subscribed" }
  ]
}

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.
  • On 429 or a 5xx, retry with exponential backoff (2s, 4s, 8s), as the PHP example does. Other 4xx are validation refusals: fix the payload or the key instead of retrying.
  • 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