API getting started

Source: content/4preferences/developers/api-getting-started.md

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"
      }'
  • email and corePreferenceId are required. Everything else is optional.
  • value is whatever the preference's type produces: on for a single checkbox, an option value from the catalog for a list type, free text for a text type.
  • channels and frequencies are strings, not arrays.
  • source is 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 as api_direct.
  • An empty value deletes 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_id entirely, 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.