Skip to content

Contacts, segments and address checks

A contact is a person you may email, kept once per address. Start by adding one. This needs a key with operate.

import { Colding } from "colding";

const colding = new Colding("cd_your_key");
const contact = await colding.contacts.create({
  email: "[email protected]",
});
console.log(contact.id, contact.email);

Replace the key and the address with yours. The address here is an example.

Example output: the new contact's ID, then its address
6f1d2c3a-5b4e-4f70-9a8b-1c2d3e4f5a6b [email protected]

If the address is already here, in any letter case, the call answers 409 and names the contact in details.contactId. To add or fill in without caring, use upsert.

Add or update many by address

Add or update many by address

contacts.upsert takes up to 500 contacts in one call. An address that is new is created. One that is already here is filled in, or left alone if you pass mode: "skip".

const result = await colding.contacts.upsert({
  contacts: [
    { email: "[email protected]", firstName: "Ana", company: "Example Ltd" },
    { email: "[email protected]", firstName: "Ben" },
  ],
});
console.log(result.created, result.updated, result.failed);

created, updated and failed count the rows. results has one entry per row, in the order you sent them.

  • A blank value never erases what is stored. To clear a field, use contacts.update, where a blank value replaces what was there.
  • subscribed: false records that the person asked not to be emailed. Nothing here undoes it, and subscribed: true does not either. Only taking the address off the suppression list does.
  • A bad address fails only its own row.
  • If the new contacts would pass your plan's contact limit, nothing is stored and the call answers 402.

Read contacts and their history

contacts.get reads one contact: its fields, labels and latest address verdict. A contact in another workspace answers 404. contacts.count takes the same filters as contacts.list (q, labelId, subscribed, check and segmentId) and answers how many contacts match, so run it before a bulk action. Both need read.

const subscribed = await colding.contacts.count({ subscribed: true });
console.log(subscribed.count);

const contactId = "66666666-6666-4666-8666-666666666666";
const contact = await colding.contacts.get({ contactId });
console.log(contact.email, contact.verification?.status);

const history = await colding.contacts.timeline({ contactId, limit: 20 });
for (const event of history.items) console.log(event.at, event.type);

const fields = await colding.contacts.listCustomFields();
console.log(fields.keys, fields.more);

contacts.timeline answers what happened with one contact, newest first, a page at a time. Pass nextCursor as cursor to get the next page. Every event has an id, an at and a type, and the other fields depend on the type. An email set to go later is listed at the time it will go, which can be in the future.

The types of event

added
The contact was created. source says how, and import names the import while we still keep it.
campaign_joined
The contact became a lead of a campaign.
check
One of our address checks finished. It has the status, the reason and the address that was checked.
email
A campaign or direct email to the contact: sent, waiting or stopped. One a person canceled before it went is not listed.
reply
Mail the contact sent to one of your mailboxes. Warmup mail, bounce reports and spam reports are not replies.
bounce
An email to the contact came back for good.
complaint
The contact marked an email as spam.
suppressed
The address, or its whole domain, went on the suppression list.
unsuppressed
The address, or its domain, came off the suppression list.
campaign_left
A person or a key took the lead out of the campaign. by says who.
opened
The contact opened an email for the first time. One event per email, and opens counts the opens.
clicked
The contact clicked a link in an email. One event per link, at its first click. url is the page the link leads to.
outside_email
Mail a person on your team wrote to the contact in their own mail app. It has the subject, the mailbox and the thread, never the body.
import_updated
An import changed a contact that was already here. import names the import while we still keep it. A contact the import left as it was has no event.

An email we sent is listed once, as email. An open or a click by a mail scanner or a mail proxy is never in the history, and neither is warmup mail.

contacts.listCustomFields answers keys, the names of the custom fields your contacts carry, A to Z with capitals ignored, up to 200. more is true when there are others. A name is what a merge field reads in a template. The standard fields (first_name, last_name, company, phone and email) are not listed.

File contacts under labels

A label is a name you file contacts, and inbox threads, under. Names are unique, capitals ignored. contacts.label adds and removes labels for a selection in one call.

const label = await colding.labels.create({ name: "Met at the fair" });
const touched = await colding.contacts.label({
  selection: {
    ids: ["77777777-7777-4777-8777-777777777777", "88888888-8888-4888-8888-888888888888"],
  },
  add: [label.id],
});
console.log(touched.count);

The call answers how many contacts it touched.

A selection is a list of up to 1,000 contact IDs, or every contact that matches a filter. For the second, pass all: true, the filters and expectedCount, the number you checked. If a different number match now, nothing changes and the call answers 409.

The filters are q (words in the address, name or company), labelId, subscribed, check and segmentId. Deleting, exporting and checking addresses take the same selection.

labels.update renames a label and labels.remove deletes one. Both need operate. Contacts and threads keep a renamed label. Deleting a label removes only the label: the contacts and threads that carried it stay.

const renamed = await colding.labels.update({
  labelId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
  name: "Met at the fair 2030",
});
console.log(renamed.name);

await colding.labels.remove({ labelId: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" });

Delete contacts

contacts.remove deletes one contact. contacts.removeMany deletes a selection. Both need operate and cannot be undone.

await colding.contacts.remove({ contactId: "66666666-6666-4666-8666-666666666666" });

const filters = { check: "invalid" } as const;
const matching = await colding.contacts.count(filters);
const removed = await colding.contacts.removeMany({
  selection: { all: true, filters, expectedCount: matching.count },
});
console.log(removed.count);

A selection works as it does for labels: up to 1,000 IDs, or all: true with filters, exclude (IDs to leave out) and expectedCount. Run contacts.count first and pass its number, less the IDs in exclude, as expectedCount. If a different number matches now, nothing is deleted and the call answers 409. A selection of more than 50,000 contacts is refused. The call answers how many contacts it deleted.

A deleted contact leaves its campaigns, and its address checks go with it. The suppression list is not touched: an address that opted out stays on it. Its labels stay for your other contacts. A batch of checks with nothing left to do finishes.

Import a CSV or Excel file

In the console, use Import on the Contacts page when a person has the file. It has four steps: File, Columns, Review and Done. Use the API when a program has the data. For up to 500 contacts at a time, contacts.upsert is simpler.

To send a whole file, start an import and commit it. createImport changes nothing yet. It answers the columns it found, the settings it suggests and preview, a dry run of what a commit would do. Change a setting with contacts.updateImport, which raises revision. commitImport names the revision you read.

const started = await colding.contacts.createImport({
  fileName: "leads.csv",
  csv: "email,first name\[email protected],Ana\[email protected],Ben\n",
});
console.log(started.preview?.blockers);

const done = await colding.contacts.commitImport({
  importId: started.id,
  revision: started.revision,
});
console.log(done.result);

A commit writes every row or none. The same revision again answers the same result, so a retry is safe. A cell that marks someone Do not email puts their address on the suppression list. A Labels column gives each contact the labels in its cell, one per line, as the export writes them. If you have a label by that name, capitals ignored, the contact gets it; if not, the commit creates it. preview.newLabels says which ones it would create.

A file can have up to 50,000 rows and be up to 10 MB. A CSV's cells are separated by commas, semicolons or tabs, and you send it as csv text or as content in base64. An Excel file (.xlsx, .xlsm or .xls) goes as content only. We read its first visible sheet, as the text its cells show: a formula gives its last saved result, and a date in Excel's short format comes as year-month-day. A file locked with a password, one with more than 10 MB of text in its cells, or one in another format (Numbers, OpenDocument or Excel Binary) answers 400 and says what to do: save the list as .xlsx or CSV. An import you do not commit is deleted, file and all, after 24 hours. A committed import's summary stays 7 days. The contacts stay for good.

Rows that fail are listed in preview, the first 1,000 of them. To get all of them, contacts.exportImportFailures({ importId, revision }) answers a link to a CSV of the rows that would fail at that revision: the file's own header row, each cell as written, and a last column with the reason. Fix the rows and import that file. The link works for a few minutes and the file is written when you open it. Once an import is committed its cells are gone, so its failed rows can't come back: upload the file again.

To find an import again, contacts.listImports lists the workspace's imports, newest first, a page at a time. Each comes with its state and counts, never the cells of the file. Open one with contacts.getImport.

Save a filter as a segment

A segment is a saved filter with a name. It keeps no list of people: whoever matches its conditions when you use it is in it. A contact you add tomorrow can join it without you doing anything.

const tried = await colding.segments.count({
  conditions: [{ field: "domain", op: "equals", value: "example.com" }],
});
console.log(tried.count);

const segment = await colding.segments.create({
  name: "People at example.com",
  conditions: [{ field: "domain", op: "equals", value: "example.com" }],
});
const page = await colding.contacts.list({ segmentId: segment.id });
console.log(page.items.map((contact) => contact.email));

segments.count tries conditions without saving them. segments.create saves them. A workspace keeps up to 100 segments.

Pass the segment's ID as segmentId to list, count, label, delete, export or check the contacts that match it. If the segment was deleted since, the call answers 404.

A condition has a field, an op (the test) and, for tests that compare, a value or values. The fields include search, email, domain, first_name, last_name, company, subscribed, suppressed, check, label, created_at and updated_at. A custom field is named by key. match is all (every condition) or any (one is enough). A group, one level deep, holds conditions of its own, and each one counts toward the limit. A segment holds up to 20 conditions. campaign_sent, campaign_replied, campaign_opened and campaign_clicked test what happened to a campaign's email. campaign_opened and campaign_clicked count a person's open or click, never a scanner's or a mail proxy's. They need open or click tracking on a verified tracking domain. With none in use, in and any match nobody, and not_in and none match everyone, including people the campaign never emailed. To find people who got an email and did nothing with it, add campaign_sent too.

The tests each field takes are in the OpenAPI file. A condition that does not fit its field is refused with 400 and the path of that condition. Open the OpenAPI JSON

segments.update takes the revision you read. If someone changed the segment since, nothing is written and the call answers 409 with the current revision.

segments.list answers every segment, A to Z, each with its conditions. It is one page, because a workspace keeps up to 100. segments.remove deletes one and needs operate. Its contacts stay as they are, and a call that still names it answers 404. A segment a campaign takes leads from answers 409 segmentLinked, with the campaigns in details: unlink it from each one first, with campaigns.unlinkSegment.

const segments = await colding.segments.list();
const old = segments.find((segment) => segment.name === "People at example.com");
if (old) await colding.segments.remove({ segmentId: old.id });

Check addresses before you send

An address check says whether an address can receive mail. We check in steps. First, our own look at the address and its domain's mail servers. Then our own check asks the address's mail server whether the mailbox exists. It skips Microsoft 365, Outlook.com, Yahoo, AOL and iCloud because their servers don't say. Those addresses, and what we can't settle ourselves, go to a paid checking service. Starting a check needs the validate permission, because the service charges for each address it answers.

const check = await colding.emailChecks.check({
  address: "[email protected]",
});
console.log(check.state, check.status, check.reason);

check waits a little for the answer. If state is still queued, running or dispatched, read it again with emailChecks.get. When state is done, status is the verdict and reason says why.

What a verdict means

valid
The address can receive mail.
risky
We could not confirm it, but it may still work.
invalid
It cannot receive mail.
unknown
We could not tell. This is not invalid.

A check that cannot finish waits instead of guessing. If the checking service is down or the month is used up, state is on_hold and holdReason says why. An outage never becomes a verdict.

What a check spends

Each answer counts once against your plan's checks for the month, whether our own check or the paid service gave it. A plan with no monthly limit never runs out. When none are left, the call answers 429 with details.resetAt and details.upgradeTo. If the last payment failed, it answers 402 and queues nothing.

A verdict stays good 30 days, and an address with a good verdict is not checked again. recheck: true asks for a new check anyway, but not within 24 hours of the checking service's last answer: that answer comes back as it is, with no new check.

Read the setup before a big batch. month has what the workspace used and its limit, which is null when the plan has none.

const setup = await colding.emailChecks.setup();
console.log(setup.state, setup.month);

When your key holds validate and checks are on, a contact that a write creates is checked too: contacts.create, a new address in contacts.upsert or a new row in an import. A contact already here keeps its verdict and gets no new check. Pass checkAddresses: false to skip it. A month that is used up never refuses the write: the contact is saved, and its check waits on hold.

Checks need the owner to accept the acceptable use policy first. Until then, emailChecks.check and emailChecks.start answer 403 AUP_REQUIRED, and a write saves the contact but starts no check (notStartedBecause: aup_required). An agent asks the owner with an accept_aup handoff.

To check many addresses, start a batch with a selection, up to 50,000 contacts a batch. Pass an intentKey so a retry answers the same batch. Follow it with emailChecks.getRun, or wait for the email_check.run_completed event. The email_check.run_held event says when the batch waits on hold.

const run = await colding.emailChecks.start({
  selection: {
    all: true,
    filters: { segmentId: "99999999-9999-4999-8999-999999999999" },
  },
  intentKey: "check-example-com-2030-01-15",
});
const now = await colding.emailChecks.getRun({ runId: run.id });
console.log(now.state, now.counts);

Find, stop and retry batches

emailChecks.list lists checks, newest first, a page at a time. Narrow it with address (capitals ignored), contactId, runId (one batch), state or status, the verdict. emailChecks.listRuns lists batches the same way, and each one has counts: how many of its checks are where. We keep a check 30 days after it ends and a batch 30 days after it finishes. The contact keeps its latest verdict. Both need read.

const checks = await colding.emailChecks.list({ address: "[email protected]" });
for (const check of checks.items) console.log(check.state, check.status);

const runs = await colding.emailChecks.listRuns({ limit: 10 });
for (const run of runs.items) console.log(run.id, run.state, run.counts);

emailChecks.cancelRun stops a batch. Its checks that have not gone to the checking service stop and cost nothing, unless another batch wants them too or emailChecks.check asked for them. A check already sent to the service finishes, because it may already count. A batch that is already canceled or finished comes back as it is.

emailChecks.retryRun sends a batch's unanswered checks to the service again: the uncertain ones, and the ones on hold after their retries ran out (noAnswer in counts). Each may count against your month again, so the rules of a new check apply: 429 when the month is used up, 402 after a failed payment, and 403 AUP_REQUIRED until the owner accepts the acceptable use policy. A canceled batch answers 409: start a new batch instead. Both calls need validate.

const stopped = await colding.emailChecks.cancelRun({
  runId: "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
});
console.log(stopped.state, stopped.counts.cancelled);

const again = await colding.emailChecks.retryRun({
  runId: "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
});
console.log(again.state, again.counts.uncertain, again.counts.noAnswer);

A verdict never lifts a suppression

A check is evidence, not permission. A valid verdict does not put an address back on the list of people you may email. The suppression list is read first, and only taking the address off that list changes it. An address found invalid is never sent to. A new message to a risky or unknown address needs recipientReviewed: true. Each row stops one stream, cold (campaigns and messages) or transactional (your app's email). suppressions.list and suppressions.add take stream. A bounce stops both streams. The console lets you remove both rows at once. If either row being removed records the person's own opt-out, pass recipientAsked: true only if they asked to receive email again. Without it, nothing is removed.

A new message needs a fresh verdict before it goes out. The send guide shows where that fits. Send an email and follow it

Export contacts as a file

contacts.export takes a selection and answers a link, not the file. Download the file with a plain GET and no key. The link is the permission, so share it only with whoever should see these contacts.

const file = await colding.contacts.export({
  selection: {
    ids: ["77777777-7777-4777-8777-777777777777", "88888888-8888-4888-8888-888888888888"],
  },
});
console.log(file.url, file.expiresAt);

The link works for 10 minutes and opens at most 3 times. Every call makes a new link, so call again after expiresAt. A file holds up to 100,000 contacts.

The file has each contact's fields, custom fields, labels (one per line in their cell), Do not email and the latest address check. Its column names come in the language you pass as locale, the workspace's by default, with a comma between cells in English and a semicolon in Portuguese and Spanish, as Excel expects. It holds the contacts that match when it is downloaded, so count is the number at the moment you made the link.

A cell a spreadsheet could run as a formula starts with a '.