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
- Go to Store settings → API keys and create a key. The narrow Contact intake only scope is enough for this endpoint; the optional
crmblock below needs a Full access key. - Authenticate every call with
Authorization: Bearer sc_live_…(orX-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:
{
"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.
{
"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
consentnever changes an existing contact's state; a new contact lands asUNKNOWN. "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.
{
"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).