CustomerIdentity is a person, keyed by phone number, living in the core schema — deliberately not scoped to any tenant. It owns saved payment methods that work everywhere.
Why phone, why WhatsApp
Email is a weak identity in Nigeria — barely checked, easily mistyped. The phone number is the real primary key for a person, and WhatsApp is the channel everyone has and trusts. So Duro verifies ownership of the phone with a one-time code over the WhatsApp Cloud API, and the verified phone becomes the wallet’s key.The OTP flow
Security properties, all real:- The code is never stored in clear — only
sha256(code). Verification is a timing-safe hash compare (Hash.verifyHashedSecret). - Brute force is bounded — 6 digits × 5 attempts per challenge × 5 challenges per 15 minutes is negligible search space, and rate-limited besides.
- Codes expire in 10 minutes and are single-use (
consumedAt). - The identity token is marked
keyId: identityso it can’t masquerade as a tenant credential — present it to/v1and it resolves to an identity id, not a tenant, and fails. See request lifecycle →
The cross-merchant wallet
ASavedPaymentMethod hangs off the identity, not a tenant. The presenter exposes only rail, brand, and last4 — the gateway token is never returned over the API. When the customer pays at any merchant, Duro links a tenant-scoped Customer to the same identity by phone, so the merchant sees their customer while the wallet stays shared.