> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apten.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# External Compliance Checks

> Gate every outbound SMS, email, or call against your own compliance middleware before Apten sends.

## Overview

External Compliance Checks are a **per-organization** product feature: your org can wire a custom HTTPS endpoint into Apten's send pipeline. Before outbound sends on the channels you select, Apten POSTs a check request to your middleware. If your middleware says the contact is not allowed on that channel, Apten suppresses the send, writes an audit record, and surfaces a `COMPLIANCE_BLOCKED` event in the conversation timeline and events export.

Use this when you have an internal compliance system (state DNC lists, litigator scrubbers, vendor-provided suppression sources, internal customer-preference databases) that must be the source of truth for whether a given lead can be contacted right now.

<Note>
  **Apten-managed DNC** runs on every outbound SMS and voice call, regardless of
  these settings. Numbers on Apten's standing Do Not Contact list are blocked
  before send. Manage that list via the
  [Do Not Contact API](/api-reference/dnc/register). This feature composes with
  — it does not replace — Apten's built-in
  [opt-out handling](/webhooks/opt-out).
</Note>

## Setup

External compliance is configured per-organization in the **Settings → Compliance** tab. Only users with the **Admin** role can change these settings.

### Apten-managed DNC

Apten always checks outbound **SMS / RCS** and **voice** against our standing DNC list. This is not configurable in the Compliance tab.

### Enable the external API

Toggle **External compliance API** on to POST to your middleware before sends on selected channels.

### External API channels

Select which channels (**SMS / RCS**, **Voice**, **Email**) should also be checked against your middleware. Channels you don't select skip the external check. Unchecking all channels (or turning the external API off) does **not** disable Apten-managed DNC on SMS and voice.

### Failure mode

You must explicitly choose how Apten behaves when your middleware is unreachable, returns a non-2xx response, or times out:

* **Fail Closed** — block the send. Safer for regulatory compliance, but a middleware outage will pause outbound traffic.
* **Fail Open** — allow the send. Avoids dropped sends during outages, but a malfunctioning middleware will not suppress contacts.

There is no system default — the trade-off depends on your regulatory posture.

### Endpoint URL

The HTTPS URL of your middleware. Apten will POST every check request here. The URL must use `https://`.

### Signing secret

Apten signs every request with an HMAC-SHA256 of the request body, using a shared secret you control. Your middleware uses this to verify that requests genuinely came from Apten.

In the **Signing Secret** field you can:

* **Generate** — produces a random 32-byte base64url secret in your browser and fills the field. Click before saving and copy the value to your middleware.
* **Paste** — paste a secret you generated elsewhere.
* **View saved** — appears once a secret has been saved. Click to fetch and display the currently saved secret so you can copy it to a new middleware deployment. (Saved secrets are encrypted at rest with AWS KMS and only revealed to admins on demand.)
* **Eye toggle / Copy** — show or hide the secret in the field, or copy it to the clipboard.

Leave the field blank when saving to keep the existing secret unchanged.

### Timeout

How long Apten waits for your middleware to respond before applying the failure mode. Default `1500ms`, configurable from `100ms` to `10000ms`.

## Compliance check API

For every gated send, Apten makes the following request to your endpoint:

```http theme={null}
POST {endpointUrl}
Content-Type: application/json
User-Agent: Apten-Compliance/1.0
X-Apten-Signature: sha256=<hmac-sha256(secret, body)>
X-Apten-Timestamp: <unix-ms>
X-Signature-SHA256: <hmac-sha256(secret, body)>
```

```json theme={null}
{
  "leadId": "a7e92c5d-f301-4b89-ae16-8f35d02cx9z",
  "channel": "sms",
  "phone": "+14155551212",
  "email": null,
  "externalLeadId": "003XX000004DHPY",
  "triggeredBy": "send-text-message",
  "additionalInfo": { "dncLastCheck": "2026-06-01", "matchRequestId": "req-123" }
}
```

### Request properties

| Property         | Type   | Description                                                                                                                                                                 |
| ---------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `leadId`         | string | The Apten lead ID being contacted.                                                                                                                                          |
| `channel`        | string | The channel about to be used. One of `sms` (also covers RCS), `voice`, or `email`.                                                                                          |
| `phone`          | string | Lead phone number in E.164 format. Set when `channel` is `sms` or `voice`. `null` for email checks.                                                                         |
| `email`          | string | Lead email address. Set when `channel` is `email`. `null` for SMS / voice checks.                                                                                           |
| `externalLeadId` | string | CRM lead ID, when known (Salesforce / HubSpot / Dynamics / Close CRM).                                                                                                      |
| `triggeredBy`    | string | Internal label for which send path triggered the check, e.g. `send-text-message`, `queue-call`, `voice-poller-dial`. Useful for diagnostics; not part of a stable contract. |
| `additionalInfo` | object | The lead's custom [`additionalInfo`](/api-reference/leads/get-lead) fields, as a key/value object. `{}` when none are set.                                                  |

<Note>
  Only the identifier relevant to the channel is included. SMS / voice checks
  send only `phone`; email checks send only `email`. The unused field is
  `null`. This minimizes the PII surface forwarded to your middleware.
</Note>

### Expected response

The only field Apten reads is a top-level `allowed` boolean. The minimum valid response is:

```http theme={null}
200 OK
Content-Type: application/json
```

```json theme={null}
{ "allowed": true }
```

or, to block the send:

```json theme={null}
{ "allowed": false }
```

| Property  | Type    | Description                                                                                                    |
| --------- | ------- | -------------------------------------------------------------------------------------------------------------- |
| `allowed` | boolean | **Required.** `true` to allow the send; `false` to suppress it. Any other type is treated as a provider error. |

A non-2xx status, a timeout, or a non-boolean `allowed` field is treated as a **provider error** — your org's configured failure mode then decides whether the send proceeds.

#### Optional extra fields

Anything else you include in the JSON body is recorded verbatim in Apten's audit row for your own debugging. Apten does not interpret these fields or use them to make decisions. Two conventional fields:

```json theme={null}
{
  "allowed": false,
  "reason": "internal_dnc",
  "metadata": { "matched_list": "litigator-q2" }
}
```

* `reason` — a short tag explaining why you blocked. Stored on Apten's internal audit row so you can correlate the block back to your own systems if you ever request an audit export.
* `metadata` — any additional context you want preserved alongside the audit row.

You don't need to send these. Apten's gating decision is `allowed` and only `allowed`.

## Verifying the signature

Apten signs the request body with the same HMAC-SHA256 scheme used for [outbound webhooks](/webhooks/verification). Verify in your middleware by recomputing the HMAC using the shared signing secret and comparing it to the `X-Signature-SHA256` header.

<Note>
  Every request also carries an `X-Apten-Signature` header in the form
  `sha256=<hex>`. It is the same HMAC value as `X-Signature-SHA256` with a
  scheme prefix. Verify against **`X-Signature-SHA256`** (raw hex) — that is
  the canonical header for this endpoint.
</Note>

<Warning>
  Always use a constant-time comparison (`crypto.timingSafeEqual`,
  `hmac.compare_digest`, etc.) — never `==`. Guard against missing or
  wrong-length signatures **before** the comparison; both `timingSafeEqual`
  and `compare_digest` raise when their inputs aren't both present and of the
  same length.
</Warning>

<CodeGroup>
  ```javascript Node.js theme={null}
  import crypto from 'crypto';

  // Mount Express with `express.raw({ type: 'application/json' })` (or your
  // framework's equivalent) for this route so `req.body` is the raw Buffer
  // that Apten signed. Re-serializing a parsed object via JSON.stringify can
  // change whitespace and key ordering and will break the signature.
  function verifyComplianceRequest(req) {
    const secret = process.env.APTEN_COMPLIANCE_SECRET;
    const signature = req.headers['x-signature-sha256'];
    if (typeof signature !== 'string' || signature.length === 0) {
      return false;
    }

    const expected = crypto
      .createHmac('sha256', secret)
      .update(req.body) // raw Buffer
      .digest('hex');

    if (expected.length !== signature.length) {
      return false;
    }
    return crypto.timingSafeEqual(
      Buffer.from(expected, 'utf8'),
      Buffer.from(signature, 'utf8'),
    );
  }
  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  import os

  def verify_compliance_request(request):
      secret = os.environ['APTEN_COMPLIANCE_SECRET']
      payload = request.get_data()  # raw bytes — do not re-serialize
      signature = request.headers.get('X-Signature-SHA256')
      if not signature:
          return False

      expected = hmac.new(
          secret.encode('utf-8'),
          payload,
          hashlib.sha256,
      ).hexdigest()

      if len(expected) != len(signature):
          return False
      return hmac.compare_digest(expected, signature)
  ```
</CodeGroup>

<Note>
  Always sign and verify the **raw** request body bytes, exactly as they arrived
  on the wire. Re-serializing a parsed JSON object can change whitespace and
  key ordering and break the signature.
</Note>

## Provider behavior

### Apten DNC and external HTTP ordering

**Apten-managed DNC** runs first on every outbound **SMS** and **voice** send (phone-keyed list via the [Do Not Contact API](/api-reference/dnc/register)). It does not apply to **email**.

If **External compliance API** is enabled and the channel is in your `channels` set, Apten then POSTs to your middleware. A DNC block returns before that HTTP call. Provider errors on the HTTP leg follow your **Failure Mode**; a definitive DNC block is never overridden by fail-open.

### Per-channel decisions

Each channel is checked independently. If a lead is blocked on `sms` but allowed on `email`, only the SMS send is suppressed. For the first-message **Text + Email** outreach pattern, a block on one channel does not prevent the other from sending.

### No caching

Every send triggers a fresh check against your middleware. Apten does not cache responses, because follow-up cadences typically space outbound attempts hours or days apart — a useful cache TTL would expire before the next send anyway. Size your middleware capacity for your peak send volume.

## Audit log

Every check — allowed or blocked — is written to Apten's internal audit table including the request, response, source, channel, and timestamp. This is the system of record for SOC 2 evidence. **Blocked** checks also appear as `COMPLIANCE_BLOCKED` events on [`GET /leads/{leadId}/events`](/api-reference/leads/list-events); allowed checks are audit-only. Contact Apten support if you need a full audit export (including allowed checks) for a specific time range.

## Failure modes at a glance

| Scenario                                                    | Outcome                                                                                  |
| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| Middleware returns `{ "allowed": true }`                    | Send proceeds.                                                                           |
| Middleware returns `{ "allowed": false }`                   | Send is suppressed; audit row written.                                                   |
| Middleware returns 5xx, times out, or non-boolean `allowed` | Provider error — your **Failure Mode** decides whether the send proceeds.                |
| External API disabled or channel not in `channels`          | That channel skips the external middleware check. SMS/voice still run Apten-managed DNC. |
| `phone` and `email` both missing for the requested channel  | Send is suppressed with reason `COMPLIANCE_TARGET_MISSING`. Audit row written.           |

## Best practices

* **Always start in fail-open** while bringing your middleware online; flip to fail-closed once you've watched real traffic and tuned your latency.
* **Keep middleware p99 latency well under your configured timeout.** A 1500ms timeout with a 1400ms p99 will produce sporadic fail-mode activations.
* **Rotate the signing secret quarterly** from the Settings UI. Use **View saved** to retrieve the current value before rotation; then **Generate**, save, and deploy the new value to your middleware.
* **Log the `X-Apten-Timestamp` header** and reject requests older than a few minutes to protect against replay if your endpoint is ever publicly reachable.
