Developers

Contacts API: Add and Sync Contacts to Your Email List

By Lalit Kumar Jangid15 min

On this page+

The Cresca contacts API is one endpoint: POST /api/v1/contacts. It adds up to 1,000 people per request to a named contact list, creates the list if it does not exist, and updates existing contacts instead of duplicating them. Every address is validated and checked against your suppression list, so the people it adds are ready for your marketing campaigns.

This guide covers exactly what this add contacts API accepts and returns, how it handles duplicates and rejected addresses, its limits and errors, and three recipes to sync contacts to email marketing from your product.

In this guide

  • What the contacts API does, and what it does not do
  • Authentication and the request fields
  • Adding contacts with cURL, Node.js and Python
  • Duplicates and updates (upsert behaviour)
  • Validation, suppression and rejected contacts
  • Limits and errors
  • Recipes: add a user on signup, sync a signup form, group users by plan
  • Using the contacts in campaigns, and staying on the right side of consent

What the contacts API does (and does not do)

The Emails and Templates APIs are covered in the send email API guide and the email templates API guide, with an overview of the whole API.

Think of it as an email list API: it gets people from your app, CRM or website into Cresca lists you can send campaigns to. Today that is one operation:

MethodPathWhat it does
POST/api/v1/contactsAdd 1 to 1,000 contacts to a list, creating the list if needed. Existing contacts are updated, never duplicated.

There is no endpoint to list, read, delete or unsubscribe contacts, and the request has no field for tags or custom fields. A contact can carry an email address, a first name and a last name, and it joins the list you name. Tags, custom fields, deleting contacts and segment previews live in the Cresca app, on the Contacts and Lists pages.

Authentication

All requests go to https://api.cresca.xyz/api/v1 with your API key as a bearer token. Create keys on the API keys page of the app. Keys start with cr_live_ and are shown once.

Authorization: Bearer cr_live_xxxxxxxxx

The X-API-Key header also works. Adding contacts needs a full-access key. A sending-access key, the kind you give a production server that only sends email, gets 401 restricted_api_key on this endpoint. Keep the key on your server: never put it in browser code, a mobile app or a public repository. See Authentication for key permissions.

Add contacts: POST /contacts

Send a JSON body with a list name and an array of contacts. Each entry is either a plain email string or an object. The full reference is at Add contacts.

FieldTypeRequiredNotes
list_namestringYesName of the contact list. Matched by exact name; created if it does not exist. Trimmed, up to 200 characters.
contactsarrayYes1 to 1,000 entries. Each is an email string or an object.
contacts[].emailstringYes (in objects)The address. Trimmed and lowercased before matching.
contacts[].first_namestringNoTrimmed, up to 100 characters.
contacts[].last_namestringNoTrimmed, up to 100 characters.

cURL

curl -X POST https://api.cresca.xyz/api/v1/contacts \ -H "Authorization: Bearer cr_live_xxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "list_name": "Newsletter signups", "contacts": [ { "email": "ana@example.com", "first_name": "Ana", "last_name": "Silva" }, "ben@example.com" ] }'

Node.js (fetch, Node 18+)

const CRESCA_API = 'https://api.cresca.xyz/api/v1'; ,[object Object],

const result = await addContacts('Newsletter signups', [ { email: ',[object Object],', first_name: 'Ana', last_name: 'Silva' }, ',[object Object],', ]); console.log(result.summary);

Python (requests)

import os import requests,[object Object], ,[object Object], ,[object Object],

result = add_contacts("Newsletter signups", [ {"email": ",[object Object],", "first_name": "Ana", "last_name": "Silva"}, ",[object Object],", ]) print(result["summary"])

Each address gets a DNS lookup during validation, so give large batches a generous client timeout. When you test, use real addresses you control: placeholder domains can fail the MX check and come back as rejected.

Response

A processed import returns 200 with an import summary, even if some contacts were rejected:

{ "object": "contact_import", "list": { "id": "6713b2a8c2a94e0012ab3511", "name": "Newsletter signups", "created": false }, "summary": { "received": 2, "created": 1, "updated": 0, "already_present": 1, "rejected": 0, "added_to_list": 1, "limit_reached": false }, "errors": [] }
FieldMeaning
list.createdtrue if this request created the list.
summary.receivedEntries in your contacts array.
summary.createdNew contacts created in your account (each also joins the list).
summary.updatedExisting contacts whose name changed or who were added to this list.
summary.already_presentExisting contacts already on the list with nothing to change.
summary.rejectedEntries that were not added. Details are in errors.
summary.added_to_listContacts newly added to this list (new plus existing).
summary.limit_reachedtrue if your plan's contact limit stopped some new contacts.
errorsUp to 100 rejected entries, each with index, email (when present) and error.

Duplicates and updates: how the upsert works

Contacts are unique per email address in your account. The API looks up each address after normalising it, so you can send the same person again without creating a second record:

  • Addresses are trimmed and lowercased, and a few common domain typos are corrected (for example gmial.com becomes gmail.com). Ana@Example.com and ana@example.com are the same contact.
  • A new address creates a contact and puts it on the list. It counts in created.
  • An existing contact who is not on the list is added to it. They stay on every other list they already belong to.
  • If you send a non-empty first_name or last_name that differs from the stored one, it replaces it. An empty or missing name never clears the stored value.
  • An existing contact already on the list with the same names counts as already_present.
  • The same address twice in one request: the first entry is processed, the second is rejected with Duplicate email in this request.

This makes the request safe to repeat. The endpoint does not take an Idempotency-Key, and it does not need one: sending the same batch twice leaves your contacts exactly as one send would, with the second response showing already_present instead of created.

Lists are matched by exact name, so pick one naming scheme and stick to it. Newsletter and newsletter would become two lists.

Validation and suppression

Contacts are for marketing campaigns, so they are checked more strictly than recipients of API email. A bad entry never fails the whole request. It is skipped, counted in summary.rejected and listed in errors with its position in your array; the rest are still added. These are the reasons you can see:

errorWhy
email is requiredThe entry is not a string or object, or has no email.
Invalid email syntaxThe address is not a valid email.
Email address too long / Local part exceeds 64 charactersOver the 254 or 64 character limits.
Role-based email not acceptedNo-reply and system addresses such as noreply@, no-reply@, donotreply@, mailer-daemon@ and postmaster@.
Disposable/temporary email domainA throwaway inbox provider.
Domain has no MX recordsThe domain cannot receive email.
Duplicate email in this requestThe address appeared earlier in the same array.
Email is suppressedThe address is on your suppression list or the global bounce list.
Contact limit reached for this planAdding a new contact would exceed your plan's contact limit.

The suppression check matters. For campaigns, a suppressed address includes hard bounces, repeated soft bounces, spam complaints, addresses you suppressed manually and people who unsubscribed from your marketing. So a nightly sync cannot quietly re-add someone who opted out. This is stricter than the Emails API, which ignores marketing unsubscribes so password resets still arrive; Suppression explains the difference.

Here is a response with rejections. The request had a new contact, an existing contact, a no-reply address, the first address again in different case, and an object with no email:

{ "object": "contact_import", "list": { "id": "6713b2a8c2a94e0012ab3511", "name": "Product updates", "created": false }, "summary": { "received": 5, "created": 1, "updated": 1, "already_present": 0, "rejected": 3, "added_to_list": 2, "limit_reached": false }, "errors": [ { "index": 2, "email": ",[object Object],", "error": "Role-based email not accepted" }, { "index": 3, "email": ",[object Object],", "error": "Duplicate email in this request" }, { "index": 4, "error": "email is required" } ] }

Use index to map errors back to your records. Only the first 100 errors are returned, but summary.rejected always has the full count.

Limits

LimitValue
Contacts per request1,000
Import requests per account120 per 15 minutes
API requests per second (all endpoints, per account)Free 2, Professional 10, Premium 20, Ultra 50, Enterprise 100
Request body10 MB
first_name, last_name100 characters each
list_name200 characters
Contacts in your accountYour plan's contact limit (see pricing)

At full speed that is up to 120,000 contacts per 15 minutes. The per-second limit is shared by all of your account's API keys and endpoints, so a contact sync and your transactional email draw from the same budget. Existing contacts never count against your contact limit; only new ones do. Adding contacts does not use your monthly email allowance, which API email and campaigns share. Full details are in Rate limits and quotas.

Errors

Request-level errors return a JSON body with statusCode, a stable name and a message. Branch on name:

{ "statusCode": 422, "name": "missing_required_field", "message": "Missing ,[object Object], field." }
StatusnameWhenRetry?
401missing_api_keyNo API key in the request.No
401restricted_api_keyA sending-access key called this endpoint.No
403invalid_api_keyUnknown or revoked key, or suspended account.No
403plan_inactiveYour plan is not active, so contacts cannot be added.No
404not_foundWrong path or method under /contacts.No
413validation_errorBody larger than 10 MB.No
422missing_required_fieldlist_name is missing or empty.No
422validation_errorcontacts is not a non-empty array, has more than 1,000 entries, or the body is not valid JSON.No
429rate_limit_exceededOver the per-second limit, over 120 imports in 15 minutes, or too many requests with a bad key.Yes, after waiting
500application_errorSomething failed on Cresca's side.Yes, with backoff
503service_unavailableYour subscription could not be verified right now.Yes, with backoff

Per-second 429s include a retry-after header. The 15-minute import cap does not, so wait about a minute before retrying when the header is missing. Because repeating an import is harmless, retrying 429 and 5xx responses is always safe. The full list is in Errors.

Recipe 1: add a user on signup from your backend

The common case: someone creates an account and ticks "Send me product updates". Do not add them at signup. Send a confirmation email first, then add them when they click the link. That is double opt-in, and double opt-in explained covers why it is worth the extra step. Cresca has no hosted signup forms or built-in double opt-in switch, so the confirmation step lives in your app.

In Node.js with Express, using the addContacts helper from above:

import express from 'express'; import { addContacts } from './cresca.js';,[object Object], ,[object Object], ,[object Object], ,[object Object],

try { const result = await addContacts('Product updates', [ { email: user.email, first_name: user.firstName, last_name: user.lastName }, ]); if (result.summary.rejected > 0) { console.warn('Cresca rejected contact', result.errors); } } catch (err) { // Never block the user on the sync; queue it and retry later. await queueContactSync(user.id, err.message); } res.redirect('/subscribed'); });

The same flow in Python with Flask, reusing add_contacts:

from flask import Flask, request, redirect from cresca import add_contacts,[object Object], ,[object Object], ,[object Object], ,[object Object], ,[object Object], ,[object Object], ,[object Object], ,[object Object],

sync("Newsletter signups", load_confirmed_subscribers()) # your query

Because the import is an upsert, you can run this every night over everyone who confirmed, or only over people who confirmed since the last run.

Recipe 3: group users by plan

You may want to email Free users about an upgrade and Pro users about a new Pro feature. The API has no tag field, so use a list per plan. Lists are what campaigns send to, so this maps directly onto your audiences:

import { addContacts } from './cresca.js';,[object Object], ,[object Object], ,[object Object],

for (const [listName, contacts] of byPlan) { for (let i = 0; i < contacts.length; i += 1000) { const { list, summary } = await addContacts(listName, contacts.slice(i, i + 1000)); console.log(list.name, list.created ? '(new list)' : '', summary); await new Promise((resolve) => setTimeout(resolve, 8000)); } }

The API only adds people to lists; it never removes them. When someone upgrades, they join Customers - Pro but stay on Customers - Free until you change that in the app. If you need tags or custom fields such as a plan name or signup date for segment previews and personalisation, set them in the app or with a CSV import with column mapping, as described on the contact management page.

Using API contacts in your marketing campaigns

Contacts added through the API are ordinary Cresca contacts. Once they are on a list:

  1. Create a campaign in the app and choose the list (for example Product updates) as the audience.
  2. Use the segment preview on the Lists page to check a subset by tags, country or custom fields before you send.
  3. Send from a verified domain. Set one up on sending domains, and check SPF, DKIM and DMARC done right and the Gmail and Yahoo sender requirements.

Campaigns always respect unsubscribes, bounces and complaints, so the suppression rules that protect your imports also protect every send.

The API makes adding people easy, so the discipline has to come from your code. Creating an account is not consent to marketing. Account emails such as receipts and password resets are transactional and go through the Emails API (transactional vs marketing email explains the line). Marketing lists should only hold people who said yes.

  • Call the contacts API only after an explicit opt-in, ideally a confirmed one.
  • Store when and how each person agreed, in your own database.
  • Never pre-tick the marketing checkbox, and never bulk-import a purchased list.
  • Leave unsubscribes alone: Cresca refuses to re-add a suppressed address, and your sync should not try.

Frequently asked questions

Is there an endpoint to list, update or delete contacts?

No. The contacts API has one endpoint, POST /api/v1/contacts. To update a contact's name, send them again with the new first_name or last_name; the upsert replaces it. Viewing, deleting and organising contacts happens in the Cresca app.

Can I set tags or custom fields through the contacts API?

Not today. Each contact accepts an email, a first name and a last name, and joins the list you name. Use lists to group people from your backend, for example one list per plan.

Tags and custom fields can be added in the app or through a CSV import with column mapping, and segment previews can use them.

What happens if I add a contact who already exists?

Nothing is duplicated. Cresca matches the normalised address, adds the contact to the list if they are not already on it, and replaces their first or last name only when you send a different, non-empty value. The response counts them as updated or already_present, so repeating a batch is safe.

Why was a valid-looking address rejected?

Check the error string. Common reasons are a disposable domain, a domain with no MX records, a no-reply address, or suppression. Suppression covers bounces, spam complaints, manual suppressions and people who unsubscribed from your marketing, so an address that opted out cannot be re-added through the API.

Can I call the contacts API directly from my signup form?

No. The endpoint needs a full-access API key, and that key must never reach a browser or mobile app. Post the form to your own server, send a confirmation email, and call the API from the server once the person confirms.

Cresca does not offer hosted signup forms or a built-in double opt-in switch.

Do contacts added through the API count toward my plan?

New contacts count toward your plan's contact limit; contacts that already exist do not. When the limit is reached, limit_reached becomes true and the remaining new contacts are rejected, while existing ones are still updated. Adding contacts does not use your monthly email allowance. See pricing for each plan's limits.

Start syncing contacts

Create a full-access key, send one test request with an address you own, and check the list in the app. The quickstart walks through keys and your first request, and the Add contacts reference has every field.

Try Cresca free for 30 days and connect your signups to your campaigns today.

Continue learning