Compliance

Suppression lists, opt-out, contact hours, disclosures, consent, retention, erasure and the audit log.

View as Markdown

Jelliu places calls and sends messages on your behalf, so every outbound contact passes through one compliance gate before it happens. This page describes that gate, the controls you can configure through the API, and the records Jelliu keeps so you can show what happened and why.

Jelliu gives you controls and evidence; it does not decide which laws apply to your business. The defaults below are starting points taken from the platform’s configuration, not legal advice.

How it works

The gate reads your per-country configuration, chosen from the prefix of the contact’s phone number. Numbers with no recognizable prefix (including email-only contacts) use the DEFAULT profile.

PrefixCountryPrefixCountry
+57CO Colombia+54AR Argentina
+52MX Mexico+56CL Chile
+55BR Brazil+51PE Peru
+1US United StatesotherDEFAULT

Endpoints

MethodPathAPI key scope
POST/api/compliance/check-callwrite
GET/api/compliance/configsread
GET/api/compliance/configs/{countryCode}read
GET/api/compliance/defaults/{countryCode}read
PUT/api/compliance/configsfull
DELETE/api/compliance/configs/{countryCode}full
GET/api/compliance/suppressionsfull
POST/api/compliance/suppressions/importfull
DELETE/api/compliance/suppressionsfull
GET/api/compliance/consent/{contactId}read
GET/api/compliance/consent/check/{contactId}/{consentType}read
POST/api/compliance/consentfull
DELETE/api/compliance/consentfull
GET/api/compliance/retentionread
PUT/api/compliance/retentionfull
POST/api/compliance/retention/enforcefull
POST/api/compliance/erase/{contactId}full
GET/api/auditfull
GET/api/audit/{id}full
GET/api/audit/verify-chainfull
GET/api/legal/dpa-statusread
POST/api/legal/accept-dpafull
DELETE/api/legal/dpafull
GET/api/settings/data-residencyread

Routes marked full are restricted to workspace administrators or the owner, and an API key stands in for them only with the full scope. See API keys.

Opt-out means total silence

When a person asks not to be contacted, Jelliu stops contacting them on that channel’s identifier and sends nothing further, not even a confirmation. What protects the person is the suppression itself, not an acknowledgement.

Where the request is madeHow it is recognizedWhat Jelliu records
WhatsAppThe whole message is an opt-out keyword, or a phrase such as “no me vuelvas a escribir”. Voice notes are checked after transcription.Every contact with that number set to dnc, and the number added to the suppression list.
EmailThe first non-empty line is an opt-out keyword, or the first 500 characters contain a phrase such as unsubscribe or darme de baja.Every contact with that address (compared case-insensitively) set to dnc.
Phone callThe person asks during the call not to be called again.The contact set to dnc, and the number added to the suppression list, including on inbound calls and calls with no contact.
Instagram and MessengerSame keywords and phrases as WhatsApp.The contact suppressed.

The opt-out keywords are the same on every text channel: stop, stopall, stop all, cancel, unsubscribe, quit, end, baja, cancelar, darme de baja, dar de baja, no molestar. Keywords count only when they are the whole message, so “quiero cancelar mi cita” is answered normally.

After an opt-out:

  • sends to the contact fail (for example 403 CONTACT_OPTED_OUT on WhatsApp, 403 EMAIL_ERROR on email, or 423 from the compliance gate);
  • inbound messages from the person are not answered automatically;
  • campaigns skip the contact.

See WhatsApp and Email for channel details.

Suppression list

The suppression list holds phone numbers your workspace may not contact. It is keyed by number, not by contact, so it survives deleting and re-importing a contact, and the compliance gate checks it before every call and WhatsApp message regardless of any other setting. Jelliu’s own opt-out paths write to it, and you can load lists into it.

Import a registry or your own list

National do-not-call registries such as Colombia’s Registro de Números Excluidos or the US National Do Not Call Registry do not offer a per-number API, so you download the list and import it.

numbers
string[]Required

1 to 200,000 numbers. Spaces, dashes, dots and parentheses are removed; what remains must be E.164 (+ and country code). Local-format numbers are rejected, not guessed.

source
stringRequired

registry:rne_co (Colombia), registry:dnc_us (United States), registry:repep_mx (Mexico), registry:nmp_br (Brazil) or customer_list.

reason
string

Up to 200 characters, stored on every new row, for example RNE export 2026-09-01.

curl -sS -X POST "https://api.jelliu.co/api/compliance/suppressions/import" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": "registry:rne_co",
"reason": "RNE export 2026-09-01",
"numbers": ["+57 300 123 4567", "+573109876543", "3001112233"]
}'
201 Created
{
"data": {
"suppressed": 2,
"received": 2,
"rejected": 1,
"rejectedSample": ["3001112233"]
}
}
FieldDescription
receivedDistinct valid numbers in the request, after removing duplicates.
suppressedNumbers now on the list: new ones plus ones already there.
rejectedEntries that were not valid E.164 and were skipped.
rejectedSampleUp to 10 of the rejected entries, to spot a format problem.

Imports are idempotent. A number already on the list keeps its original reason and date, so re-importing a monthly export never overwrites the record of when someone first opted out.

JSON request bodies are limited to 1 MB. A registry export with hundreds of thousands of numbers does not fit in one request: split it into chunks, as in the examples.

Check the list and its freshness

curl -sS "https://api.jelliu.co/api/compliance/suppressions" \
-H "Authorization: Bearer $JELLIU_API_KEY"
{
"data": {
"total": 184233,
"bySource": [
{ "source": "registry:rne_co", "count": 184100, "lastImportedAt": "2026-08-01T14:03:11.000Z" },
{ "source": "whatsapp", "count": 121, "lastImportedAt": "2026-09-12T19:40:02.000Z" },
{ "source": "customer_list", "count": 12, "lastImportedAt": "2026-07-20T10:00:00.000Z" }
],
"stale": [
{ "source": "registry:rne_co", "ageDays": 44 }
]
}
}

stale lists registry sources whose most recent import is older than 31 days, the maximum age the US National Do Not Call rule allows for a scrub. It is a report, not an enforcement: calls are not blocked because a registry is stale. Schedule a monthly re-import to keep it empty.

Lift one suppression

Removing a number is done one number at a time and requires a written justification, which is stored in the audit log. There is no bulk removal.

curl -sS -X DELETE "https://api.jelliu.co/api/compliance/suppressions" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phoneNumber": "+573001234567",
"justification": "Renewed written consent received on 2026-09-14, ticket 4821"
}'

justification must be 10 to 500 characters. The response is { "data": { "removed": true } }, or false if the number was not on the list.

Lifting a suppression does not change contacts already marked dnc. Update those contacts separately if the person has genuinely consented again.

Compliance configuration

Each workspace can store one configuration per country. Without one, the country defaults apply.

Fields

FieldTypeDescription
country_codestringCO, MX, BR, US, AR, CL or PE.
allowed_call_hoursobjectstart and end as HH:MM, and an IANA timezone.
max_call_attempts_per_dayinteger1 to 10. Calls to the contact today, counted in that timezone.
max_call_attempts_totalinteger1 to 50. The contact’s total call attempts.
require_dnc_checkbooleanAlso block any number that belongs to a contact marked do-not-call. Default true.
blocked_prefixesstring[]Number prefixes that must never be contacted.
require_recording_consentbooleanOpen calls with a recording notice.
consent_scriptstring or nullThe notice to use, up to 2,000 characters.
require_ai_disclosureboolean or nullOpen calls by saying the caller is an AI. null inherits the country default.
require_opt_inbooleanStored with the configuration.
data_retention_daysinteger30 to 3,650. See Data retention.

Country defaults

CountryAllowed hoursTimezoneAttempts per day / totalRetentionRecording notice
CO08:00 to 20:00America/Bogota3 / 10365 daysYes
MX08:00 to 21:00America/Mexico_City3 / 10365 daysYes
BR08:00 to 20:00America/Sao_Paulo3 / 8365 daysYes
US08:00 to 21:00America/New_York3 / 15730 daysYes
AR08:00 to 20:00America/Argentina/Buenos_Aires3 / 10365 daysYes
CL08:00 to 20:00America/Santiago3 / 10365 daysYes
PE08:00 to 20:00America/Lima3 / 10365 daysYes
DEFAULT08:00 to 20:00UTC, evaluated as America/Bogota3 / 10365 daysYes

Every default checks the do-not-call list and has AI disclosure off. GET /api/compliance/defaults/{countryCode} returns the full defaults, including the country’s consent_script and an ai_disclosure_note explaining what is known about AI-disclosure rules there.

Read and change a configuration

GET /api/compliance/configs lists your stored configurations; GET /api/compliance/configs/{countryCode} returns one, or { "data": null } when none is stored.

PUT /api/compliance/configs creates or replaces the configuration for country_code. country_code, require_recording_consent, max_call_attempts_per_day, max_call_attempts_total, allowed_call_hours and data_retention_days are required.

curl -sS -X PUT "https://api.jelliu.co/api/compliance/configs" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"country_code": "CO",
"require_recording_consent": true,
"consent_script": "Esta llamada será grabada con fines de calidad y cumplimiento según la Ley 1581 de Protección de Datos Personales. ¿Autoriza continuar?",
"require_dnc_check": true,
"max_call_attempts_per_day": 2,
"max_call_attempts_total": 6,
"allowed_call_hours": { "start": "09:00", "end": "19:00", "timezone": "America/Bogota" },
"data_retention_days": 180,
"blocked_prefixes": []
}'

DELETE /api/compliance/configs/{countryCode} removes a stored configuration (204), so the country defaults apply again. It returns 404 if none exists.

data_retention_days on a country configuration also moves your workspace’s retention window: it becomes the shortest value across all your country configurations. Setting 30 days for one country shortens retention for all your data.

Allowed contact hours

ChannelAllowed hours apply
CallsYes
WhatsAppYes
EmailNo

Hours are evaluated in the configuration’s timezone, with two exceptions:

  • if your workspace has a timezone set (PATCH /api/settings with { "timezone": "America/Bogota" }), that timezone is used instead;
  • if the resolved timezone is UTC, America/Bogota is used.

A window whose end is earlier than its start spans midnight. Campaigns can narrow the hours further with their own schedule; see Campaigns.

Attempt limits count calls. A contact who has reached today’s call limit is also blocked on WhatsApp and email until the next day.

Check before you contact

POST /api/compliance/check-call runs the gate without contacting anyone:

curl -sS -X POST "https://api.jelliu.co/api/compliance/check-call" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "phoneNumber": "+573001234567", "contactId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" }'
{
"data": {
"allowed": false,
"reason": "Outside allowed call hours (08:00-20:00 America/Bogota)"
}
}

phoneNumber must be E.164. contactId is optional; without it the attempt limits and the contact’s own status are not checked. Possible reasons:

reason
Outside allowed call hours (08:00-20:00 America/Bogota)
Phone number matches blocked prefix: +1900
Phone number is on the opt-out suppression list
Contact is on the Do Not Call list
Phone number is on the Do Not Call list
Max daily call attempts reached (3/day)
Max total call attempts reached (10 total)

Recording notice and AI disclosure

Recording notice. When require_recording_consent is on for the callee’s country (it is on in every default), the notice is added to the opening of the call, using consent_script or a built-in sentence in the call’s language. If the agent’s greeting already announces recording, it is not repeated. If the configuration cannot be read, the notice is included anyway.

AI disclosure. Off by default in every country, because no national rule in the supported countries requires it. It is decided in this order:

  1. the agent’s own discloseAi setting, when set (see Agents);
  2. require_ai_disclosure on the country configuration, when set;
  3. the country default, which is false.

When it applies, the call opens with a sentence such as Hola, soy un asistente de inteligencia artificial de Clínica Norte.

Some rules are state-level or foreign and cannot be derived from a phone prefix. For example, California requires AI-voice calls to say so at the start, and the EU AI Act requires telling people they are interacting with an AI. If your agents may reach people covered by such rules, turn disclosure on for those agents.

Consent records store evidence that a contact agreed to be contacted or to have data processed.

FieldDescription
consent_typevoice_call, email or data_processing.
statusgranted, revoked or erased.
sourceWhere consent was obtained, 1 to 255 characters.
evidenceFree text, up to 2,000 characters.
ip_addressIP address of the request that recorded it.
granted_at, revoked_at, expires_atTimestamps.

Grant consent (returns 201 with { "data": { "success": true } }). Granting again for the same type revokes the previous active record and creates a new one:

curl -sS -X POST "https://api.jelliu.co/api/compliance/consent" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"contactId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"consentType": "voice_call",
"source": "web_form:landing-septiembre",
"evidence": "Checkbox accepted on https://clinicanorte.example.com/contacto",
"expiresAt": "2027-09-14T00:00:00Z"
}'
  • DELETE /api/compliance/consent with { "contactId", "consentType" } revokes it.
  • GET /api/compliance/consent/{contactId} returns the full history, newest first.
  • GET /api/compliance/consent/check/{contactId}/{consentType} returns { "data": { "hasConsent": true } } when a granted, unexpired record exists.

Workspaces configured to require consent before calling skip, in campaigns, any contact without an active voice_call consent.

Data retention

Retention is one number of days for your workspace, from 1 to 3,650 (365 when never set). A daily sweep at 03:00 UTC applies two windows derived from it:

WindowLengthWhat happens
ContentThe shorter of your retention and your plan’s transcript limitCall transcripts, message bodies and conversation summaries are blanked.
RowsYour retentionCall records are deleted from view and messages are deleted.
PlanTranscript and content limit
Starter7 days
Growth30 days
Business90 days
EnterpriseUnlimited (only your retention applies)

Read and set retention

curl -sS "https://api.jelliu.co/api/compliance/retention" \
-H "Authorization: Bearer $JELLIU_API_KEY"
{
"data": {
"retentionDays": 365,
"contentRetentionDays": 30,
"tier": "growth",
"tierCapDays": 30,
"minDays": 1,
"maxDays": 3650
}
}

contentRetentionDays and tierCapDays are null when the plan has no content limit. Set a new window with PUT /api/compliance/retention and { "data_retention_days": 180 }; the response has the same shape. The value is also copied to every stored country configuration.

POST /api/compliance/retention/enforce runs the sweep for your workspace immediately and returns what it did:

{
"data": {
"deletedCalls": 12,
"redactedTranscripts": 48,
"redactedMessages": 310,
"deletedMessages": 95,
"redactedConversations": 22
}
}

Shortening retention destroys data on the next sweep, and enforcing it destroys data immediately. Neither can be undone.

Right to erasure

POST /api/compliance/erase/{contactId} permanently erases a person’s data from your workspace:

  • the contact is anonymized (name and phone replaced with [ERASED], email, WhatsApp number and metadata removed) and deleted;
  • the contact’s calls keep their metrics for analytics, but transcripts and summaries are stripped of personal data, and the conversations are deleted at the voice provider;
  • the contact’s messages are replaced with [ERASED];
  • reservations and appointments matching the person are anonymized;
  • consent records are marked erased;
  • personal data in matching audit log entries is redacted, and each redaction is signed so the audit chain still verifies.
curl -sS -X POST "https://api.jelliu.co/api/compliance/erase/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" \
-H "Authorization: Bearer $JELLIU_API_KEY"
{ "data": { "success": true, "message": "All contact data has been erased" } }

Erasure is irreversible and restricted to the workspace owner, or a full key. It is not available through the MCP server. Erasure does not add the person’s number to the suppression list; if they also asked not to be contacted, import the number as customer_list.

Audit log

Jelliu records who changed what in your workspace: creating, changing and deleting agents, campaigns, contacts imports, webhooks, integrations and connected apps, WhatsApp senders and templates, phone numbers, billing changes, identity and SSO settings, exports, compliance configuration, retention, consent, suppressions and erasure. Actions made with an API key are attributed to apikey: followed by the key ID. Sensitive values in changes are redacted before they are stored.

Entries are hash-chained: each row’s hash covers its content and the previous row’s hash, so any alteration is detectable.

The audit log is available to the workspace owner, or a full key, on every plan.

Query entries

ParameterDescription
resourceTypeFor example campaign, agent, outbound_webhook, contact_suppressions.
resourceIdUUID of the resource.
userIdWho acted: a user ID, or apikey: plus a key ID.
limit1 to 100, default 50.
offsetDefault 0.
curl -sS "https://api.jelliu.co/api/audit?resourceType=campaign&limit=20" \
-H "Authorization: Bearer $JELLIU_API_KEY"
{
"data": [
{
"id": "c0a8012e-4b1f-4d6a-9e3b-2f7c5a1d8e90",
"tenant_id": "9f0fafb4-c3d1-488e-887f-ae2b8375d967",
"user_id": "apikey:6d2b8e14-1a3c-4f5e-8d7b-0c9a2e4f6b13",
"action": "activate",
"resource_type": "campaign",
"resource_id": "0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90",
"ip_address": "203.0.113.24",
"user_agent": "node",
"created_at": "2026-09-14T15:42:07.318Z"
}
]
}

GET /api/audit/{id} returns one entry with its full changes.

Verify the chain

GET /api/audit/verify-chain recomputes the hash chain, optionally between from and to (ISO 8601). The response is not wrapped in data:

{
"valid": true,
"totalChecked": 1842,
"redactedRows": 3,
"redactedRowIds": ["5f1e2d3c-4b5a-4968-8776-655443322110"],
"anchoredMidChain": false
}

When valid is false, firstInvalidRow names the first bad row and reason is hash_mismatch, link_broken or unattested_redaction. anchoredMidChain: true is normal with a from date or after old rows were removed: the first row links to one outside the range, so that single link cannot be checked.

Stream it to your SIEM

Subscribe a webhook to audit.log_recorded to receive each entry as it is written, and filter it by action prefix with auditActions. The event is not included in "*": name it explicitly. See Webhooks.

Data processing agreement

Acceptance of the Data Processing Agreement is recorded per workspace, normally when the owner completes onboarding.

curl -sS "https://api.jelliu.co/api/legal/dpa-status" \
-H "Authorization: Bearer $JELLIU_API_KEY"
{
"data": {
"accepted": true,
"acceptedAt": "2026-08-25T16:20:41.000Z",
"acceptedBy": "user_2abcDEF",
"acceptedVersion": "2026-04-17-v1",
"currentVersion": "2026-04-17-v1"
}
}

POST /api/legal/accept-dpa records acceptance of the current version (optionally { "version": "..." }). DELETE /api/legal/dpa revokes it and requires the header X-Confirm-Revoke: yes. Both are owner-only. For a countersigned DPA, contact support@jelliu.com.

Data residency

GET /api/settings/data-residency returns your workspace’s residency commitment and the processing chain behind it:

FieldDescription
regionglobal (no residency commitment) or us.
label, meaningWhat the commitment means, in Spanish.
pinned, pinnedAt, pinnedByWhether and when the region was committed.
optionsEvery region, whether it can be requested, and why not.
chainEach subprocessor, what it handles, its region today where verified, and whether it can be pinned.
chainVerifiedOnWhen the chain was last verified.

Jelliu’s database and compute run in the United States. A us commitment is available; an EU region is not offered because there is no EU deployment. Committing to a region is a one-time decision made by the owner in the dashboard; API keys cannot make it.

Encryption of stored credentials

Credentials you give Jelliu for connected systems, such as accounting, payment and social-media tokens, are encrypted at the application level before they are stored, with AES-256-GCM and a key derived per record. For transport security, tenant isolation and webhook signing, see Security.

Errors

StatusCodeWhen
400VALIDATION_FAILEDInvalid body or parameters. For retention: data_retention_days must be a whole number of days between 1 and 3650. For DPA revocation without the header: DPA revoke requires X-Confirm-Revoke: yes header.
403FORBIDDENThe key is not full on an admin or owner route: This operation requires an API key with the 'full' scope.
403FORBIDDENCommitting a data region with an API key: This action requires a signed-in admin session — API keys cannot perform credential-management operations.
404NOT_FOUNDCompliance config not found for country CO, or Audit log entry not found.
423COMPLIANCE_BLOCKED or EMAIL_ERRORA send or call was stopped by the compliance gate. The message is the gate’s reason.
429RATE_LIMIT_EXCEEDEDRate limit. On the audit log: Too many audit log requests, please slow down.

Limits

LimitValue
/api/compliance, /api/legal, /api/settingsGeneral per-workspace API limit. See Rate limits.
/api/audit20 requests per minute, in addition to the general limit.
Suppression import200,000 numbers per request, within a 1 MB JSON body.
Consent evidence2,000 characters.
Retention1 to 3,650 days (30 to 3,650 on a country configuration).