ContactRoll
Menu
Start free, no cardStart free

Developers

Connect your app to ContactRoll

Your own app can tell ContactRoll who signed up, started a trial or used a feature, and keep its people in step with your contacts here, from your server, with a key your team makes. Three requests get you started.

Quick start

Make a key

In ContactRoll, Settings › Connect your app makes an events key, and Settings › API keys makes an API key. Choose one that may write. Each key is shown once: copy it into your server’s environment, as CONTACTROLL_EVENTS_KEY and CONTACTROLL_API_KEY, never into a web page or an app a customer can open. ContactRoll keeps only a fingerprint of it, and an administrator can revoke it at any time.

Send every request from your server, with your app’s name in its User-Agent. Python’s default one is turned away before ContactRoll sees the request.

Send your first contact

Keep a person in step by your app’s own id for them. POST https://app.contactroll.com/api/v1/contacts/upsert finds the contact by your external_id, or else by their email address, and changes it with the fields you send. If nobody has either, it makes them. A new contact answers 201 and a changed one 200, and the ContactRoll-Matched header says how it was found. An Idempotency-Key makes a request that is sent twice count once. Companies work the same way at /api/v1/companies/upsert, by your id for the account, else its domain.

curl
curl -X POST https://app.contactroll.com/api/v1/contacts/upsert \
  -H "Authorization: Bearer $CONTACTROLL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "User-Agent: YourApp-server/1.0" \
  -H "Idempotency-Key: u_1842-2026-10-23" \
  -d '{"external_id":"u_1842","email":"person@example.com","name":"Ann Lee","status":"trial"}'
Node
// Node 18 or later, on your server — never in a browser
const res = await fetch('https://app.contactroll.com/api/v1/contacts/upsert', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CONTACTROLL_API_KEY}`,
    'Content-Type': 'application/json',
    'User-Agent': 'YourApp-server/1.0',
    'Idempotency-Key': 'u_1842-2026-10-23',
  },
  body: JSON.stringify({
    "external_id": "u_1842",
    "email": "person@example.com",
    "name": "Ann Lee",
    "status": "trial"
  }),
})
console.log(res.status, await res.json())
Python
# Python 3, on your server. Name your app in the User-Agent: urllib’s own is turned away.
import json, os, urllib.request

req = urllib.request.Request(
    "https://app.contactroll.com/api/v1/contacts/upsert",
    data=json.dumps({
        "external_id": "u_1842",
        "email": "person@example.com",
        "name": "Ann Lee",
        "status": "trial"
    }).encode(),
    headers={
        "Authorization": "Bearer " + os.environ["CONTACTROLL_API_KEY"],
        "Content-Type": "application/json",
        "User-Agent": "YourApp-server/1.0",
        "Idempotency-Key": "u_1842-2026-10-23",
    },
    method="POST",
)
with urllib.request.urlopen(req) as res:
    print(res.status, json.load(res))

An address that already belongs to another of your app’s contacts is refused with 409, in words, and never taken over.

Send your first event

One event per request, to https://app.contactroll.com/api/events, with an events key: the person (their address, and your id for them), what happened, and the company they belong to. The company is found by your id for it, else its domain, else made, and the person is linked to it. With a company and no person, the event is about the whole account.

curl
curl -X POST https://app.contactroll.com/api/events \
  -H "Authorization: Bearer $CONTACTROLL_EVENTS_KEY" \
  -H "Content-Type: application/json" \
  -H "User-Agent: YourApp-server/1.0" \
  -d '{"email":"person@example.com","external_id":"u_1842","event":"trial_started","company":{"external_id":"acct_77","domain":"example.com","name":"Example Ltd"}}'
Node
// Node 18 or later, on your server — never in a browser
const res = await fetch('https://app.contactroll.com/api/events', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CONTACTROLL_EVENTS_KEY}`,
    'Content-Type': 'application/json',
    'User-Agent': 'YourApp-server/1.0',
  },
  body: JSON.stringify({
    "email": "person@example.com",
    "external_id": "u_1842",
    "event": "trial_started",
    "company": {
      "external_id": "acct_77",
      "domain": "example.com",
      "name": "Example Ltd"
    }
  }),
})
console.log(res.status, await res.json())
Python
# Python 3, on your server. Name your app in the User-Agent: urllib’s own is turned away.
import json, os, urllib.request

req = urllib.request.Request(
    "https://app.contactroll.com/api/events",
    data=json.dumps({
        "email": "person@example.com",
        "external_id": "u_1842",
        "event": "trial_started",
        "company": {
            "external_id": "acct_77",
            "domain": "example.com",
            "name": "Example Ltd"
        }
    }).encode(),
    headers={
        "Authorization": "Bearer " + os.environ["CONTACTROLL_EVENTS_KEY"],
        "Content-Type": "application/json",
        "User-Agent": "YourApp-server/1.0",
    },
    method="POST",
)
with urllib.request.urlopen(req) as res:
    print(res.status, json.load(res))

An event that is recorded answers 202. Then open Settings › Connect your app: What arrived lists the last 20 things your app sent, newest first, each with what ContactRoll did. A sequence can start on an event, and a segment can count them.

The events

An event carries no free text. Every field is named, and a field nobody named is refused with its name. Only the status events change a contact’s status.

eventnameThe contact
signed_up—No change
trial_started—Trial
became_active—Active
canceled—Canceled
feature_usedRequired: the feature, such as exportNo change
customRequired: your own event, such as invited_teammateNo change
plan_changedto required, from optional: the plan’s name, no pricesSets the contact’s plan
login—Sets last seen, at most once an hour a person

Webhooks

On Plus, ContactRoll can tell your server when something changes. Add an endpoint under Settings › Webhooks (an https address of your own, up to ten an organization) and choose which of these it is sent:

contact.created, contact.updated, contact.merged, contact.unsubscribed, company.created, company.updated, company.merged, deal.created, deal.updated, case.created, case.updated, case.replied, call.ended, member.role_changed.

Each delivery is a JSON POST carrying the record’s id and the fields that changed, signed with HMAC-SHA256 in ContactRoll-Signature, using a secret that is shown once. Refuse a timestamp more than five minutes old, and take each ContactRoll-Webhook-Id once. An answer other than 2xx is tried again, seven times over about twenty hours. Each endpoint keeps its last 25 deliveries, with what your server answered and Send it again.

The API reference

Every operation of the API, with its fields, its answers and a curl example on your own organization’s address, is in ContactRoll under Settings › API reference, for every member: app.contactroll.com/settings/api once you are signed in. It is drawn from the same OpenAPI 3.1 description the code is checked against on every change. Each API key has a rate of its own, 120 requests a minute unless an administrator sets another, and the headers of each answer say what is left.

Which plan has what

What your app can doFreeStandardPlus
Events from your app, about a personIncludedIncludedIncluded
A company in an event, and events about a whole accountNot includedIncludedIncluded
Settings › Connect your app, with what arrivedNot includedIncludedIncluded
Contacts and companies kept in step by your own id (upsert)Not includedIncludedIncluded
The whole API: read and write contacts, companies, deals, cases and moreNot includedNot includedIncluded
WebhooksNot includedNot includedIncluded

On Free, events keys are made on the Team page. On Standard, an API key makes the two upserts and nothing else.

Start free

Start free, no card

Every new organization starts with a 14-day trial of Plus, with no card. When it ends, a team of 3 people or fewer moves to Free by itself, and a larger team chooses Standard or Plus. There is no second trial.

Start free, no card

A question first? Email us at

hello@contactroll.com

The people who build ContactRoll answer it. We use your message only to answer you, and we do not add you to any list.