---
title: "One-time passwords"
description: "Generate and verify one-time passwords for secure authentication and verification flows."
canonical: "https://studio.inttegro.com/api/otps"
format: inttegro-machine-docs/v1
language: "Python"
sdk_version: "v12.0.0"
reader_profile: optional
reader_manifest: "https://studio.inttegro.com/machine/python/readers/index.json"
reader_identification: "Optional: declare your client only if known and your current-session capabilities; use reader_manifest for supplemental recommendations."
preamble_estimated_tokens: 494
model_token_report: "https://studio.inttegro.com/machine/token-report.json"
preamble_questions: 5
preamble_source: "machine-only-decisions"
recommended_mcp_tools: []
estimated_tokens: 5412
token_estimate_method: "utf8_bytes_divided_by_4"
---

## Machine preamble

**Summary:** Generate and verify one-time passwords for secure authentication and verification flows. This Python representation uses the selected SDK's native names wherever Studio has a verified mapping.

### Five questions

#### Q1. Is One-time passwords the right source for this task?

Use this reference for generate and verify one-time passwords for secure authentication and verification flows. Keep the four decision rules below with its exact operations and fields; do not infer a broader workflow from endpoint order.

#### Q2. Does a successful verify request prove the code matched?

**HTTP success is not proof that the code matched.** Verify returns a recorded
attempt for both matching and non-matching tokens. Continue the protected
action only when [`verification_attempt.result.verdict`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Verification) is [`inttegro.otp.VerificationVerdict.PASS`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#VerificationVerdict.PASS).

#### Q3. How is verification bound to the initiated transaction?

**Bind verification to the initiated transaction.** Submit the returned
transaction ID, the same normalized recipient, and the customer-entered
token. Do not substitute a new recipient or infer success from delivery
status.

#### Q4. How should token material be handled?

**Treat token material as sensitive.** Initiation and lookup never return the
generated code. Do not log the submitted or presented token, include it in
analytics, or place it in model context.

#### Q5. What is the difference between delivery and verification?

**Distinguish delivery from verification.** Pending delivery means
transmission details may not exist yet; pending verification means the SMS
gateway or email provider accepted the message and the transaction awaits an
attempt. Use [Chimes](https://studio.inttegro.com/machine/python/api/chimes.md) for ordinary messaging, not OTP delivery
semantics.

## Document content

# One-time passwords (OTPs)

Use One-Time Passwords (OTPs) to confirm that a customer controls a phone number
or email address
before you sign them in, change sensitive account data, or approve an action.
Inttegro generates and sends the time-limited code; your application should
continue only after verification returns a [`inttegro.otp.VerificationVerdict.PASS`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#VerificationVerdict.PASS) verdict.

## Authorize only from the verification verdict

The four decision rules are in the machine preamble above.

## The OTP transaction object

An OTP transaction represents one code from generation through delivery and
verification. Initiation and lookup responses never expose the generated code:
collect the code from the customer, then submit that value to the verify endpoint.

### Properties

- [`cancel_reason`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Transaction) — `string`

  Explanation for why this transaction was canceled. Omitted unless
  [`transaction.status`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Transaction) is
  [`inttegro.otp.Status.CANCELED`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Status.CANCELED).

- [`canceled_at`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Transaction) — `timestamp`

  When this transaction was canceled. Omitted unless
  [`transaction.status`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Transaction) is
  [`inttegro.otp.Status.CANCELED`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Status.CANCELED).

- [`expires_at`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Transaction) — `timestamp`

  When this token expires and can no longer be verified.

- [`full_message`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Transaction) — `string`

  The message template with the service name substituted and the token
  placeholder preserved as `'{token}'`. The token value is
  not included in this field.

- [`id`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Transaction) — `string`

  Unique identifier for this transaction—use it to verify tokens and lookup
  transaction status.

- [`initiated_at`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Transaction) — `timestamp`

  When this transaction was created and token generation began.

- [`status`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Transaction) — `string`

  Current transaction status. Possible values are
  [`inttegro.otp.Status.CANCELED`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Status.CANCELED),
  [`inttegro.otp.Status.EXPIRED`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Status.EXPIRED),
  [`inttegro.otp.Status.PENDING`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Status.PENDING),
  [`inttegro.otp.Status.PENDING_DELIVERY`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Status.PENDING_DELIVERY),
  [`inttegro.otp.Status.PENDING_VERIFICATION`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Status.PENDING_VERIFICATION), and
  [`inttegro.otp.Status.VERIFIED`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Status.VERIFIED).
  [`inttegro.otp.Status.PENDING_DELIVERY`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Status.PENDING_DELIVERY) means
  delivery-attempt fields are not available yet;
  [`inttegro.otp.Status.PENDING_VERIFICATION`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Status.PENDING_VERIFICATION) means the message
  was sent and the transaction is waiting for a verification attempt.

- [`transmission`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Transaction)

  Delivery details for the OTP message. Recipient and sender are always present; attempt fields are omitted until delivery begins.

  - [`transmission.recipient`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Transaction) — `string`

    Normalized international phone number or bare email address targeted by delivery.

  - [`transmission.sender_id`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Transaction) — `string`

    SMS sender identifier or noreply@inttegro.com for email OTP.

  - [`transmission.sent_at`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Transaction) — `timestamp`

    When the message was sent. Omitted until the delivery attempt.

  - [`transmission.sent_via`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Transaction) — `string`

    Delivery mechanism: `'sms'` or `'email'`. Omitted until the delivery attempt starts.

  - [`transmission.status`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Transaction) — `string`

    Delivery status: [`inttegro.otp.TransmissionStatus.SUBMITTED`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#TransmissionStatus.SUBMITTED), [`inttegro.otp.TransmissionStatus.DELIVERED`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#TransmissionStatus.DELIVERED), or [`inttegro.otp.TransmissionStatus.FAILED`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#TransmissionStatus.FAILED) for SMS. accepted means an email provider accepted the send, not that the inbox received it. Omitted until the delivery attempt.

## Initiate OTP transaction

Generate a one-time password, deliver it by SMS to an international phone
number or by email to a bare email address, and return a typed transaction
ID for verification.

### Required attributes

- [`recipient`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#InitiateRequest.recipient) — `str`

  International phone number that will receive SMS, such as
  `+233241234567`, or a bare email address. The transport
  is inferred unless specified.

- [`service_name`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#InitiateRequest.service_name) — `str`

  Service or application name substituted for
  `'{service}'` in the message template. Length: 2-20
  characters.

- [`purpose`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#InitiateRequest.purpose) — `Literal['account_creation', 'account_recovery', 'email_verification', 'financial_account_verification', 'password_reset', 'payment_confirmation', 'payment_method_verification', 'payout_confirmation', 'phone_verification', 'sensitive_action', 'sign_in', 'transaction_confirmation', 'unspecified', OTPPurpose.ACCOUNT_CREATION, OTPPurpose.ACCOUNT_RECOVERY, OTPPurpose.EMAIL_VERIFICATION, OTPPurpose.FINANCIAL_ACCOUNT_VERIFICATION, OTPPurpose.PASSWORD_RESET, OTPPurpose.PAYMENT_CONFIRMATION, OTPPurpose.PAYMENT_METHOD_VERIFICATION, OTPPurpose.PAYOUT_CONFIRMATION, OTPPurpose.PHONE_VERIFICATION, OTPPurpose.SENSITIVE_ACTION, OTPPurpose.SIGN_IN, OTPPurpose.TRANSACTION_CONFIRMATION, OTPPurpose.UNSPECIFIED]` — required

  Intent of the verification flow. Supported values are
  `account_creation`,
  `account_recovery`,
  `email_verification`,
  `financial_account_verification`,
  `password_reset`,
  `payment_confirmation`,
  `payment_method_verification`,
  `payout_confirmation`,
  `phone_verification`,
  `sensitive_action`,
  `sign_in`,
  `transaction_confirmation`, and
  `unspecified`. Unsupported values are rejected. Use
  `unspecified` explicitly for a general verification
  request.

- [`token_size`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#InitiateRequest.token_size) — `int`

  Generated token length. The current API requires a value from 5 through 10, inclusive.

### Optional attributes

- [`async_delivery`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#InitiateRequest.async_delivery) — `bool`

  When `False` or omitted, the request waits for the
  initial transmission attempt. When `True`, the
  transaction can be returned before transmission details are available,
  with status [`inttegro.otp.Status.PENDING_DELIVERY`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Status.PENDING_DELIVERY).

- [`message_template`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#InitiateRequest.message_template) — `str`

  Custom SMS body or email copy. It must include
  `'{token}'` exactly once and can include
  `'{service}'`. Email sends it as the plain-text
  fallback and renders the copy around the code in a branded HTML layout.
  For email, custom copy overrides the purpose-specific default body, but
  the selected purpose still controls the subject and heading. If omitted,
  the API uses its transport-specific purpose-aware template.

- [`sender`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#InitiateRequest.sender) — `str`

  SMS sender identifier (3-12 characters). Email OTP always uses
  `'{service_name} <noreply@inttegro.com>'`; omit this
  field for email.

- `preferred_gateway` — `string` — wire-only

  Optional SMS gateway. Do not supply it for email OTP; email uses the
  configured default provider.

- `transport` — `enum` — wire-only

  Optional `'sms'` or `'email'`. A phone
  recipient selects SMS and an email recipient selects email when omitted.

- [`token_alphabet`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#InitiateRequest.token_alphabet) — `str`

  Custom alphabet for token generation. Pass a string of characters to
  use (for example, `'0123456789ABCDEF'` for hexadecimal
  tokens). Mutually exclusive with
  [`token_alphabet_type`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#InitiateRequest.token_alphabet_type)—use one or the other, not both.

- [`token_alphabet_type`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#InitiateRequest.token_alphabet_type) — `Literal['numeric', 'alpha', 'alphanumeric', OTPAlphabetType.NUMERIC, OTPAlphabetType.ALPHA, OTPAlphabetType.ALPHANUMERIC]`

  Predefined alphabet type for token generation. Supported values are
  [`inttegro.otp.AlphabetType.ALPHA`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#AlphabetType.ALPHA),
  [`inttegro.otp.AlphabetType.ALPHANUMERIC`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#AlphabetType.ALPHANUMERIC), and
  [`inttegro.otp.AlphabetType.NUMERIC`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#AlphabetType.NUMERIC). If neither alphabet
  field is supplied, the default alphabet is alphanumeric. Mutually
  exclusive with [`token_alphabet`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#InitiateRequest.token_alphabet).

- [`validity_duration_in_minutes`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#InitiateRequest.validity_duration_in_minutes) — `int`

  How long the token remains valid. Accepted values are 3 through 10,080 minutes (7 days), inclusive. Defaults to 10 minutes.

The fully rendered SMS, including the generated code and substituted service
name, must be no longer than 120 characters; email text is limited to 2,048
characters. The transaction expires after its validity window even if
delivery is delayed.

For email OTP, send a bare email recipient and omit the SMS-only
`preferred_gateway` field. The
`'{service_name} <noreply@inttegro.com>'` From mailbox and
SES provider are selected automatically:

```json
{
  "recipient": "customer@example.com",
  "service_name": "Inttegro",
  "token_size": 6,
  "purpose": "sign_in"
}
```

The verification request uses the returned transaction ID, this same
recipient, and the code supplied by the customer.

### Response

With synchronous delivery, inspect [`transaction.transmission.status`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Transaction): [`inttegro.otp.TransmissionStatus.SUBMITTED`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#TransmissionStatus.SUBMITTED)  means the SMS message was submitted for delivery, [`inttegro.otp.TransmissionStatus.DELIVERED`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#TransmissionStatus.DELIVERED)  means delivery was confirmed, and [`inttegro.otp.TransmissionStatus.FAILED`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#TransmissionStatus.FAILED)  means the delivery attempt failed. For email, `accepted`  means SES accepted the send; it does not prove inbox delivery. **Request — POST /otp/initiate**

```python
import os

import inttegro
from inttegro import InttegroClient

client = InttegroClient(api_key=os.environ["INTTEGRO_API_KEY"])

result = client.otp.initiate(inttegro.otp.InitiateRequest(
  message_template="Your {service} code is {token}.",
  purpose="transaction_confirmation",
  recipient="+233241234567",
  sender="Acme",
  service_name="Acme Bank",
  token_alphabet_type="numeric",
  token_size=6,
  validity_duration_in_minutes=10,
))
```

```json
{
  "transaction": {
    "expires_at": "2026-08-28T10:10:00Z",
    "full_message": "Your Acme Bank code is {token}.",
    "id": "ot_<TRANSACTION_ID>",
    "initiated_at": "2026-08-28T10:00:00Z",
    "status": "pending_verification",
    "transmission": {
      "recipient": "+233241234567",
      "sender_id": "Acme",
      "sent_at": "2026-08-28T10:00:01Z",
      "sent_via": "sms",
      "status": "submitted"
    }
  }
}
```

## Verify OTP

Record a verification attempt for a user-submitted token. An HTTP 200
response means the attempt was recorded; it does not mean the token
matched. Authorize the user or action only when
[`verification_attempt.result.verdict`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Verification) is
[`inttegro.otp.VerificationVerdict.PASS`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#VerificationVerdict.PASS).

### Required attributes

- [`recipient`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#VerifyRequest.recipient) — `str`

  Phone number or bare email address that received the OTP. Must match the recipient and stored transport from initiation.

- [`token`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#VerifyRequest.token) — `str`

  The OTP token submitted by the user. This is the code they received by SMS or email. Cannot be blank or whitespace-only.

- [`transaction_id`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#VerifyRequest.transaction_id) — `str`

  Typed ID returned from `/otp/initiate`, in the form
  `ot_<TRANSACTION_ID>`.

The current transaction accepts at most five recorded attempts; after that,
initiate a new transaction.

### Response

Returns both [`transaction`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Verification) and
[`verification_attempt`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Verification). The attempt contains
[`verification_attempt.attempted_at`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Verification),
[`verification_attempt.id`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Verification),
[`verification_attempt.presented_token`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Verification),
[`verification_attempt.recipient`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Verification), and
[`verification_attempt.result`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Verification).
[`verification_attempt.result.verdict`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Verification) is [`inttegro.otp.VerificationVerdict.PASS`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#VerificationVerdict.PASS)  or
[`inttegro.otp.VerificationVerdict.FAIL`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#VerificationVerdict.FAIL); [`verification_attempt.result.detail`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Verification) is omitted for
a passing result and explains a failed match when available. Treat
[`verification_attempt.presented_token`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Verification) as sensitive and
exclude verification request and response bodies from logs.
Matching and non-matching tokens both produce a successful response. Use
[`verification_attempt.result.verdict`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Verification) to determine whether
verification passed.

**Request — POST /otp/verify**

```python
import os

import inttegro
from inttegro import InttegroClient

client = InttegroClient(api_key=os.environ["INTTEGRO_API_KEY"])

result = client.otp.verify(inttegro.otp.VerifyRequest(
  recipient="+233241234567",
  token="<OTP_CODE_FROM_USER>",
  transaction_id="ot_<TRANSACTION_ID>",
))
```

A failed match still returns HTTP 200 with a [`inttegro.otp.VerificationVerdict.FAIL`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#VerificationVerdict.FAIL)  verdict. The API echoes the submitted value as
[`verification_attempt.presented_token`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#Verification); the example uses
a placeholder so no real code is exposed.

```json
{
  "verification_attempt": {
    "attempted_at": "2026-08-28T10:03:00Z",
    "id": "ov_<ATTEMPT_ID>",
    "presented_token": "<OTP_CODE_FROM_USER>",
    "recipient": "+233241234567",
    "result": {
      "detail": "token_mismatch",
      "verdict": "fail"
    }
  },
  "transaction": {
    "expires_at": "2026-08-28T10:10:00Z",
    "full_message": "Your Acme Bank code is {token}.",
    "id": "ot_<TRANSACTION_ID>",
    "initiated_at": "2026-08-28T10:00:00Z",
    "status": "pending_verification",
    "transmission": {
      "recipient": "+233241234567",
      "sender_id": "Acme",
      "sent_at": "2026-08-28T10:00:01Z",
      "sent_via": "sms",
      "status": "submitted"
    }
  }
}
```

## Lookup OTP transaction

Retrieve details of an existing OTP transaction by its ID. Check transaction status, delivery state, and verification status.

### Required attributes

- [`transaction_id`](https://python.inttegro.dev/v12.0.0/inttegro/otp.html#LookupRequest.transaction_id) — `str`

  Typed OTP transaction ID to look up.

### Response

Returns the transaction without the generated code or verification-attempt
history. Optional cancellation and delivery-attempt fields are omitted when
they do not apply.

**Request — POST /otp/lookup**

```python
import os

import inttegro
from inttegro import InttegroClient

client = InttegroClient(api_key=os.environ["INTTEGRO_API_KEY"])

result = client.otp.lookup(inttegro.otp.LookupRequest(
  transaction_id="ot_<TRANSACTION_ID>",
))
```

```json
{
  "transaction": {
    "expires_at": "2026-08-28T10:10:00Z",
    "full_message": "Your Acme Bank code is {token}.",
    "id": "ot_<TRANSACTION_ID>",
    "initiated_at": "2026-08-28T10:00:00Z",
    "status": "pending_verification",
    "transmission": {
      "recipient": "+233241234567",
      "sender_id": "Acme",
      "sent_at": "2026-08-28T10:00:01Z",
      "sent_via": "sms",
      "status": "submitted"
    }
  }
}
```

## Related resources

- [Create customer](https://studio.inttegro.com/machine/python/api/customers.md#create-customer) - Associate verified contact details with a customer record.
- [Send Chime](https://studio.inttegro.com/machine/python/api/chimes.md#send-chime) - Send transactional SMS and email notifications after verification.
