OTP flow
An OAuth access token authenticates your integration. For certain operations — completing a pre-authorization, finalising a dispensation, approving a settlement — the sponsor's rules require an additional confirmation from the beneficiary. That confirmation is an OTP (one-time password): a short code the sponsor delivers to the beneficiary by SMS or email, and that the beneficiary reads aloud (or types in) at the provider's counter.
Two public endpoints back this flow:
| Endpoint | Purpose |
|---|---|
POST /v1/otp/send | Ask the sponsor to send an OTP to the beneficiary. |
POST /v1/otp/validate | Submit the code the beneficiary provided; get back an OTP-verification token. |
The verification token you get from validateOtp is what you attach to the next sensitive operation to prove the OTP challenge was cleared.
When you'll need OTP
You don't decide when to run OTP — the sponsor does. Two signals tell you:
- Sponsor configuration —
getSponsorConfigurationlists which operations require OTP for a given(sponsor, product_type)pair. - Verification status endpoints — during a dispensation,
getDispensationVerificationStatustells you whether OTP is required for this specific dispensation.
If OTP isn't required, skip the flow entirely. If it is, run send → validate → attach.
Flow
1. Send
curl -X POST https://sandbox.osigu.com/v1/otp/send \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"beneficiary_id": "bnf_01HW8GQ2KMNP3F4R5T6Y7U8I9O",
"sponsor_slug": "sponsor-sandbox",
"channel": "SMS"
}'
Response:
{
"otp_id": "otp_01HX9NPQ12ABCDEFGH34567890",
"channel": "SMS",
"expires_in": 300,
"masked_destination": "+57 300 *** ** 42"
}
otp_id— the identifier you'll reference invalidate.expires_in— how long (seconds) the OTP is valid. 300 (5 minutes) is typical.masked_destination— a hint you can show to the user ("we sent a code to +57 300 *** ** 42"). Never expose the full number/email.
2. Validate
Once the beneficiary reads the code, submit it:
curl -X POST https://sandbox.osigu.com/v1/otp/validate \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"otp_id": "otp_01HX9NPQ12ABCDEFGH34567890",
"code": "482913"
}'
Response:
{
"otp_verification_token": "otvt_01HX9NR3ZQABCD...",
"expires_in": 600
}
The verification token is opaque — treat it as a black box. It's valid for a short window (10 minutes typical) and is single-use.
3. Attach
Pass otp_verification_token on the sensitive operation. Where it goes depends on the endpoint — usually as a request-body field or, for dispensations, on the completion call.
Errors
| Status | error_code | Meaning |
|---|---|---|
400 | 071-201 | Malformed request — missing beneficiary_id or channel. |
403 | 071-202 | Sponsor doesn't allow OTP on this operation, or provider not authorised. |
404 | 071-203 | otp_id not found or already consumed. |
409 | 071-204 | OTP already validated — reuse the existing verification token or start over. |
410 | 071-205 | OTP expired. Call sendOtp again. |
422 | 071-206 | Wrong code. The beneficiary can retry (typically 3 attempts before the OTP is invalidated). |
429 | 071-207 | Too many sendOtp calls for this beneficiary. Back off. |
Channels
Supported values for channel:
SMS— default; SMS to the beneficiary's on-file phone.EMAIL— for beneficiaries whose sponsor stores an email of record.
The sponsor decides which channels it supports. If you request an unsupported channel you get 422.
Rate limits
- Per beneficiary: max 5 OTP sends per hour, per sponsor.
- Per client: standard 50 req/min applies to both
sendandvalidate.
Related concepts
- Dispensing — the main consumer of OTP-verification tokens.
- Obtaining tokens — the OAuth token is still required; OTP is in addition to it.
- Sponsors — whether OTP is required is a sponsor-driven rule.