PRATICK Auth PRATICK API
OpenAPI Portal

Start here

HTTPS JSON API for sending and verifying SMS OTP codes. Base URL: https://auth.pratick.net

Typical flow

  1. Get keys from the client portal (API Keys tab) after your account is approved.
  2. POST /v1/send with { phone, message } — test with pk_test_* first.
  3. 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?

GoalAuthCalls
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"}'
POST /v1/send

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

FieldInRequiredNotes
phone body Yes E.164, e.g. +22890123456
message body Yes Your label for logs (OTP text is generated server-side)

Responses

202 — Live (queued)
{
  "status": "queued",
  "messageId": "msg_…",
  "cost_cfa": 25
}
200 — Sandbox (pk_test_*)
{
  "status": "delivered",
  "mode": "test",
  "messageId": "ts_…"
}
402 — Insufficient balance
{
  "success": false,
  "error": "INSUFFICIENT_BALANCE"
}
502 — Delivery failed
{
  "success": false,
  "error": "DELIVERY_FAILED"
}
429 — Rate limited
{
  "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);
POST /v1/verify

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

FieldInRequiredNotes
phone body Yes Same number as /v1/send
code body Yes 6 digits

Responses

200 — Match
{
  "success": true,
  "cost_cfa": 25
}
200 — No match
{
  "success": false,
  "cost_cfa": 25
}
200 — Expired
{
  "success": false,
  "error": "OTP_EXPIRED"
}
200 — Locked out
{
  "success": false,
  "error": "MAX_ATTEMPTS"
}
400 — Bad input
{
  "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);
GET /v1/messages/:messageId

Message delivery status

Poll async send status for a queued OTP (tenant-scoped).

Auth x-pratick-key

Body / query

FieldInRequiredNotes
messageId path Yes From POST /v1/send

Responses

200 — Status
{
  "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);
POST /api/v1/auth/login

Login

Email + password → JWT (for portal/KYC routes, not for /v1/send).

Auth None

Body / query

FieldInRequiredNotes
email body Yes
password body Yes

Responses

200 — OK
{
  "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);
POST /api/v1/auth/signup

Register

Create account; verify email before login. Alias: /api/v1/auth/register.

Auth None

Body / query

FieldInRequiredNotes
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

201 — Created
{
  "success": true,
  "clientId": "PRAT-042",
  "emailVerificationRequired": true
}
400 — Terms not accepted
{
  "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);
POST /api/v1/auth/accept-terms

Accept Terms & Privacy

Record acceptance of the current Terms of Service and Privacy Policy (required after version updates).

Auth Authorization: Bearer <jwt>

Body / query

FieldInRequiredNotes
acceptTerms body Yes Must be true

Responses

200 — OK
{
  "success": true,
  "termsVersionAccepted": "2026-07-20",
  "privacyVersionAccepted": "2026-07-20"
}
400 — Not accepted
{
  "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);
GET /api/v1/auth/me

Current user

Profile, KYC status, balance — requires JWT.

Auth Authorization: Bearer <jwt>

Responses

200 — OK
{
  "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);
GET /wallet/balance

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

200 — OK
{
  "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);
GET /api/v1/otp-sends

OTP history

Recent sends for your tenant. Query: limit (1–200). Export: …/export/excel or …/export/pdf.

Auth x-pratick-key

Body / query

FieldInRequiredNotes
limit query No Default 100

Responses

200 — OK
[
  {
    "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);
POST /api/v1/support/ticket

Support ticket

subject (1–200 chars) + message (1–8000 chars).

Auth x-pratick-key

Body / query

FieldInRequiredNotes
subject body Yes
message body Yes

Responses

201 — OK
{
  "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

MessageHTTPWhat 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.