# Compliance

Every outbound call Sphoro Voice places is checked before the phone rings — against your do-not-call list, against consent where your account requires it, and against the clock where the person you are calling actually is. This page is what those rules are, what each refusal means, and which parts stay yours.

> **None of this is legal advice.** These are mechanical controls that make the common requirements hard to skip. What the law requires of you, where you operate and for the calls you are placing, is a question for your own counsel — not for this page and not for a setting.

## The checks, in the order they run

The order is not an optimisation. Each one is asked only if the one before it said nothing, and a call has to clear all of them twice — see below.

| # | Check | What it asks |
| --- | --- | --- |
| 1 | **The number** | Is this a dialable number at all? Formatting is stripped and forgiven; what is left has to be a plausible international number. |
| 2 | **Suppression** | Is it on your do-not-call list? Asked first of the three real checks because nothing overrides it — not consent, not the hour, not a different campaign. |
| 3 | **Consent** | If your account requires one, is there a valid record of who agreed to be called, when, and how? Asked before the clock so that a number which may *never* be called is reported as such, rather than as “try again at 8am”. |
| 4 | **The calling window** | Is it a reasonable hour *where they are*? Not where you are, and not where this server is. |

> **An uncertain answer is a refusal, every time.** An unresolvable time zone, a list that cannot be read, a number that will not parse — all refuse. The asymmetry is deliberate: a refused call costs you one conversation, and a wrongly placed one costs a statutory penalty per call plus the case behind it. There is no setting that makes this fail the other way.

## Checked twice, and the second one is the one that counts

Once when you create the call, so you find out immediately — that is the `422` you get back — and again in the instant before the number is dialled.

The second check is the one with legal weight. A call created at 8:55pm can reach the front of the queue at 9:05pm, and somebody can opt out in the minutes between; only the check next to the dial can see either. If that check refuses, the call ends as `failed` with the verdict on `end_reason` — except for the calling window, which is re-tried rather than abandoned.

## The calling window

Stated in the local time of the person you are calling. Not your local time, and not this server's — which is the whole reason the platform has to work out where a number is before it can decide anything.

| On this deployment | Setting |
| --- | --- |
| **Calling hours** | `08:00` to `21:00`, local to the number. The end is exclusive: at `21:00` the window has closed, so the last call starts a minute before it. |
| **Days** | Every day. |
| **Consent record** | Not required. A number with no consent record on file is still callable, provided it clears the other checks. |

These are set by whoever operates this deployment. Your account can set two of them for its own calls — whether a consent record is required, and whether the calling window is honoured at all — under **Workspace → Compliance** in the portal, or through `PUT /v1/calls/compliance/policy`. The do-not-call list is deliberately not one of them: nothing an account can set takes a number off it. The pre-flight check below always answers with the rules in force for *you*, after any of that has been applied, which is why it is worth asking rather than reading this table.

### How we work out what time it is where a number is

| Number | How the zone is found |
| --- | --- |
| North America (`+1`) | By area code. Codes that genuinely straddle two zones are deliberately absent from the table, and absent means refused. |
| Single-zone countries | By country calling code. India, the UK, Singapore, the UAE and about thirty others. |
| Countries spanning several zones | Refused. Russia, Brazil, Australia and Mexico are deliberately not listed, because one guess for them is wrong for most of the country. |

Zones are named rather than stored as fixed offsets, so daylight saving is handled properly — Arizona and Saskatchewan do not observe it, and an offset table gets them wrong for two-thirds of the year without ever looking broken.

> **A number we cannot place is refused, not guessed at.** You get `unknown_timezone` rather than a call at six in the morning. If a country you dial is refused this way, say so — the answer is a table entry, not a workaround at your end.

## Every verdict, and what to do about it

These strings are stable. They come back from the pre-flight check below, they are written to `end_reason` on a call that was refused, and they are what an audit is reconstructed from — so they are safe to branch on in your own code.

| Verdict | What it means, and what to do |
| --- | --- |
| `allowed` | Nothing stood in the way. The call is placed, or queued for the dialer.Nothing to do. |
| `invalid_number` final | The number is not a dialable international number — too short, too long, or not a number at all once the formatting is stripped.Fix it at the source. Formatting is forgiven; missing digits are not. |
| `suppressed` final | The number is on your do-not-call list. Nothing overrides this — not consent on file, not a different agent, not a different campaign.If it is there in error, remove it deliberately with `DELETE /v1/calls/suppressions/{number}`. |
| `no_consent` final | Your account requires a consent record before a number may be called, and this number has none.Record how consent was obtained before dialling. See below. |
| `consent_expired` final | A consent record exists, and its `expires_at` has passed.Obtain permission again and record it afresh. The lapsed record is kept. |
| `consent_revoked` final | The person withdrew permission. Revoking also adds the number to the do-not-call list, so this verdict is usually followed by `suppressed`.Do not call. Re-recording consent does not undo a revocation. |
| `unknown_timezone` final | We could not work out what time it is where the number is, so the calling window cannot be honoured.Nothing to retry — this is a property of the number, not of the moment. See the window section for which numbers resolve. |
| `outside_calling_window` wait and it changes | It is too early or too late where the number is, or the day is one your policy excludes.Wait. This is the one verdict that changes on its own: the response carries `callable_at`, and a queued call is re-tried rather than dropped. |
| `compliance_lookup_failed` final | A check could not be completed — the do-not-call list was unreadable, or consent is required and no consent store is configured.Report it. An unanswerable check refuses the call by design, so this is an outage, not a verdict about the number. |

## Ask before you dial

One number, one answer, no call placed. Use it to screen a list before you upload it, and to answer “why did that one not go out” without opening a support ticket.

`GET /v1/calls/compliance/check`

**Shell**

```bash
curl -s -G https://voice.sphoro.com/v1/calls/compliance/check \
  -H "Authorization: Bearer $SPHORO_API_KEY" \
  --data-urlencode "number=+12125551234"
```

**Response200 OK**

```json
{
  "number": "+12125551234",
  "allowed": false,
  "reason": "outside_calling_window",
  "detail": "it is 22:41 for this number; calling is permitted between 08:00 and 21:00 local time",
  "retryable": true,
  "timezone": "America/New_York",
  "callable_at": "2026-08-30T12:00:00Z"
}
```

`callable_at` is absolute time, not a local one, so you can schedule against it without doing any time-zone arithmetic yourself. It is present only when waiting would help.

> **Uploading a campaign already does this for you.** Every row is run through the same checker before anything is dialled. Rows that are merely early are kept and held until their window opens; rows refused for any other reason are listed back to you with the line number and the reason, so you fix the file rather than discovering the problem one failed call at a time.

## The do-not-call list

Your list, per account. It is never shared with another customer, and no other customer's list affects your calls.

`POST /v1/calls/suppressions`

**Shell**

```bash
curl -s -X POST https://voice.sphoro.com/v1/calls/suppressions \
  -H "Authorization: Bearer $SPHORO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"number":"+12125551234","source":"registry_import","note":"national DNC, imported 2026-08-29"}'
```

| Endpoint | What it does |
| --- | --- |
| `GET /v1/calls/suppressions` | List the entries, paginated. |
| `POST /v1/calls/suppressions` | Add one. Idempotent — the number is the key, so adding it twice replaces the entry rather than producing two that disagree about why it is there. |
| `DELETE /v1/calls/suppressions/{number}` | Remove one. A deliberate, separate action, because taking somebody off this list is re-enabling calls to a person who asked for them to stop. |

Numbers are normalised on the way in, so the same number written four different ways in four different exports is one entry and matches all four.

### Numbers on this list are not written down in full anywhere else

In logs and events a number appears masked to its last four digits. A do-not-call list is a list of people who explicitly asked to be left alone; the last four are enough to match a complaint to a call, and the rest has no business in a log aggregator.

## A caller who asks to be left alone is taken off the list during the call

Not when somebody reads the transcript afterwards — by then the next campaign has already run. When the person on the phone says any of these, their number is added to your do-not-call list mid-call and a `call.opt_out` event is sent to your webhook endpoints:

`stop calling` `do not call` `dont call me` `take me off` `remove me from your list` `remove my number` `unsubscribe` `opt out` `opt me out` `no longer wish to be contacted` `stop contacting me` `lose my number` `never call me`

Matching is deliberately generous, and it is generous in one direction on purpose: hearing an opt-out that was not quite one costs you a contact, and missing one that was costs a call to somebody who asked you to stop. It works on either direction of call — somebody who rang in to say “stop calling me” has asked exactly as clearly as somebody who was called — and it takes the other party's number, never your own caller ID.

## Consent records

Whether a consent record is *required* is set for the whole deployment; whether one *exists* is per number, and yours to record. An account calling only its own customers under an established relationship may legitimately not track per-number consent. Anything that looks like acquisition should.

`POST /v1/calls/consents`

**Shell**

```bash
curl -s -X POST https://voice.sphoro.com/v1/calls/consents \
  -H "Authorization: Bearer $SPHORO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "number": "+919876543210",
    "method": "web_form",
    "evidence": "signup-8f31c2e4",
    "granted_at": "2026-08-01T09:12:00Z",
    "expires_at": "2027-08-01T09:12:00Z"
  }'
```

| Field | Type | Description |
| --- | --- | --- |
| `number` required | string | The number the permission covers. |
| `method` required | string | How it was obtained — `web_form`, `verbal_recorded`, `contract`. Free text, because the taxonomy differs by jurisdiction and by counsel. Required, because a consent record with no method is not evidence of anything: the question asked afterwards is “how did you obtain consent”, and “we have a row in a table” is not an answer to it. |
| `evidence` optional | string | What proves it — a form submission id, a recording URL, a contract reference. The claim that survives scrutiny is the evidence, not the flag. |
| `granted_at` optional | timestamp | When they agreed. Defaults to now. |
| `expires_at` optional | timestamp | When it lapses. Omit it and the consent does not expire — which is a claim worth making deliberately rather than by leaving a field out. |

### Revoking

`DELETE /v1/calls/consents/{number}`

Two things at once, because they are one event seen from two sides: the record is marked revoked, *and* the number goes on your do-not-call list. Somebody withdrawing permission has also asked not to be called, and recording the first without doing the second would keep calling them.

The record is marked, never deleted. That consent once existed and when it ended is itself the evidence, and deleting it destroys your own defence along with the row.

## What stays yours

Everything below is real and none of it is done for you. It is listed rather than left unsaid, because a platform that implies it handles all of this is the reason somebody stops checking.

| Not done for you | What that means |
| --- | --- |
| **Registry imports** | There is no automatic sync with a national do-not-call registry. Entries arrive through the API above; scheduling that import is yours. |
| **Registration where it is required** | India's DLT regime and its equivalents elsewhere are registrations held by you, in your own name, and no API here substitutes for one. |
| **Rules narrower than the window** | Several US states are stricter than the federal hours, and the window here is one policy per deployment rather than one per state. |
| **Mobile or landline** | Some rules turn on which a number is. That needs a carrier lookup this platform does not perform. |
| **Reassigned numbers** | A number whose owner has changed is a known trap. No reassignment database is consulted. |
| **How often you call** | Attempt caps and frequency limits are not enforced. Two campaigns can reach the same person on the same day. |
| **What the agent says** | Identifying yourself, saying who is calling and why, and disclosing a recording are your prompt and your greeting — not a setting. |

> **Recording is off until you turn it on, per agent.** Not per deployment, because the rules for an inbound support line and an outbound collections call are different ones, and a single switch would force the looser onto both. Whether you may record — and whether you have to say so out loud first — depends on where both ends of the call are.

## Check it before you dial a list

- You have run one real number through `/v1/calls/compliance/check` and read the verdict, rather than assuming the checks are on.
- Your do-not-call list has whatever you are already obliged to honour in it — nothing imports it for you.
- Your greeting says who is calling. Nothing on this page does that for you.
- Someone who is not us has told you which rules apply to the calls you are about to place.

Back to [the overview](https://voice.sphoro.com/docs/), or [step 3](https://voice.sphoro.com/docs/calls) for placing the call itself.
