Start here
HTTPS JSON API for sending and verifying SMS OTP codes. Base URL: https://auth.pratick.net
Typical flow
- Get keys from the client portal (API Keys tab) after your account is approved.
- POST /v1/send with { phone, message } — test with pk_test_* first.
- POST /v1/verify with { phone, code } when the user enters the SMS code.
Authentication
x-pratick-key: pk_test_YOUR_KEY # sandbox x-pratick-key: pk_live_YOUR_KEY # production
pk_test_* = no SMS, no charge. pk_live_* = real SMS. Keep live keys on your server only.
Prepaid credits are added in the client portal wallet — not through this API.
Which API do I need?
| Goal | Auth | Calls |
|---|---|---|
| Send & verify OTP from your backend | x-pratick-key (pk_test_* / pk_live_*) | POST /v1/send → POST /v1/verify |
| Portal / mobile app login | JWT from POST /api/v1/auth/login | signup → verify email → login → /auth/me |
| Check prepaid balance | API key or JWT | GET /wallet/balance or GET /api/v1/wallet/balance |
| OTP usage history | API key | GET /api/v1/otp-sends |
Try it (sandbox send)
curl -X POST "https://auth.pratick.net/v1/send" \
-H "Content-Type: application/json" \
-H "x-pratick-key: pk_test_YOUR_KEY" \
-d '{"phone":"+22890123456","message":"Login"}'Send OTP
Send a 6-digit SMS OTP. Live keys dispatch SMS and log the send; test keys simulate success without SMS or charge.
Auth x-pratick-key — pk_test_* or pk_live_* only
Body / query
| Field | In | Required | Notes |
|---|---|---|---|
phone |
body | Yes | E.164, e.g. +22890123456 |
message |
body | Yes | Your label for logs (OTP text is generated server-side) |
Responses
{
"status": "queued",
"messageId": "msg_…",
"cost_cfa": 25
}
{
"status": "delivered",
"mode": "test",
"messageId": "ts_…"
}
{
"success": false,
"error": "INSUFFICIENT_BALANCE"
}
{
"success": false,
"error": "DELIVERY_FAILED"
}
{
"error": "RATE_LIMITED",
"retryAfter": 42
}
Sandbox sends do not create rows you can verify — use pk_live_* when testing /v1/verify.
Example
curl -X POST "https://auth.pratick.net/v1/send" \
-H "Content-Type: application/json" \
-H "x-pratick-key: pk_live_YOUR_KEY" \
-d '{"phone":"+22890123456","message":"Login"}'const res = await fetch("https://auth.pratick.net/v1/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-pratick-key": "pk_live_YOUR_KEY"
},
body: JSON.stringify({
"phone": "+22890123456",
"message": "Login"
}),
});
const data = await res.json();
console.log(res.status, data);Verify OTP
Check the code against the latest pending send for that phone. Response success true|false — always HTTP 200 unless validation fails.
Auth x-pratick-key
Body / query
| Field | In | Required | Notes |
|---|---|---|---|
phone |
body | Yes | Same number as /v1/send |
code |
body | Yes | 6 digits |
Responses
{
"success": true,
"cost_cfa": 25
}
{
"success": false,
"cost_cfa": 25
}
{
"success": false,
"error": "OTP_EXPIRED"
}
{
"success": false,
"error": "MAX_ATTEMPTS"
}
{
"success": false,
"error": "Missing or invalid phone or code"
}
Example
curl -X POST "https://auth.pratick.net/v1/verify" \
-H "Content-Type: application/json" \
-H "x-pratick-key: pk_live_YOUR_KEY" \
-d '{"phone":"+22890123456","code":"482910"}'const res = await fetch("https://auth.pratick.net/v1/verify", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-pratick-key": "pk_live_YOUR_KEY"
},
body: JSON.stringify({
"phone": "+22890123456",
"code": "482910"
}),
});
const data = await res.json();
console.log(res.status, data);Message delivery status
Poll async send status for a queued OTP (tenant-scoped).
Auth x-pratick-key
Body / query
| Field | In | Required | Notes |
|---|---|---|---|
messageId |
path | Yes | From POST /v1/send |
Responses
{
"messageId": "msg_…",
"status": "queued|sent|delivered|failed",
"createdAt": "2026-07-16T…",
"deliveredAt": null
}
Example
curl -X GET "https://auth.pratick.net/v1/messages/msg_abc123" \ -H "Content-Type: application/json" \ -H "x-pratick-key: pk_live_YOUR_KEY"
const res = await fetch("https://auth.pratick.net/v1/messages/msg_abc123", {
method: "GET",
headers: {
"Content-Type": "application/json",
"x-pratick-key": "pk_live_YOUR_KEY"
},
});
const data = await res.json();
console.log(res.status, data);Login
Email + password → JWT (for portal/KYC routes, not for /v1/send).
Auth None
Body / query
| Field | In | Required | Notes |
|---|---|---|---|
email |
body | Yes | |
password |
body | Yes |
Responses
{
"success": true,
"accessToken": "…",
"user": {
"email": "…",
"clientId": "…"
}
}
Example
curl -X POST "https://auth.pratick.net/api/v1/auth/login" \
-H "Content-Type: application/json" \
-d '{"email":"you@company.com","password":"…"}'const res = await fetch("https://auth.pratick.net/api/v1/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
"email": "you@company.com",
"password": "…"
}),
});
const data = await res.json();
console.log(res.status, data);Register
Create account; verify email before login. Alias: /api/v1/auth/register.
Auth None
Body / query
| Field | In | Required | Notes |
|---|---|---|---|
companyName |
body | Yes | |
firstName |
body | Yes | |
lastName |
body | Yes | |
email |
body | Yes | |
password |
body | Yes | 8+ chars, upper, lower, special |
confirmPassword |
body | Yes | |
phone |
body | Yes | E.164 |
acceptTerms |
body | Yes | Must be true — user accepts Terms of Service and Privacy Policy |
Responses
{
"success": true,
"clientId": "PRAT-042",
"emailVerificationRequired": true
}
{
"error": "TERMS_NOT_ACCEPTED"
}
Example
curl -X POST "https://auth.pratick.net/api/v1/auth/signup" \
-H "Content-Type: application/json" \
-d '{"companyName":"Acme","firstName":"Ama","lastName":"K","email":"a@acme.tg","password":"…","confirmPassword":"…","phone":"+22890123456","acceptTerms":true}'const res = await fetch("https://auth.pratick.net/api/v1/auth/signup", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
"companyName": "Acme",
"firstName": "Ama",
"lastName": "K",
"email": "a@acme.tg",
"password": "…",
"confirmPassword": "…",
"phone": "+22890123456",
"acceptTerms": true
}),
});
const data = await res.json();
console.log(res.status, data);Accept Terms & Privacy
Record acceptance of the current Terms of Service and Privacy Policy (required after version updates).
Auth Authorization: Bearer <jwt>
Body / query
| Field | In | Required | Notes |
|---|---|---|---|
acceptTerms |
body | Yes | Must be true |
Responses
{
"success": true,
"termsVersionAccepted": "2026-07-20",
"privacyVersionAccepted": "2026-07-20"
}
{
"error": "TERMS_NOT_ACCEPTED"
}
Example
curl -X POST "https://auth.pratick.net/api/v1/auth/accept-terms" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_JWT" \
-d '{"acceptTerms":true}'const res = await fetch("https://auth.pratick.net/api/v1/auth/accept-terms", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_JWT"
},
body: JSON.stringify({
"acceptTerms": true
}),
});
const data = await res.json();
console.log(res.status, data);Current user
Profile, KYC status, balance — requires JWT.
Auth Authorization: Bearer <jwt>
Responses
{
"success": true,
"user": {
"email": "…",
"kycStatus": "APPROVED"
}
}
Example
curl -X GET "https://auth.pratick.net/api/v1/auth/me" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_JWT"
const res = await fetch("https://auth.pratick.net/api/v1/auth/me", {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_JWT"
},
});
const data = await res.json();
console.log(res.status, data);Prepaid balance
Read remaining CFA credits. Fund account in the client portal — not via API. JWT path: GET /api/v1/wallet/balance.
Auth API key or JWT
Responses
{
"walletBalanceCfa": 12500,
"currency": "XOF"
}
Example
curl -X GET "https://auth.pratick.net/wallet/balance" \ -H "Content-Type: application/json" \ -H "x-pratick-key: pk_live_YOUR_KEY"
const res = await fetch("https://auth.pratick.net/wallet/balance", {
method: "GET",
headers: {
"Content-Type": "application/json",
"x-pratick-key": "pk_live_YOUR_KEY"
},
});
const data = await res.json();
console.log(res.status, data);OTP history
Recent sends for your tenant. Query: limit (1–200). Export: …/export/excel or …/export/pdf.
Auth x-pratick-key
Body / query
| Field | In | Required | Notes |
|---|---|---|---|
limit |
query | No | Default 100 |
Responses
[
{
"sentAt": "…",
"country": "Togo",
"phoneMasked": "+228***3456",
"costCfa": 25,
"otpVerified": true
}
]
Example
curl -X GET "https://auth.pratick.net/api/v1/otp-sends" \ -H "Content-Type: application/json" \ -H "x-pratick-key: pk_live_YOUR_KEY"
const res = await fetch("https://auth.pratick.net/api/v1/otp-sends", {
method: "GET",
headers: {
"Content-Type": "application/json",
"x-pratick-key": "pk_live_YOUR_KEY"
},
});
const data = await res.json();
console.log(res.status, data);Support ticket
subject (1–200 chars) + message (1–8000 chars).
Auth x-pratick-key
Body / query
| Field | In | Required | Notes |
|---|---|---|---|
subject |
body | Yes | |
message |
body | Yes |
Responses
{
"success": true,
"ticketId": "tk_…"
}
Example
curl -X POST "https://auth.pratick.net/api/v1/support/ticket" \
-H "Content-Type: application/json" \
-H "x-pratick-key: pk_live_YOUR_KEY" \
-d '{"subject":"Delivery issue","message":"…"}'const res = await fetch("https://auth.pratick.net/api/v1/support/ticket", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-pratick-key": "pk_live_YOUR_KEY"
},
body: JSON.stringify({
"subject": "Delivery issue",
"message": "…"
}),
});
const data = await res.json();
console.log(res.status, data);Common errors
| Message | HTTP | What to do |
|---|---|---|
Unauthorized |
401 | Send pk_test_* or pk_live_* in x-pratick-key. |
Invalid API Key format. |
401 | /v1/send requires a gateway key — JWT alone is rejected. |
Invalid international phone number |
400 | Use E.164, e.g. +22890123456. |
Unsupported country or prefix |
400 | Destination not in coverage — see landing page. |
DELIVERY_FAILED |
502 | SMS not delivered; you are not billed. Retry. |
RATE_LIMITED |
429 | Too many sends — wait retryAfter seconds. |
INSUFFICIENT_BALANCE |
402 | Top up wallet before sending with pk_live_*. |
OTP_EXPIRED |
200 | Code TTL elapsed — call /v1/send again. |
MAX_ATTEMPTS |
200 | Too many wrong codes — request a new OTP. |
Missing or invalid phone or code |
400 | Verify body needs phone + 6-digit code. |