OTP delivery and management
Every approval that moves money or changes risk posture is signed with a 6-digit OTP delivered to the checker's registered phone via Africa's Talking. The OTP is the legal signature of the checker — without it, no money moves.

How OTPs are generated
When a checker clicks Approve, the server calls Python's secrets.randbelow(1_000_000) to produce a 6-digit code. The plaintext is sent to Africa's Talking and immediately discarded from server memory. The SHA-256 hash plus a salt is stored on the mfi.dual.control.otp record.
When the checker enters their code, the server hashes the input and compares it constant-time to the stored hash. There is no "decrypt the OTP" — the plaintext truly does not exist on the server side after dispatch.
SMS delivery via Africa's Talking
Africa's Talking is the configured SMS gateway for Kenya (best deliverability, ~2 second median). The sender ID is the tenant's registered Sender ID (e.g. MWANANCHIMFI). The message template is:
Other channels supported (configurable per rule on the OTP tab): e-mail (slower, less safe), Twilio (fallback if Africa's Talking is down), Both (SMS + e-mail simultaneously). Voice call OTP is on the roadmap.
Your MWANANCHIMFI approval code is 738241. Do not share. Valid 10 min. Ref DCR-2026-02144.
Validation rules
- Exactly 6 numeric digits — anything else rejected client-side
- Lifetime 10 minutes from dispatch; configurable per rule but min 5 max 30
- 3 wrong attempts triggers 15-minute lockout on that OTP slot
- Resend after 60 seconds; new OTP invalidates the old one
- Re-using a successful OTP is rejected ("OTP already consumed")
- OTPs are scoped to a single request — you cannot use OTP from DCR-001 to approve DCR-002
OTP audit log
Under OTP menu you see every OTP dispatch in the last 90 days. Columns: dispatch time, user, channel, request ref, result (Verified / Expired / Wrong / Locked / Bypassed). This is read-only for everyone including admins — it cannot be edited or deleted.
External auditors typically sample 30-50 rows from this table for a compliance review. Provide them read access via the Auditor security group; they cannot mutate.
Emergency bypass procedure
Africa's Talking outages happen — about 2-3 times per year for short periods (15-90 min). Disbursements cannot wait. Bypass is the emergency escape valve.
The IT Admin or CTO opens OTP > Bypass. The screen asks for:
Bypass requires TWO authorisers (CTO + CEO, or CFO + CRO). Both sign in person at the same terminal. The bypass is logged with a BIG RED flag in Audit and triggers an automatic e-mail to the Internal Auditor.
- Request ID (e.g. DCR-2026-02144)
- Checker's user ID
- Bypass reason — free text, mandatory 50+ characters
- Bypass evidence attachment — typically a screenshot of the AT status page or a phone verification e-mail
Managing checker phone numbers
Phone numbers live on res.users.phone (or x_otp_phone if set — overrides res.users.phone). Updates require dual-control under the USR_PHONE_CHANGE rule (add this if not default). Format: +254XXXXXXXXX (E.164). Africa's Talking rejects local-format (07XXXXXXXX).
When a checker changes phones (new SIM, lost phone), they file a ticket with IT. IT updates the number, which triggers USR_PHONE_CHANGE dual-control. A test OTP is sent before the change is committed.
Worked scenarios
Scenario — Africa's Talking outage during salary day
| Character | Role |
|---|---|
| Kimani Mwangi | CTO |
| Samuel Kibet | CEO |
| Aisha Hassan | Branch Manager (checker, locked out) |
| Jane Wambui | Loan Officer (waiting) |
Timeline
- 13:43: Aisha tries to approve DCR-03101 — clicks Approve, no SMS (OTP dispatched OK from our side, but AT queue is stuck)
- 13:45: Aisha clicks Resend twice — still nothing (—)
- 13:47: Aisha phones IT — Kimani confirms AT outage on status page (—)
- 13:50: Kimani opens OTP > Bypass (Form requires CEO co-signature)
- 13:51: Samuel walks to Kimani's desk (—)
- 13:52: Kimani enters DCR-03101, Aisha's UID, reason: "AT outage 13:42 onwards, status page screenshot attached" (Attachment saved)
- 13:53: Samuel co-signs with his BridgeERP credentials (Bypass committed — RED FLAG in Audit)
- 13:53: Underlying disbursement fires (M-Pesa B2C sends KES 95,000 to borrower)
- 13:54 - 14:30: Same procedure for 11 more requests (All bypassed by CTO+CEO)
- 14:31: AT comes back; bypass mode disabled (Normal SMS flow resumes)
- Monday: Internal Audit reviews 12 bypassed requests — all verified legitimate (Audit memo filed)
Outcome — 12 legitimate disbursements processed during outage. Zero fraud. SASRA inspector at next visit confirms the bypass procedure is acceptable for documented outages.
Reference
OTP channel comparison
| Channel | Median delivery | Cost / OTP (KES) | Reliability | Recommended use |
|---|---|---|---|---|
| Africa's Talking SMS | 2 sec | 1.20 | 99.5% | Primary — default for Kenya |
| Twilio SMS | 4 sec | 3.50 | 99.7% | Fallback only |
| 30 sec | 0 | 97% (spam folder risk) | Low-risk rules or fallback | |
| Voice call (roadmap) | 10 sec | 8.00 | 99.9% | Future |
OTP configuration per rule
| Field | Default | Min | Max |
|---|---|---|---|
| Length (digits) | 6 | 6 | 8 |
| Lifetime (minutes) | 10 | 5 | 30 |
| Max attempts | 3 | 2 | 5 |
| Lockout (minutes) | 15 | 5 | 60 |
| Resend cooldown (seconds) | 60 | 30 | 120 |
| Channel | SMS | — | — |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| OTP arrives but says "Invalid" | Typo or expired (>10 min old). | Click Resend OTP. New code arrives; enter promptly. |
| No OTP ever arrives | AT outage, stale phone number, or carrier filtering (some networks block short-code sender IDs). | Resend after 60s. If repeat, IT verifies res.users.phone. If outage confirmed via AT status page, use Bypass. |
| Locked out for 15 minutes after 3 wrong attempts | User mistyped or copied wrong code. | Wait it out, or IT can clear the lockout via OTP > Reset lockout (logged). |
| OTP cost in Africa's Talking dashboard much higher than expected | Bypass cron is dispatching test OTPs to all checkers nightly (set up by a previous admin). | Settings > Scheduled Actions > MFI: OTP keepalive — disable. Test OTPs are not required. |
| Auditor wants OTP plaintext for forensic review | Plaintext is not stored — only the SHA-256 hash. | Explain the hash design. AT keeps a delivery log for 30 days — pull from there if needed for that window. |

