sphoroVOICEdocs
Markdown

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.

#CheckWhat it asks
1The numberIs this a dialable number at all? Formatting is stripped and forgiven; what is left has to be a plausible international number.
2SuppressionIs 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.
3ConsentIf 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”.
4The calling windowIs 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 deploymentSetting
Calling hours08: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.
DaysEvery day.
Consent recordNot 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

NumberHow 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 countriesBy country calling code. India, the UK, Singapore, the UAE and about thirty others.
Countries spanning several zonesRefused. 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.

VerdictWhat 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
curl -s -G https://voice.sphoro.com/v1/calls/compliance/check \
  -H "Authorization: Bearer $SPHORO_API_KEY" \
  --data-urlencode "number=+12125551234"
Response200 OK
{
  "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
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"}'
EndpointWhat it does
GET /v1/calls/suppressionsList the entries, paginated.
POST /v1/calls/suppressionsAdd 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.

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
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"
  }'
FieldTypeDescription
number
required
stringThe number the permission covers.
method
required
stringHow 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
stringWhat 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
timestampWhen they agreed. Defaults to now.
expires_at
optional
timestampWhen 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 youWhat that means
Registry importsThere 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 requiredIndia'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 windowSeveral US states are stricter than the federal hours, and the window here is one policy per deployment rather than one per state.
Mobile or landlineSome rules turn on which a number is. That needs a carrier lookup this platform does not perform.
Reassigned numbersA number whose owner has changed is a known trap. No reassignment database is consulted.
How often you callAttempt caps and frequency limits are not enforced. Two campaigns can reach the same person on the same day.
What the agent saysIdentifying 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, or step 3 for placing the call itself.