Compliance
Consent, disclosures, and 10DLC are first-class in Saperly and enforced independently of any LLM — outbound contact without recorded consent is blocked before it goes out.
Saperly bakes TCPA compliance in. Consent, disclosures, and 10DLC registration are first-class objects enforced independently of any LLM — so a misbehaving model (or a model you swap out) can never route around them. Outbound voice or SMS to a peer without recorded consent is rejected before it goes out.
Base URL https://api.saperly.com. Authenticate with Authorization: Bearer sk_live_...; the workspace is resolved from the token.
Consent
Consent is recorded per (number, peer) pair: a ConsentRecord says this Saperly number has consent to contact that peer number. Saperly checks it automatically on every outbound message and call.
Consent types
ConsentType | Meaning |
|---|---|
implied_inbound | The peer contacted you first (an inbound call or SMS), which implies consent to reply. |
explicit_outbound | The peer explicitly opted in to be contacted — required before you initiate outbound contact. |
Endpoints
| Method | Path | Returns | Scope |
|---|---|---|---|
GET | /consent | ConsentRecord[] | — |
POST | /consent | ConsentRecord (201) | consent:write |
POST | /consent/revoke | ConsentRecord | consent:write |
GET | /consent/check?numberId=&peerNumber= | { hasConsent, type? } | — |
The ConsentRecord shape
| Field | Type | Notes |
|---|---|---|
id | string | Record id. |
numberId | string | The Saperly number. |
peerNumber | string | The peer, in E.164 (e.g. +15555550123). |
consentType | ConsentType | implied_inbound or explicit_outbound. |
source | string | Free-form provenance label, e.g. "sms_optin" or "call_recording_disclosure". |
grantedAt | string | ISO 8601 timestamp. |
revokedAt | string | null | ISO 8601 timestamp once revoked, else null. |
Worked flow: check, record, then contact
Always check consent before initiating outbound contact, record explicit_outbound consent when the peer opts in, then send. The source label is your audit trail — make it describe how consent was obtained.
# 1. Check before contacting
curl "https://api.saperly.com/consent/check?numberId=num_...&peerNumber=%2B15555550123" \
-H "Authorization: Bearer $SAPERLY_API_KEY"
# -> { "hasConsent": false }
# 2. The peer opts in — record explicit outbound consent
curl -X POST https://api.saperly.com/consent \
-H "Authorization: Bearer $SAPERLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"numberId": "num_...",
"peerNumber": "+15555550123",
"consentType": "explicit_outbound",
"source": "sms_optin"
}'
# 3. Now the send/call is allowed (see Messaging / Voice)import { Saperly } from '@saperly/sdk'
const saperly = new Saperly({ apiKey: process.env.SAPERLY_API_KEY! })
// 1. Check
const { hasConsent } = await saperly.consent.check({
numberId: 'num_...',
peerNumber: '+15555550123',
})
// 2. Record on opt-in
if (!hasConsent) {
await saperly.consent.record({
numberId: 'num_...',
peerNumber: '+15555550123',
consentType: 'explicit_outbound',
source: 'sms_optin',
})
}
// 3. Now send — see Messaging
await saperly.messages.send({
fromNumberId: 'num_...',
to: '+15555550123',
body: 'Thanks for opting in!',
})Outbound without consent is a TCPA violation
Revoking consent
POST /consent/revoke with { numberId, peerNumber } sets revokedAt and stops further outbound contact. Two things revoke consent without an explicit call:
STOP— an inbound SMS with theSTOPkeyword automatically revokes the sender's consent. Subsequent outbound sends to that number are blocked.HELP— an inboundHELPkeyword is logged (it does not change consent).
curl -X POST https://api.saperly.com/consent/revoke \
-H "Authorization: Bearer $SAPERLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "numberId": "num_...", "peerNumber": "+15555550123" }'Disclosures
A disclosure is the AI notice spoken at the start of a call so the caller is told they are talking to an AI. It lives on the connection, not as a separate resource: each connection has complianceEnabled (a boolean, on by default) and a disclosure string. When compliance is on, the disclosure is spoken as the line's first, uninterruptible utterance — the TCPA notice — before the agent takes a turn, across every mode (hosted, manual, and OpenAI-realtime).
// The disclosure is a field on the connection — set it when you create or update one
await saperly.connections.create({
name: 'support line',
instructions: 'You are a friendly support agent for Acme Inc.',
complianceEnabled: true,
disclosure: 'You are speaking with an AI assistant for Acme.',
})Leave disclosure empty with complianceEnabled on and Saperly fills a standard, org-named default so the line is never silently non-disclosing. Set complianceEnabled to false and there is no forced opener. When a recorded disclosure is the basis for contact, record it as a consent source (e.g. "call_recording_disclosure") so the provenance is captured in your audit trail.
10DLC
10DLC is the US carrier registration that certifies an application-to-person (A2P) SMS campaign. Registering a brand and campaign is what lets your US SMS deliver reliably instead of being filtered. Saperly tracks the registration and advances its status as carrier webhooks arrive.
Campaign status
TenDlcStatus | Meaning |
|---|---|
pending | Submitted to the carrier, awaiting review. |
approved | Certified — A2P SMS is cleared. |
rejected | The carrier declined the campaign. |
suspended | A previously approved campaign was suspended. |
The TenDlcCampaign shape
| Field | Type | Notes |
|---|---|---|
id | string | Campaign id. |
useCase | string | The declared A2P use case (e.g. 2fa, customer_care). |
description | string | Human-readable campaign description. |
status | TenDlcStatus | pending on creation, advanced by webhook. |
createdAt | string | ISO 8601 timestamp. |
Registering a campaign
Register with { useCase, description }. The campaign starts pending; the final status arrives by webhook — listen for 10dlc status events rather than polling.
{
"useCase": "customer_care",
"description": "Appointment reminders and support replies for Acme Clinic."
}10DLC is primarily a dashboard + webhook flow today
10DLC management currently lives mostly in the dashboard, and campaign status updates arrive by webhook as the carrier reviews the brand and campaign. Wire up a webhook for 10dlc events so your system learns when a campaign moves to approved (or rejected / suspended) without polling.
Related
- Messaging — SMS sends; consent is enforced on every outbound message.
- Voice — outbound calls; consent is enforced before the call starts.
- Webhooks — receive
STOP/HELP, delivery receipts, and 10DLC status events. - Core concepts — where compliance sits in the Saperly model.
- API reference — every
consentendpoint with a try-it playground.
Connections
A connection is the handler bound to a phone number — the thing that answers a call — in either hosted or manual mode, with audio always staying in-network.
Billing
Saperly is prepaid — you top up a balance and usage meters against it through a reserve → settle → release ledger that can never overspend a balance or a scoped key's spend cap.