Skip to main content
Create an SMS OTP challenge, persist its ID against a local application session, then verify the recipient’s code against that saved challenge. This terminal application uses Python’s standard HTTP and SQLite libraries. It needs Python 3.11 or later and no pip packages. This is a Python application tutorial, not an official SDK or complete login system. For individual requests in cURL, JavaScript, Python and Java, use Create and verify challenges.

1. Run the application tests offline

The test suite starts a local HTTP stub and temporary database. No credentials are needed and no SMS is sent. It tests saved idempotency after a lost response, challenge/user correlation, duplicate verification, wrong codes, expiry, lockout, invalidation, consumed challenges and uncertain verification outcomes. Read the complete application and README. These tests verify the application’s behavior. Approved Sandbox access is still required to exercise actual code delivery and platform policy.

2. Prepare a permitted Sandbox request

Complete the Quickstart. You need Sandbox access to create, verify and retrieve OTP challenges for your business, an authorized sender, a permitted test recipient and an approved OTP template key. If OTP is unavailable for your business, contact Support before continuing. See OTP integration for the workflow. In the application directory:
Create private request.json, replacing the sender, destination and template key:
template_key identifies an approved OTP template. It is distinct from an SMS template_id. If business policy permits inline OTP content, this alternative request includes the required {{code}} placeholder and omits template_key:
Choose one payload before creating the local session. Do not put the actual code into this file; OneFunnel generates it. Omitting both content selectors requires a matching approved default binding; see Create an OTP challenge.

3. Create the challenge and save its ID

login-1001 is this attempt’s stable application session ID; user-42 represents the application user. The terminal operator supplies both in this exercise. A web integration must obtain user/session identity from trusted server context; accepting a caller-supplied user ID is not authentication. Before POST, the application commits an idempotency key and request bytes to SQLite. It requires HTTP 202, then saves the returned challenge ID, status, code_length and expires_at. The create response never includes the code. Acceptance does not mean the SMS was delivered. If the response is lost, repeat the exact command with the same request and database. It reuses the saved key/body instead of creating a new attempt. Once an ID is saved, it skips another create POST. Changing the payload or subject under the same local session is rejected. Follow idempotency recovery if acceptance remains uncertain beyond the seven-day replay window.

4. Verify the code against the saved challenge

Enter the code received by the test recipient at the hidden prompt. Do not put codes in command arguments, logs or the database. The application loads the challenge ID associated with that local session and subject, then calls:
The URL supplies challenge correlation; the body carries only the code. See Verify an OTP challenge. Verification does not support an idempotency key and the application does not add one or automatically retry the code. Success requires HTTP 200, the saved challenge ID, the original purpose and status: verified. The application persists verification_state: verified and verified_at. It does not issue a login token or claim SMS delivery. A repeat local verify command is blocked instead of making another POST.

5. Handle rejected and uncertain attempts

A wrong code returns HTTP 422 with otp_verification_failed. The application records that error and permits a deliberate new recipient submission. The platform still enforces its attempt limit; there is no automatic code retry. Expiry returns HTTP 422 with otp_challenge_expired. Lockout returns HTTP 429 with otp_challenge_locked. Invalidation and an already consumed code return HTTP 409 with their distinct codes. These block verification locally; none are treated as a new successful check. For other 429 responses, inspect the error code and respect Retry-After. See Error handling. If verification times out or the process exits during the request, the database retains verification_state: uncertain. Inspect it and retrieve current state:
The status command requires HTTP 200 from Retrieve OTP challenge status. A GET reporting verified is an observation, not evidence that a fresh code submission succeeded in this application. The example keeps local proof uncertain and issues no token. Apply your product’s reviewed recovery policy; do not blindly retry verification or edit local state to manufacture success. After a definitively expired or blocked flow, a user can explicitly begin a new attempt under your product policy with a new local session ID. Do not do this just to bypass an uncertain response, cooldown or attempt limit. Follow Resend and invalidate challenges for those separate actions; the application performs neither automatically.

Operational boundaries

Use a separate database for each API host/business, on persistent local disk. It stores the destination and original request but no actual code, hash or access token. Protect directory and backup access and define retention. The README explains the stored fields and recovery states. This terminal exercise exposes no browser routes. Adapting it to a web login requires trusted session binding, abuse controls, CSRF protection and secure session issuance with its own failure recovery. Keep expiry and attempt policy authoritative at OneFunnel; do not infer a fixed TTL from example timestamps.