Skip to content

YeboID API Reference

RESTful API for authentication and identity management.


Base URL

EnvironmentURL
Productionhttps://api.yeboid.com
Staginghttps://api-staging.yeboid.com
Localhttp://localhost:3000

Authentication

Public Endpoints

No authentication required:

  • POST /auth/* (except logout)
  • GET /users/@:handle
  • GET /users/handle/check
  • GET /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

json
{
  "success": true,
  "data": { ... }
}

Error Response

json
{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable message",
    "details": { ... }
  }
}

Endpoints


Health Check

GET /health

Check API status.

Response:

json
{
  "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:

json
{
  "phone": "+26878422613",
  "purpose": "signup"
}
FieldTypeRequiredDescription
phonestringYesE.164 format phone number
purposestringYessignup or pin_reset

Response:

json
{
  "success": true,
  "data": {
    "expires_in": 300,
    "message": "OTP sent to +268****613"
  }
}

Errors:

CodeStatusDescription
INVALID_PHONE400Phone number format invalid
PHONE_EXISTS409Phone already registered (signup)
PHONE_NOT_FOUND404Phone not registered (pin_reset)
RATE_LIMITED429Too many OTP requests

Verify OTP

POST /auth/otp/verify

Verify the OTP code. Returns a temporary token for signup/reset.

Request:

json
{
  "phone": "+26878422613",
  "code": "123456",
  "purpose": "signup"
}

Response:

json
{
  "success": true,
  "data": {
    "verified": true,
    "temp_token": "eyJ...",
    "expires_in": 600
  }
}

Errors:

CodeStatusDescription
INVALID_OTP400Wrong or expired code
OTP_EXPIRED400Code has expired
TOO_MANY_ATTEMPTS429Max verification attempts

Sign Up

POST /auth/signup

Create a new account after OTP verification.

Request:

json
{
  "temp_token": "eyJ...",
  "pin": "1234",
  "handle": "laslie",
  "name": "Laslie Georges Jr."
}
FieldTypeRequiredDescription
temp_tokenstringYesFrom OTP verify
pinstringYes4-6 digits
handlestringYes3-30 chars, lowercase
namestringNoDisplay name

Response:

json
{
  "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:

CodeStatusDescription
INVALID_TEMP_TOKEN400Token invalid or expired
INVALID_PIN400PIN doesn't meet requirements
HANDLE_TAKEN409Handle already in use
HANDLE_RESERVED409Handle is reserved
HANDLE_INVALID400Handle format invalid

Sign In

POST /auth/signin

Authenticate with phone and PIN.

Request:

json
{
  "phone": "+26878422613",
  "pin": "1234"
}

Response:

json
{
  "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:

CodeStatusDescription
INVALID_CREDENTIALS401Wrong phone or PIN
ACCOUNT_LOCKED403Too many failed attempts
ACCOUNT_NOT_FOUND404Phone not registered

Reset PIN

POST /auth/pin/reset

Set a new PIN after OTP verification.

Request:

json
{
  "temp_token": "eyJ...",
  "new_pin": "5678"
}

Response:

json
{
  "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:

json
{
  "refresh_token": "abc123..."
}

Response:

json
{
  "success": true,
  "data": {
    "access_token": "eyJ...",
    "refresh_token": "def456...",
    "expires_in": 900
  }
}

Note: Refresh token is rotated (old one invalidated).

Errors:

CodeStatusDescription
INVALID_REFRESH_TOKEN401Token invalid or revoked
REFRESH_TOKEN_EXPIRED401Token has expired

Logout

POST /auth/logout

Revoke the current refresh token.

Headers: Authorization: Bearer <access_token>

Request:

json
{
  "refresh_token": "abc123..."
}

Response:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "name": "Laslie G.",
  "bio": "CEO @ Omevision",
  "avatar_url": "https://...",
  "language": "en"
}

All fields are optional. Only include fields to update.

Response:

json
{
  "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:

json
{
  "pin": "1234",
  "confirmation": "DELETE MY ACCOUNT"
}

Response:

json
{
  "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:

json
{
  "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):

json
{
  "success": true,
  "data": {
    "handle": "newhandle",
    "available": true
  }
}

Response (taken):

json
{
  "success": true,
  "data": {
    "handle": "laslie",
    "available": false,
    "reason": "taken"
  }
}

Response (reserved):

json
{
  "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:

json
{
  "new_handle": "laslie_ceo",
  "pin": "1234"
}

Response:

json
{
  "success": true,
  "data": {
    "old_handle": "laslie",
    "new_handle": "laslie_ceo",
    "next_change_available": "2026-04-18T20:00:00Z"
  }
}

Errors:

CodeStatusDescription
HANDLE_COOLDOWN429Must wait 30 days between changes
HANDLE_TAKEN409Handle already in use

Sessions

List Sessions

GET /sessions

List all active sessions for the user.

Headers: Authorization: Bearer <access_token>

Response:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "success": true,
  "data": {
    "verification_url": "https://verify.yeboid.com/session/abc123",
    "expires_in": 1800
  }
}

User is redirected to YeboVerify to complete verification.


Error Codes

CodeHTTP StatusDescription
INVALID_REQUEST400Malformed request body
INVALID_PHONE400Phone number format invalid
INVALID_PIN400PIN doesn't meet requirements
INVALID_HANDLE400Handle format invalid
INVALID_OTP400OTP code is wrong
OTP_EXPIRED400OTP code has expired
INVALID_CREDENTIALS401Wrong phone or PIN
INVALID_TOKEN401Access token invalid
TOKEN_EXPIRED401Access token expired
INVALID_REFRESH_TOKEN401Refresh token invalid
ACCOUNT_LOCKED403Account temporarily locked
FORBIDDEN403Not allowed to perform action
NOT_FOUND404Resource not found
PHONE_NOT_FOUND404Phone number not registered
HANDLE_TAKEN409Handle already in use
HANDLE_RESERVED409Handle is reserved
PHONE_EXISTS409Phone already registered
HANDLE_COOLDOWN429Must wait between handle changes
RATE_LIMITED429Too many requests
INTERNAL_ERROR500Server error

Rate Limits

EndpointLimitWindow
POST /auth/otp/send31 hour
POST /auth/signin515 minutes
POST /auth/otp/verify5per code
GET /users/handle/check301 minute
All other endpoints1001 minute

Rate limit headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1710770000

Webhooks

See WEBHOOKS.md for webhook events.


API Version: 1.0Last updated: March 18, 2026

One chat. Everything done.