API getting started
This page is for someone writing code against 4Preferences. If you are looking for the screens, start at Getting started with 4Preferences.
4Preferences is part of the 4Comply API. Same host, same credentials, same conventions, and the same paging, sorting and filtering. If you have already integrated 4Comply, there is nothing new to learn about authentication here.
Production is https://api.4comply.io. Every path below is relative to that.
Authentication
Two headers on every call.
Authorization: Bearer sk_eyJhbGciOi...
tenant_id: 6a1f00c2b48e3d5417ab9d20
Both values come from the Integrations tab of Settings in the dashboard, described in
Settings. Tenant ID goes in the
tenant_id header and Secret Key goes in the Authorization header, prefixed with
Bearer . The key already starts with sk_, so do not add that yourself.
The two are checked together: the secret key has to be the current key for the tenant named
in the tenant_id header. Rotating the key with Update Token invalidates the old one
immediately, and a call with a mismatched pair gets a 401 reading
Secret Key Token does not match the tenant.
A secret key can do anything in the tenant, including the writes that need the Admin or
Config role in the dashboard. The one thing it cannot do is rotate itself: calling the
token-update endpoint with a secret key returns 401.
TODO(review): confirm this is the credential customers are told to use for server-to-server integration, and whether an Auth0 user token is supported for anything other than the dashboard. The API accepts both.
Bodies and responses are JSON with camelCase field names. The tenant is never read from the body, only from the header, so there is no tenant field to send.
The route families
Every path is under /v1. Configuration first, the things you build once:
| Route | Holds |
|---|---|
/v1/corepreferences |
the preference definitions |
/v1/corecontactfields |
the contact-attribute definitions |
/v1/preferencecatalogs |
the answer sets |
/v1/preferenceoptions |
the individual options inside them |
/v1/preferencechannels |
the channels |
/v1/preferenceFrequencys |
the frequencies |
/v1/preferencepages |
the pages |
/v1/preferenceforms |
the page layouts, versioned |
/v1/preferenceagents |
the conversational agents |
/v1/preferencewhitelists |
who may load a public page or agent |
/v1/preferencewebhooks |
your webhook subscriptions |
/v1/corepreference-integrations |
preference to compliance-input mappings |
/v1/preferenceFrequencys is spelled exactly that way, with the capital F and the plural
s. It is a typo in the route, but it is the live path, and any other spelling returns
404.
Per-contact state, the things that change constantly:
| Route | Holds |
|---|---|
/v1/contactpreferences |
what each contact has chosen now |
/v1/contactpreferenceshistory |
every change they have made |
Reporting:
| Route | Holds |
|---|---|
/v1/preferencedashboard |
the dashboard tiles and charts |
/v1/preferenceflowreport |
pages to preferences to contacts |
/v1/preferenceanalytics |
the date-ranged summary and its Excel export |
Most families follow the same shape: GET / lists, GET /{id} reads one, POST / creates,
PUT /{id} updates, DELETE /{id} deletes, and GET /exists/{id} answers whether an id is
real without returning the record. Several also offer GET /{id}/checkDependencies and
GET /{id}/dependencies/detailed, which tell you what would break before you try a delete.
The first useful call
Read the catalog before you write anything, because a write has to reference ids that already exist.
curl -s https://api.4comply.io/v1/preferencecatalogs \
-H "Authorization: Bearer sk_..." \
-H "tenant_id: <your tenant id>"
That is GET /v1/preferencecatalogs, and it returns the answer sets with their options
inline. Then read the preferences themselves with GET /v1/corepreferences. Each one
carries the fields you need for everything else:
| Field | Why you need it |
|---|---|
id |
the key for a write to /v1/contactpreferences/individual |
internalID |
the key a preference page's form submission uses |
name |
what your own logs and screens should call it |
type |
whether the answer is a checkbox, a list choice or free text |
preferenceCatalogId |
which answer set applies, for the list types |
channelsIds, frequenciesIds |
which channels and frequencies the contact may pick |
businessUnit |
which pages can offer it |
The channel and frequency ids resolve against GET /v1/preferencechannels and
GET /v1/preferenceFrequencys.
Request and response shapes for every endpoint named on this page are in the 4Preferences API reference beside these guides. Search it for the path.
Writing one preference
POST /v1/contactpreferences/individual sets one contact's answer to one preference. It is
the API equivalent of a contact ticking one box, and it is the endpoint an integration should
use.
curl -s -X POST https://api.4comply.io/v1/contactpreferences/individual \
-H "Authorization: Bearer sk_..." \
-H "tenant_id: <your tenant id>" \
-H "Content-Type: application/json" \
-d '{
"email": "demo1@example.com",
"corePreferenceId": "<a core preference id>",
"value": "on",
"channels": "Email",
"frequencies": "Weekly",
"source": "crm-sync"
}'
emailandcorePreferenceIdare required. Everything else is optional.valueis whatever the preference's type produces:onfor a single checkbox, an option value from the catalog for a list type, free text for a text type.channelsandfrequenciesare strings, not arrays.sourceis your own label and it lands in the Source column of Contact Preferences. Set it to something that identifies your integration. Omit it and the row is recorded asapi_direct.- An empty
valuedeletes the contact's preference rather than storing an empty one, because an empty answer is an opt-out. The response says"action": "deleted"when that happens.
Every call writes a history row whether the answer changed or not. That makes the endpoint safe to call repeatedly, but it is not free: a nightly full resync fills Contact Preference History with rows that record no change.
Reading a contact back
GET /v1/contactpreferences/by-email/{email} returns everything one contact has chosen.
That is the call to make before a send, if you are the system deciding what to send.
GET /v1/contactpreferences with a Sieve filter is the bulk version, and it returns an empty
list rather than the whole table when you send no filter at all. Paging headers come back on
the response, as described in
Paging, Sorting and Filtering.
TODO(review): confirm the intended pattern for a sending system that has to check thousands of contacts before a campaign. Per-contact reads, a Sieve-filtered bulk read, or a webhook feed into the sender's own store? The three have very different costs, and the answer belongs on this page.
The public endpoints
Five 4Preferences endpoints take no credentials, because the caller is a contact rather than a system. They are documented where they are used, but collected here so nobody wires a secret key into them by accident.
| Endpoint | Guide |
|---|---|
GET /v1/preferencepages/externalrender/{tenantId}/{pageId} |
Preference pages |
POST /v1/contactpreferences/{pageId} |
Preference pages |
GET /v1/preferenceagents/public/{tenantId}/{slug} |
Preference agents |
POST /v1/preferenceagents/public/{tenantId}/{slug}/conversations |
Preference agents |
POST /v1/preferenceagents/public/conversations/{conversationId}/messages |
Preference agents |
Two things about them differ from the rest of the API.
- The tenant comes from the route, not a header. These endpoints ignore
tenant_identirely, on purpose: an anonymous caller must not be able to name the tenant it acts against. - The agent endpoints are gated by the whitelist and by per-IP rate limits rather than by a credential. See The whitelist.
POST /v1/contactpreferences/{pageId} is the form post a preference page makes. It takes
application/x-www-form-urlencoded, not JSON: tenantId, one CC_<internalID> field per
core contact, and one CP_<internalID> field per preference, with optional
CP_<internalID>_channel and CP_<internalID>_freq alongside. It answers with a redirect to
the page's Redirect URL rather than with a body, so it is built for a browser form and
not for an integration. Use /v1/contactpreferences/individual from code.
Getting told when something changes
Rather than polling, subscribe to
preference webhooks.
GET /v1/preferencewebhooks/triggers lists the events you can subscribe to and
POST /v1/preferencewebhooks creates a subscription. The payload shape, the X-4Comply-Signature header and the retry
schedule are all on that page.