Channels and security
Let users choose their delivery channel, and see how proof-of-work difficulty, challenge expiry and Turnstile work.
Letting Users Choose Their Channel
By default a pipeline decides where a code goes. You can instead let the person receiving the
code choose, either by passing channel yourself or by letting the hosted
V2 widget ask them.
Phone channels only. whatsapp, telegram and sms can be chosen. Email cannot be
picked — if your verificationAddress includes an email, it is still sent alongside the
phone channel when that phone channel succeeds; if the chosen phone channel fails, the request
ends there and no email goes out. Passing channel without a phoneNumber is rejected, and so is
passing it as null.
What has to be switched on
Two switches are yours to think about, and they are controlled by different people:
- Your account is entitled — held by Akedly staff. If your calls are refused and your pipeline switch is on, this is the one to ask us about.
- The pipeline's channel-choice switch is on — yours, in Dashboard > Pipelines.
Both must be on. Beyond those, the feature is also unavailable on legacy (non-granular) pricing
and on accounts not billed per message; all of these refuse with the same
channel_choice_not_enabled code, so treat that code as "not available for this account and
pipeline" rather than as a statement about any one switch.
A utility pipeline is a different case: it is rejected earlier, by the endpoint itself, with
403 UTILITY_PIPELINE_ON_OTP_ENDPOINT — you never reach channel validation at all.
What happens when a chosen channel fails
There are two different failures here, and they behave differently.
The send is rejected immediately. The request fails there and then, and returns
422 with pre_send_failure when we knew before sending that the channel could not be used, or
500 with ACTIVATION_FAILED otherwise. A channel you asked for explicitly never falls back,
whatever the pipeline's enforcement setting says — that is the point of naming a channel.
The send is accepted and later reported undelivered. Only this case is governed by the per-pipeline enforcement setting:
- Fallback (default) — the pipeline's normal fallback order takes over, so the person still gets their code, just not on the channel they asked for.
- Hard pin — no fallback is sent; the attempt is left to fail rather than arriving somewhere the person did not choose. Choose this when "it must be this channel or not at all" matters more than delivery.
Only Telegram can be checked before sending
telegram is the only channel whose reachability we can test up front, so it is the only one
that can be refused with pre_send_failure for "this number cannot receive here" — and in the
hosted widget the person is returned to the picker with Telegram removed and their one resend
intact, because nothing was sent. A number with no WhatsApp is only discovered after the
provider accepts the message and later reports it undelivered, which is the second case above.
Billing
Each channel bills at its own rate, exactly as sending does today — choosing SMS costs what SMS costs. Nothing about this feature adds a charge of its own. Your own per-country rates are in Dashboard > Cost.
Security
Difficulty is adaptive
Difficulty is not a static number you set once. On a V1.2 pipeline the Enable Proof-of-Work toggle is rendered locked on, so you cannot switch PoW off from the dashboard, and Adaptive Difficulty is enabled by default, so the ramp is always running.
That is a dashboard constraint rather than a promise that a challenge is always required: challengeRequired still comes back false when dev mode bypasses PoW. Branch on the field — never assume it is always true.
The server scales difficulty linearly from the pipeline's base difficulty up to a maximum, driven by how much of that pipeline's own rate-cap the current traffic is consuming. A quiet pipeline sits at its base; a pipeline under a flood climbs automatically, and eases back down as traffic subsides. Pipelines that also enforce Turnstile start ramping later, since Turnstile already filters headless scripts.
The default base difficulty is 3. Read difficulty from the challenge response on every request — never hardcode a value or cache one across requests.
- Name
- Enable Proof-of-Work
- Type
- boolean
- Description
Shown locked on for V1.2 pipelines — the dashboard offers no way to turn it off.
- Name
- Adaptive Difficulty (Recommended)
- Type
- boolean
- Description
On by default. The system automatically adjusts the difficulty based on traffic patterns.
Challenge expiry scales with difficulty
A challenge does not live for a flat five minutes. Its TTL is derived from the difficulty the server issued, so a harder challenge is given proportionally more time to solve:
| Difficulty | Challenge TTL |
|---|---|
| 2 | 90 seconds |
| 3 (default) | 90 seconds |
| 4 | 120 seconds |
| 5 | 240 seconds |
| 6 | 300 seconds |
Most pipelines expire a challenge in 90 seconds, not 5 minutes
Because the default base difficulty is 3, the typical real-world window is 90 seconds — five minutes only ever applies at difficulty 6. Do not size your solve-and-retry budget on a constant. Use the expiresAt timestamp returned in Step 1, and request a fresh challenge once it passes rather than sending a stale one and handling POW_CHALLENGE_EXPIRED.
Turnstile is opt-in
Unlike Proof-of-Work, Turnstile is off by default. Enable it per pipeline with the Enable Turnstile toggle; switching it on provisions a Cloudflare widget for that pipeline. While it is on, Step 1 returns turnstile.required: true with a siteKey, and Step 2 rejects requests without a token. While it is off, siteKey is null and you omit turnstileToken entirely.
Cluster Gate AI Protection
Cluster Gate AI Protection is coming soon to the V1.2 REST API: an AI-powered layer designed to stop coordinated attacks such as SMS pumping before they run up your bill. A refused request returns the standard 429 rate-limit response, and availability may vary by region. Learn more
