YeboID API Reference
RESTful API for authentication and identity management.
Base URL
| Environment | URL |
|---|---|
| Production | https://api.yeboid.com |
| Staging | https://api-staging.yeboid.com |
| Local | http://localhost:3000 |
Authentication
Public Endpoints
No authentication required:
POST /auth/*(except logout)GET /users/@:handleGET /users/handle/checkGET /health
Protected Endpoints
Require Bearer token in header:
Authorization: Bearer <access_token>Token Lifecycle
- Access Token: JWT, expires in 15 minutes
- Refresh Token: Opaque string, expires in 30 days
Request Format
Headers
Content-Type: application/json
Authorization: Bearer <token> (for protected routes)
X-Request-ID: <uuid> (optional, for tracing)Body
All request bodies are JSON.
Response Format
Success Response
{
"success": true,
"data": { ... }
}Error Response
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable message",
"details": { ... }
}
}Endpoints
Health Check
GET /health
Check API status.
Response:
{
"status": "ok",
"version": "1.0.0",
"timestamp": "2026-03-18T20:00:00Z"
}Authentication
Send OTP
POST /auth/otp/send
Send a one-time password to a phone number.
Request:
{
"phone": "+26878422613",
"purpose": "signup"
}| Field | Type | Required | Description |
|---|---|---|---|
| phone | string | Yes | E.164 format phone number |
| purpose | string | Yes | signup or pin_reset |
Response:
{
"success": true,
"data": {
"expires_in": 300,
"message": "OTP sent to +268****613"
}
}Errors:
| Code | Status | Description |
|---|---|---|
| INVALID_PHONE | 400 | Phone number format invalid |
| PHONE_EXISTS | 409 | Phone already registered (signup) |
| PHONE_NOT_FOUND | 404 | Phone not registered (pin_reset) |
| RATE_LIMITED | 429 | Too many OTP requests |
Verify OTP
POST /auth/otp/verify
Verify the OTP code. Returns a temporary token for signup/reset.
Request:
{
"phone": "+26878422613",
"code": "123456",
"purpose": "signup"
}Response:
{
"success": true,
"data": {
"verified": true,
"temp_token": "eyJ...",
"expires_in": 600
}
}Errors:
| Code | Status | Description |
|---|---|---|
| INVALID_OTP | 400 | Wrong or expired code |
| OTP_EXPIRED | 400 | Code has expired |
| TOO_MANY_ATTEMPTS | 429 | Max verification attempts |
Sign Up
POST /auth/signup
Create a new account after OTP verification.
Request:
{
"temp_token": "eyJ...",
"pin": "1234",
"handle": "laslie",
"name": "Laslie Georges Jr."
}| Field | Type | Required | Description |
|---|---|---|---|
| temp_token | string | Yes | From OTP verify |
| pin | string | Yes | 4-6 digits |
| handle | string | Yes | 3-30 chars, lowercase |
| name | string | No | Display name |
Response:
{
"success": true,
"data": {
"user": {
"id": "uuid",
"phone": "+26878422613",
"handle": "laslie",
"name": "Laslie Georges Jr.",
"avatar_url": null,
"kyc_status": "none",
"created_at": "2026-03-18T20:00:00Z"
},
"access_token": "eyJ...",
"refresh_token": "abc123...",
"expires_in": 900
}
}Errors:
| Code | Status | Description |
|---|---|---|
| INVALID_TEMP_TOKEN | 400 | Token invalid or expired |
| INVALID_PIN | 400 | PIN doesn't meet requirements |
| HANDLE_TAKEN | 409 | Handle already in use |
| HANDLE_RESERVED | 409 | Handle is reserved |
| HANDLE_INVALID | 400 | Handle format invalid |
Sign In
POST /auth/signin
Authenticate with phone and PIN.
Request:
{
"phone": "+26878422613",
"pin": "1234"
}Response:
{
"success": true,
"data": {
"user": {
"id": "uuid",
"phone": "+26878422613",
"handle": "laslie",
"name": "Laslie Georges Jr.",
"avatar_url": "https://...",
"kyc_status": "verified",
"created_at": "2026-03-18T20:00:00Z"
},
"access_token": "eyJ...",
"refresh_token": "abc123...",
"expires_in": 900
}
}Errors:
| Code | Status | Description |
|---|---|---|
| INVALID_CREDENTIALS | 401 | Wrong phone or PIN |
| ACCOUNT_LOCKED | 403 | Too many failed attempts |
| ACCOUNT_NOT_FOUND | 404 | Phone not registered |
Reset PIN
POST /auth/pin/reset
Set a new PIN after OTP verification.
Request:
{
"temp_token": "eyJ...",
"new_pin": "5678"
}Response:
{
"success": true,
"data": {
"message": "PIN reset successfully",
"access_token": "eyJ...",
"refresh_token": "abc123...",
"expires_in": 900
}
}Refresh Token
POST /auth/refresh
Get a new access token using refresh token.
Request:
{
"refresh_token": "abc123..."
}Response:
{
"success": true,
"data": {
"access_token": "eyJ...",
"refresh_token": "def456...",
"expires_in": 900
}
}Note: Refresh token is rotated (old one invalidated).
Errors:
| Code | Status | Description |
|---|---|---|
| INVALID_REFRESH_TOKEN | 401 | Token invalid or revoked |
| REFRESH_TOKEN_EXPIRED | 401 | Token has expired |
Logout
POST /auth/logout
Revoke the current refresh token.
Headers: Authorization: Bearer <access_token>
Request:
{
"refresh_token": "abc123..."
}Response:
{
"success": true,
"data": {
"message": "Logged out successfully"
}
}Logout All Sessions
POST /auth/logout/all
Revoke all refresh tokens for the user.
Headers: Authorization: Bearer <access_token>
Response:
{
"success": true,
"data": {
"message": "All sessions revoked",
"sessions_revoked": 5
}
}Users
Get Current User
GET /users/me
Get the authenticated user's profile.
Headers: Authorization: Bearer <access_token>
Response:
{
"success": true,
"data": {
"id": "uuid",
"phone": "+26878422613",
"phone_verified": true,
"handle": "laslie",
"name": "Laslie Georges Jr.",
"avatar_url": "https://...",
"bio": "Building the future of Africa",
"country": "SZ",
"language": "en",
"kyc_status": "verified",
"created_at": "2026-03-18T20:00:00Z",
"updated_at": "2026-03-18T20:00:00Z"
}
}Update Profile
PATCH /users/me
Update the authenticated user's profile.
Headers: Authorization: Bearer <access_token>
Request:
{
"name": "Laslie G.",
"bio": "CEO @ Omevision",
"avatar_url": "https://...",
"language": "en"
}All fields are optional. Only include fields to update.
Response:
{
"success": true,
"data": {
"id": "uuid",
"handle": "laslie",
"name": "Laslie G.",
"bio": "CEO @ Omevision",
...
}
}Delete Account
DELETE /users/me
Permanently delete the user's account.
Headers: Authorization: Bearer <access_token>
Request:
{
"pin": "1234",
"confirmation": "DELETE MY ACCOUNT"
}Response:
{
"success": true,
"data": {
"message": "Account deleted",
"deleted_at": "2026-03-18T20:00:00Z"
}
}Note: This is irreversible. Data is hard-deleted after 30 days.
Get User by Handle
GET /users/@:handle
Get a user's public profile by handle.
Response:
{
"success": true,
"data": {
"id": "uuid",
"handle": "laslie",
"name": "Laslie Georges Jr.",
"avatar_url": "https://...",
"bio": "Building the future of Africa",
"kyc_status": "verified",
"created_at": "2026-03-18T20:00:00Z"
}
}Note: Phone, country, language are NOT included (private).
Check Handle Availability
GET /users/handle/check?handle=<handle>
Check if a handle is available.
Response (available):
{
"success": true,
"data": {
"handle": "newhandle",
"available": true
}
}Response (taken):
{
"success": true,
"data": {
"handle": "laslie",
"available": false,
"reason": "taken"
}
}Response (reserved):
{
"success": true,
"data": {
"handle": "admin",
"available": false,
"reason": "reserved"
}
}Change Handle
POST /users/handle/change
Change the user's handle.
Headers: Authorization: Bearer <access_token>
Request:
{
"new_handle": "laslie_ceo",
"pin": "1234"
}Response:
{
"success": true,
"data": {
"old_handle": "laslie",
"new_handle": "laslie_ceo",
"next_change_available": "2026-04-18T20:00:00Z"
}
}Errors:
| Code | Status | Description |
|---|---|---|
| HANDLE_COOLDOWN | 429 | Must wait 30 days between changes |
| HANDLE_TAKEN | 409 | Handle already in use |
Sessions
List Sessions
GET /sessions
List all active sessions for the user.
Headers: Authorization: Bearer <access_token>
Response:
{
"success": true,
"data": {
"sessions": [
{
"id": "uuid",
"device_name": "iPhone 15 Pro",
"platform": "ios",
"ip_address": "102.xxx.xxx.xxx",
"last_used_at": "2026-03-18T20:00:00Z",
"created_at": "2026-03-15T10:00:00Z",
"current": true
},
{
"id": "uuid2",
"device_name": "Chrome on Windows",
"platform": "web",
"ip_address": "105.xxx.xxx.xxx",
"last_used_at": "2026-03-17T15:00:00Z",
"created_at": "2026-03-10T08:00:00Z",
"current": false
}
],
"total": 2
}
}Revoke Session
DELETE /sessions/:id
Revoke a specific session.
Headers: Authorization: Bearer <access_token>
Response:
{
"success": true,
"data": {
"message": "Session revoked"
}
}KYC
Get KYC Status
GET /kyc/status
Get the user's KYC verification status.
Headers: Authorization: Bearer <access_token>
Response:
{
"success": true,
"data": {
"status": "verified",
"verified_at": "2026-03-18T20:00:00Z",
"level": "standard",
"documents": ["id_card"]
}
}Initiate KYC
POST /kyc/initiate
Start the KYC verification process.
Headers: Authorization: Bearer <access_token>
Response:
{
"success": true,
"data": {
"verification_url": "https://verify.yeboid.com/session/abc123",
"expires_in": 1800
}
}User is redirected to YeboVerify to complete verification.
Error Codes
| Code | HTTP Status | Description |
|---|---|---|
| INVALID_REQUEST | 400 | Malformed request body |
| INVALID_PHONE | 400 | Phone number format invalid |
| INVALID_PIN | 400 | PIN doesn't meet requirements |
| INVALID_HANDLE | 400 | Handle format invalid |
| INVALID_OTP | 400 | OTP code is wrong |
| OTP_EXPIRED | 400 | OTP code has expired |
| INVALID_CREDENTIALS | 401 | Wrong phone or PIN |
| INVALID_TOKEN | 401 | Access token invalid |
| TOKEN_EXPIRED | 401 | Access token expired |
| INVALID_REFRESH_TOKEN | 401 | Refresh token invalid |
| ACCOUNT_LOCKED | 403 | Account temporarily locked |
| FORBIDDEN | 403 | Not allowed to perform action |
| NOT_FOUND | 404 | Resource not found |
| PHONE_NOT_FOUND | 404 | Phone number not registered |
| HANDLE_TAKEN | 409 | Handle already in use |
| HANDLE_RESERVED | 409 | Handle is reserved |
| PHONE_EXISTS | 409 | Phone already registered |
| HANDLE_COOLDOWN | 429 | Must wait between handle changes |
| RATE_LIMITED | 429 | Too many requests |
| INTERNAL_ERROR | 500 | Server error |
Rate Limits
| Endpoint | Limit | Window |
|---|---|---|
| POST /auth/otp/send | 3 | 1 hour |
| POST /auth/signin | 5 | 15 minutes |
| POST /auth/otp/verify | 5 | per code |
| GET /users/handle/check | 30 | 1 minute |
| All other endpoints | 100 | 1 minute |
Rate limit headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1710770000Webhooks
See WEBHOOKS.md for webhook events.
API Version: 1.0Last updated: March 18, 2026