Phone check, OTP, identity check: which question does each answer?
A signup flow has a phone field, and someone on the team asks for it to be "verified." That word covers three different questions. A phone check describes the number: is it well formed, is it valid, and what kind of line it might be. An OTP tests whether the person typing can receive a code on a channel right now. An identity check evaluates a claim about a person. None of these answers another's question. A mobile line does not show who owns it, a delivered code does not establish identity, and an Approved identity result does not grant permission on its own. This article helps you decide which proof your signup task needs, then shows how GridZen's downloadable synthetic example keeps the three apart.
curl https://gridzen.ai/console/api/quickstart/trial \
-H 'Content-Type: application/json' \
-d '{"task": "signup_phone", "scenario": "success"}'The decision: name the question before choosing the proof
Start by writing down what the signup step must establish. If you only want to catch typos and malformed input before an account is created, you need number properties. If you need to know that the person can receive messages at that number, you need channel access, which means an OTP sent through a provider you operate. If a later action, such as letting a seller list goods, depends on who the person is, you need an identity claim checked through hosted onboarding, and then your own business rules. The common mistake is reading a weaker signal as a stronger one: treating a valid mobile number as proof of ownership, or treating Approved as permission. Each proof has its own failure modes, retries and owners, so keep them in separate fields and separate code paths.
What the downloadable example actually implements
The guide's verification_delivery example is a dependency-free Python 3.10+ script. It needs no credentials and makes no network calls. Its phone command has four branches: normal, unknown, provider_failure and otp. Its identity command uses a local SQLite file to store fixture sessions and events. Every output carries mode: synthetic_fixture. The phone branches always return otp_sent: false and live_line_type_available: false. The identity path accepts only synthetic events whose source is fixture_callback. It has no production signature verifier. Any permission it records is a local fixture flag, and every identity read reports live_permission_changed: false. This four-branch download is separate from the anonymous three-scenario HTTP trial; do not read results from one as results from the other. Everything else in this article that goes beyond these outputs is proposed application design for your own code.
Worked hypothetical: a seller signup
Picture a hypothetical B2B marketplace. A new seller enters a phone number. The normal branch returns state: simulated, valid: true, line_type: mobile and next: continue_customer_policy. That tells your design only that the number looks usable. It says nothing about ownership or reachability. If the team wants proof of channel access, the otp branch answers plainly: state: unsupported, valid: null, line_type: null, next: customer_otp_provider. The code must come from your own OTP provider. Later, before the seller can list goods, the identity path creates a session. The create step returns state: simulated, session: fixture-session, a hosted_link on example.invalid, link_executable: false and next: send_real_provider_link_only_after_authorization. A synthetic Approved event with policy set to true returns status: approved, deadline: 60, permission_granted: true and grant_count: 1. In this flow the phone result, the OTP outcome and the identity status each stay in their own field.
Failure handling the caller owns
An unknown phone result is not a rejection. The unknown branch returns state: unknown with next: review_or_existing_otp. The provider_failure branch returns the same unknown state with next: bounded_retry_then_review. Your design should cap the number of retries and then route the signup to review, not block it silently. On the identity side, a duplicate event changes nothing: the output stays approved with grant_count: 1. A later Declined event is treated as a correction. It sets status to declined and permission_granted to false, while grant_count stays at 1. If no decision arrives before the deadline, recover marks a pending, review or unknown session as expired. It returns permission_granted: false, grant_count: 0, next: review_or_authorized_provider_refresh and provider_polled: false. The expiry happens locally; recover never contacts a provider. An event whose session or tenant/subject binding does not match is rejected with BINDING_MISMATCH. Reading a session that does not exist returns SESSION_NOT_FOUND.
Limits of the live routes
As of 2026-10-17, eligible US/GB businesses can self-enroll in a free basic phone route. It checks format and validity only, with up to 10 requests per verified business domain. It does not request line type, ownership, reachability or OTP delivery. A hosted identity trial is also available. It requires separate Didit addendum acceptance and is capped at 2 requests per verified business domain and 10 shared lifetime requests. A newly Approved user and the customer's resulting business action remain unverified end to end. Synthetic fixture permission is not live business permission, and anonymous trials are not production approval.
Acceptance checklist
Run all four phone branches and confirm that each prints the exact state, valid, line_type and next values described above, with otp_sent: false every time. Confirm your signup stores number properties, OTP outcome and identity status in separate fields. Confirm that unknown and provider_failure lead to a bounded retry or review, never an automatic rejection. Run python3 -m unittest -v test_delivery and confirm the duplicate, correction, expiry and binding-error cases pass. Confirm that no code path grants a real permission from a fixture value or from Approved alone. Choose an authorized live OTP or identity route separately before handling any real user. If those checks fit your task, you can save a sandbox request as the next step.
Run the checked synthetic example
Download and extract https://gridzen.ai/downloads/verification-delivery.zip, enter verification_delivery, then run with Python 3.10+. This local example makes no network calls. Use a fresh database for an independent identity run.
python3 delivery.py phone --branch unknown
python3 delivery.py phone --branch provider_failureSources and scope
https://gridzen.ai/guides/phone-otp-identity/ https://gridzen.ai/guides/current-capabilities/ Based on the checked source snapshot and local fixture; proposed application rules are design advice, not proven production behavior. Written with AI assistance and reviewed independently.
Separately try three sandbox outcomes Create an account to continue integrating
A download does not prove a successful run. Local examples and the online sandbox are tested separately.