YeboSafe PRD (Product Requirements Document)
Product Name: YeboSafe
Type: African Escrow & Payment Protection API Infrastructure
Status: Built & Deployed
API URL:https://yebosafe-api-1009635282906.europe-west1.run.app
Last Updated: 2026-03-19
1. Vision & Problem Statement
1.1 Vision
YeboSafe is a standalone escrow API infrastructure designed for African markets, enabling any platform, marketplace, or application to integrate secure payment protection without building complex escrow systems from scratch. It transforms the "completion code" escrow pattern (proven in YeboShops) into a reusable API that any business can plug into.
1.2 Problem Statement
African digital commerce faces critical trust and payment challenges:
- Platform Integration Burden: Every marketplace/app must build their own escrow system, duplicating effort and increasing risk of flawed implementations
- Cross-Platform Trust: Buyers transacting on multiple platforms have no unified payment protection layer
- Fraud Exposure: Small platforms lack the resources to implement robust escrow, leaving buyers and sellers vulnerable
- Developer Experience: No simple "plug and play" escrow API exists for African fintech developers
- Compliance Complexity: Each platform must handle payment regulations, audit trails, and dispute resolution independently
1.3 Solution
YeboSafe provides:
- API-First Escrow Infrastructure: Simple REST API for creating, managing, and completing escrow transactions
- Multi-Tenant Architecture: Each merchant (platform) gets isolated escrows, wallets, and API keys
- Completion Code Pattern: Proven mechanism where buyers reveal a 6-digit code only after receiving goods/services
- Webhook Integration: Real-time event notifications for status changes
- Merchant Dashboard: Self-service portal for managing escrows, viewing balances, and generating API keys
- Full Audit Trail: Every action logged for compliance and dispute resolution
1.4 Origin Story
YeboSafe was extracted from YeboShops' "Secure Payments" system. The original implementation in YeboShops proved the completion code escrow model works for African peer-to-peer commerce. YeboSafe generalizes this into a standalone API that any platform can integrate.
2. Technical Stack
2.1 Backend (yebosafe-api)
| Component | Technology |
|---|---|
| Runtime | Node.js + TypeScript |
| Framework | Express.js 4.x |
| Database | PostgreSQL (Neon serverless) |
| ORM | Prisma 5.22 |
| Auth | JWT + API Key dual authentication |
| Security | Helmet, CORS, bcryptjs |
| Validation | express-validator |
| Logging | Morgan |
2.2 Dashboard (yebosafe-dashboard)
| Component | Technology |
|---|---|
| Framework | React 19 + TypeScript |
| Build Tool | Vite 7 |
| Styling | Tailwind CSS 3.4 |
| HTTP Client | Axios |
| Routing | React Router 7 |
| Icons | Lucide React |
2.3 Infrastructure
| Component | Service |
|---|---|
| API Hosting | Google Cloud Run (europe-west1) |
| Database | Neon Serverless PostgreSQL |
| Dashboard Hosting | Cloudflare Pages (planned) |
| CI/CD | Cloud Build |
3. Core Features (Implemented)
3.1 Escrow Management
Create Escrow Transaction
- Merchant creates escrow with amount, currency, description, and optional payer info
- System generates unique 6-digit completion code
- Escrow starts in
PENDINGstatus - Webhook fired:
escrow.created
Accept Escrow
- Merchant confirms they will fulfill the transaction
- Status moves to
ACCEPTED - Webhook fired:
escrow.accepted
Complete Escrow
- Payer reveals completion code to merchant after receiving goods/services
- Merchant enters code to release funds
- Status moves to
COMPLETED - Funds credited to merchant wallet
- Webhook fired:
escrow.completed
Refuse Escrow
- Merchant can refuse before accepting
- Requires reason
- Status moves to
REFUSED - Webhook fired:
escrow.refused
Dispute Escrow
- Either party can raise dispute before completion
- Requires reason
- Status moves to
DISPUTED - Webhook fired:
escrow.disputed
Cancel Escrow
- Escrow can be cancelled before completion
- Requires reason
- Status moves to
CANCELLED - Webhook fired:
escrow.cancelled
3.2 Merchant Management
Registration
- Email + password authentication
- Automatic initial API key generation
- Automatic wallet creation
- JWT token issued for dashboard access
API Key Management
- Generate multiple named API keys
- Keys formatted as
ys_live_{uuid} - Track last usage timestamp
- Revoke keys without affecting others
Webhook Configuration
- Set webhook URL for event notifications
- HMAC-SHA256 signed payloads
- Automatic retry (up to 3 attempts with exponential backoff)
3.3 Wallet System
Merchant Wallet
- One wallet per merchant
- Multi-currency support (default USD)
- Balance updated automatically on escrow completion
- Full transaction history
Withdrawal Requests
- Merchants can request withdrawals
- Creates DEBIT entry in transaction log
- Actual payout handled externally (manual or future integration)
3.4 Dashboard Portal
Overview Dashboard
- Wallet balance display
- Escrow statistics (total, pending, completed, disputed)
- Recent escrows list
- Dispute alerts
Escrow Management
- List all escrows with filters (status, search)
- Detailed escrow view with timeline
- Action buttons (accept, refuse, complete, dispute, cancel)
- Completion code display with copy functionality
Wallet Management
- Balance display
- Transaction history
- Withdrawal request form
Developer Tools
- API key generation and revocation
- Webhook URL configuration
- Code examples and event documentation
3.5 Admin Features
Platform Administration
- List all escrows across merchants
- List all merchants with wallet info
- Platform-wide statistics
4. User Journeys
4.1 Platform Integrator Journey
1. Discovery
└─ Find YeboSafe API documentation
└─ Understand escrow flow and integration options
2. Onboarding
└─ Register merchant account via dashboard or API
└─ Receive initial API key
└─ Configure webhook URL
3. Integration
└─ Implement POST /v1/escrow in platform backend
└─ Handle webhook events for status updates
└─ Display completion code to buyers
└─ Implement code verification flow for sellers
4. Operations
└─ Monitor escrows via dashboard
└─ Handle disputes manually or programmatically
└─ Track wallet balance and request withdrawals
5. Scale
└─ Generate additional API keys for different services
└─ Implement advanced flows (refunds, partial releases)4.2 Buyer Journey (via Integrated Platform)
1. Transaction Initiation
└─ Buyer agrees to purchase on integrated platform
└─ Platform creates escrow via YeboSafe API
└─ Buyer receives completion code (kept secret)
2. Fulfillment
└─ Seller delivers goods/services
└─ Buyer verifies delivery
3. Release
└─ Buyer reveals completion code to seller
└─ Seller enters code on platform or via API
└─ Funds released to seller
4. Dispute (if needed)
└─ Either party raises dispute before completion
└─ Platform/YeboSafe admin mediates4.3 Seller Journey (via Integrated Platform)
1. Accept Order
└─ Platform notifies seller of new escrow
└─ Seller accepts via platform interface
└─ Escrow status moves to ACCEPTED
2. Fulfillment
└─ Seller delivers goods/services
└─ Obtains completion code from buyer
3. Receive Payment
└─ Enter completion code via platform or API
└─ Funds credited to merchant wallet
└─ Withdraw to bank/mobile money (future)
4. Refuse (if needed)
└─ Can refuse escrow before accepting
└─ Must provide reason4.4 Admin Journey
1. Monitoring
└─ View platform-wide statistics
└─ Monitor active escrows across merchants
└─ Track total volume and completion rates
2. Merchant Management
└─ View all registered merchants
└─ Monitor merchant wallet balances
└─ Review escrow activity per merchant
3. Dispute Resolution
└─ Review disputed escrows
└─ Access full audit trail
└─ Manual resolution actions (future)5. Data Models
5.1 Merchant Model
model Merchant {
id String @id @default(cuid())
name String // Business name
email String @unique // Login email
password String // bcrypt hashed
webhookUrl String? // Webhook endpoint
isActive Boolean @default(true) // Account status
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
apiKeys ApiKey[]
escrows EscrowTransaction[] @relation("MerchantEscrows")
wallet MerchantWallet?
}Fields:
| Field | Type | Description |
|---|---|---|
| id | String (CUID) | Primary key |
| name | String | Business/merchant name |
| String | Unique login email | |
| password | String | bcrypt-hashed password |
| webhookUrl | String? | URL for webhook notifications |
| isActive | Boolean | Account active status |
| createdAt | DateTime | Registration timestamp |
| updatedAt | DateTime | Last update timestamp |
5.2 ApiKey Model
model ApiKey {
id String @id @default(cuid())
key String @unique // Full API key (ys_live_xxx)
prefix String // First 12 chars for display
name String? // User-given name
isActive Boolean @default(true) // Key active status
merchantId String // Owner merchant
merchant Merchant @relation(fields: [merchantId], references: [id])
createdAt DateTime @default(now())
lastUsedAt DateTime? // Last API call timestamp
}Fields:
| Field | Type | Description |
|---|---|---|
| id | String (CUID) | Primary key |
| key | String | Full API key (ys_live_{uuid}) |
| prefix | String | First 12 characters for safe display |
| name | String? | Optional friendly name |
| isActive | Boolean | Whether key is active |
| merchantId | String | FK to Merchant |
| createdAt | DateTime | Creation timestamp |
| lastUsedAt | DateTime? | Last usage timestamp |
5.3 EscrowTransaction Model
model EscrowTransaction {
id String @id @default(cuid())
reference String @unique @default(cuid()) // Public reference
merchantId String // Owner merchant
merchant Merchant @relation("MerchantEscrows", fields: [merchantId], references: [id])
// Payer Information
payerName String? // Buyer name
payerEmail String? // Buyer email
payerPhone String? // Buyer phone
// Transaction Details
amount Decimal @db.Decimal(12, 2) // Amount in currency
currency String @default("USD") // ISO currency code
description String? // Transaction description
metadata Json? // Custom metadata
// Status & Flow
status EscrowStatus @default(PENDING) // Current status
completionCode String @unique // 6-digit code
webhookUrl String? // Override webhook URL
// Timestamps
completedAt DateTime? // When completed
disputedAt DateTime? // When disputed
disputeReason String? // Dispute reason
cancelledAt DateTime? // When cancelled
cancelReason String? // Cancellation reason
refundedAt DateTime? // When refunded
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
logs EscrowLog[]
}
enum EscrowStatus {
PENDING // Created, awaiting acceptance
ACCEPTED // Merchant accepted, in escrow
COMPLETED // Code entered, funds released
REFUSED // Merchant refused to accept
DISPUTED // Dispute raised
CANCELLED // Cancelled before completion
REFUNDED // Funds returned (future)
}Fields:
| Field | Type | Description |
|---|---|---|
| id | String (CUID) | Primary key |
| reference | String | Public-facing reference ID |
| merchantId | String | FK to owning Merchant |
| payerName | String? | Buyer's name |
| payerEmail | String? | Buyer's email |
| payerPhone | String? | Buyer's phone |
| amount | Decimal(12,2) | Transaction amount |
| currency | String | ISO currency code (default: USD) |
| description | String? | Transaction description |
| metadata | Json? | Custom key-value data |
| status | EscrowStatus | Current lifecycle status |
| completionCode | String | Unique 6-digit code |
| webhookUrl | String? | Override merchant webhook |
| completedAt | DateTime? | Completion timestamp |
| disputedAt | DateTime? | Dispute timestamp |
| disputeReason | String? | Reason for dispute |
| cancelledAt | DateTime? | Cancellation timestamp |
| cancelReason | String? | Reason for cancellation |
| refundedAt | DateTime? | Refund timestamp |
| createdAt | DateTime | Creation timestamp |
| updatedAt | DateTime | Last update timestamp |
5.4 EscrowLog Model
model EscrowLog {
id String @id @default(cuid())
escrowId String // Parent escrow
escrow EscrowTransaction @relation(fields: [escrowId], references: [id])
action String // Action name
actorId String? // Who performed action
metadata Json? // Action details
createdAt DateTime @default(now())
}Fields:
| Field | Type | Description |
|---|---|---|
| id | String (CUID) | Primary key |
| escrowId | String | FK to EscrowTransaction |
| action | String | Action type (created, accepted, completed, etc.) |
| actorId | String? | ID of actor (merchant ID) |
| metadata | Json? | Additional action data |
| createdAt | DateTime | Action timestamp |
5.5 MerchantWallet Model
model MerchantWallet {
id String @id @default(cuid())
merchantId String @unique // One wallet per merchant
merchant Merchant @relation(fields: [merchantId], references: [id])
balance Decimal @default(0) @db.Decimal(12, 2)
currency String @default("USD")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
transactions WalletEntry[]
}Fields:
| Field | Type | Description |
|---|---|---|
| id | String (CUID) | Primary key |
| merchantId | String | FK to Merchant (unique) |
| balance | Decimal(12,2) | Current balance |
| currency | String | Wallet currency |
| createdAt | DateTime | Creation timestamp |
| updatedAt | DateTime | Last update timestamp |
5.6 WalletEntry Model
model WalletEntry {
id String @id @default(cuid())
walletId String // Parent wallet
wallet MerchantWallet @relation(fields: [walletId], references: [id])
type EntryType // CREDIT or DEBIT
amount Decimal @db.Decimal(12, 2) // Transaction amount
description String? // Description
reference String? // External reference
createdAt DateTime @default(now())
}
enum EntryType {
CREDIT // Money added to wallet
DEBIT // Money removed from wallet
}Fields:
| Field | Type | Description |
|---|---|---|
| id | String (CUID) | Primary key |
| walletId | String | FK to MerchantWallet |
| type | EntryType | CREDIT or DEBIT |
| amount | Decimal(12,2) | Transaction amount |
| description | String? | Human-readable description |
| reference | String? | External reference (escrow ID, etc.) |
| createdAt | DateTime | Entry timestamp |
5.7 WebhookEvent Model
model WebhookEvent {
id String @id @default(cuid())
merchantId String // Target merchant
event String // Event type
payload Json // Event payload
delivered Boolean @default(false) // Delivery status
attempts Int @default(0) // Delivery attempts
lastError String? // Last error message
createdAt DateTime @default(now())
}Fields:
| Field | Type | Description |
|---|---|---|
| id | String (CUID) | Primary key |
| merchantId | String | Target merchant ID |
| event | String | Event type (escrow.created, etc.) |
| payload | Json | Full event payload |
| delivered | Boolean | Whether delivery succeeded |
| attempts | Int | Number of delivery attempts |
| lastError | String? | Last error if delivery failed |
| createdAt | DateTime | Event creation timestamp |
6. API Reference
6.1 Authentication
API Key Authentication (Server-to-Server)
Header: X-API-Key: ys_live_xxxxxxxxxxxxUsed for programmatic API access from integrated platforms.
JWT Authentication (Dashboard)
Header: Authorization: Bearer <jwt_token>Used for dashboard sessions. Tokens issued on login, valid for 7 days.
Admin Authentication
Header: X-Admin-Key: <admin_api_key>Used for platform administration endpoints.
6.2 Merchant Endpoints (/v1/merchants)
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /register | Register new merchant | - |
| POST | /login | Login (get JWT) | - |
| GET | /me | Get current profile | JWT |
| PUT | /webhook | Set webhook URL | JWT |
| POST | /keys | Generate API key | JWT |
| DELETE | /keys/:id | Revoke API key | JWT |
POST /v1/merchants/register
Request:
{
"name": "Acme Marketplace",
"email": "admin@acme.com",
"password": "securePassword123"
}Response (201):
{
"success": true,
"message": "Merchant registered successfully",
"data": {
"merchant": {
"id": "clxxx...",
"name": "Acme Marketplace",
"email": "admin@acme.com"
},
"apiKey": {
"key": "ys_live_abc123def456...",
"prefix": "ys_live_abc1"
},
"token": "eyJhbGciOiJIUzI1..."
}
}POST /v1/merchants/login
Request:
{
"email": "admin@acme.com",
"password": "securePassword123"
}Response (200):
{
"success": true,
"message": "Login successful",
"data": {
"merchant": {
"id": "clxxx...",
"name": "Acme Marketplace",
"email": "admin@acme.com"
},
"token": "eyJhbGciOiJIUzI1..."
}
}GET /v1/merchants/me
Response (200):
{
"success": true,
"data": {
"id": "clxxx...",
"name": "Acme Marketplace",
"email": "admin@acme.com",
"webhookUrl": "https://acme.com/webhooks/yebosafe",
"isActive": true,
"createdAt": "2026-03-19T10:00:00.000Z",
"apiKeys": [
{
"id": "clyyy...",
"prefix": "ys_live_abc1",
"name": "Production Key",
"createdAt": "2026-03-19T10:00:00.000Z",
"lastUsedAt": "2026-03-19T12:00:00.000Z"
}
]
}
}PUT /v1/merchants/webhook
Request:
{
"webhookUrl": "https://acme.com/webhooks/yebosafe"
}Response (200):
{
"success": true,
"message": "Webhook URL updated",
"data": {
"id": "clxxx...",
"webhookUrl": "https://acme.com/webhooks/yebosafe"
}
}POST /v1/merchants/keys
Request:
{
"name": "Staging Key"
}Response (201):
{
"success": true,
"message": "API key created. Save this — it will only be shown once.",
"data": {
"key": "ys_live_xyz789...",
"prefix": "ys_live_xyz7"
}
}6.3 Escrow Endpoints (/v1/escrow)
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | / | Create escrow | API Key / JWT |
| GET | / | List escrows | API Key / JWT |
| GET | /:id | Get escrow by ID | API Key / JWT |
| POST | /:id/accept | Accept escrow | API Key / JWT |
| POST | /:id/refuse | Refuse escrow | API Key / JWT |
| POST | /:id/complete | Complete escrow | API Key / JWT |
| POST | /:id/dispute | Dispute escrow | API Key / JWT |
| POST | /:id/cancel | Cancel escrow | API Key / JWT |
| GET | /lookup/:code | Lookup by code (public) | - |
POST /v1/escrow
Request:
{
"amount": 150.00,
"currency": "USD",
"description": "Payment for laptop purchase",
"payerName": "John Doe",
"payerEmail": "john@example.com",
"payerPhone": "+1234567890",
"metadata": {
"orderId": "ORD-12345",
"productId": "PROD-67890"
},
"webhookUrl": "https://acme.com/webhooks/order-12345"
}Response (201):
{
"success": true,
"message": "Escrow created",
"data": {
"id": "clzzz...",
"reference": "clabc...",
"amount": "150.00",
"currency": "USD",
"description": "Payment for laptop purchase",
"payerName": "John Doe",
"payerEmail": "john@example.com",
"payerPhone": "+1234567890",
"status": "PENDING",
"completionCode": "847291",
"metadata": { "orderId": "ORD-12345", "productId": "PROD-67890" },
"webhookUrl": "https://acme.com/webhooks/order-12345",
"createdAt": "2026-03-19T12:00:00.000Z",
"logs": [
{
"id": "cllog...",
"action": "created",
"createdAt": "2026-03-19T12:00:00.000Z",
"metadata": { "amount": 150.00, "currency": "USD" }
}
]
}
}GET /v1/escrow
Query Parameters:
status- Filter by status (PENDING, ACCEPTED, COMPLETED, etc.)page- Page number (default: 1)limit- Items per page (default: 20)
Response (200):
{
"success": true,
"data": {
"escrows": [...],
"total": 42,
"page": 1,
"limit": 20,
"pages": 3
}
}GET /v1/escrow/:id
Response (200):
{
"success": true,
"data": {
"id": "clzzz...",
"reference": "clabc...",
"amount": "150.00",
"currency": "USD",
"status": "PENDING",
"completionCode": "847291",
"logs": [...]
}
}POST /v1/escrow/:id/accept
Response (200):
{
"success": true,
"message": "Escrow accepted",
"data": {
"id": "clzzz...",
"status": "ACCEPTED"
}
}POST /v1/escrow/:id/refuse
Request:
{
"reason": "Item out of stock"
}Response (200):
{
"success": true,
"message": "Escrow refused",
"data": {
"id": "clzzz...",
"status": "REFUSED"
}
}POST /v1/escrow/:id/complete
Request:
{
"completionCode": "847291"
}Response (200):
{
"success": true,
"message": "Escrow completed — funds released to wallet",
"data": {
"id": "clzzz...",
"status": "COMPLETED",
"completedAt": "2026-03-19T14:30:00.000Z"
}
}POST /v1/escrow/:id/dispute
Request:
{
"reason": "Item not as described"
}Response (200):
{
"success": true,
"message": "Dispute raised",
"data": {
"id": "clzzz...",
"status": "DISPUTED",
"disputedAt": "2026-03-19T14:45:00.000Z",
"disputeReason": "Item not as described"
}
}POST /v1/escrow/:id/cancel
Request:
{
"reason": "Buyer requested cancellation"
}Response (200):
{
"success": true,
"message": "Escrow cancelled",
"data": {
"id": "clzzz...",
"status": "CANCELLED",
"cancelledAt": "2026-03-19T14:50:00.000Z",
"cancelReason": "Buyer requested cancellation"
}
}GET /v1/escrow/lookup/:code
Public endpoint - no authentication required.
Response (200):
{
"success": true,
"data": {
"id": "clzzz...",
"reference": "clabc...",
"amount": 150.00,
"currency": "USD",
"description": "Payment for laptop purchase",
"status": "ACCEPTED",
"createdAt": "2026-03-19T12:00:00.000Z",
"merchantName": "Acme Marketplace"
}
}6.4 Wallet Endpoints (/v1/wallet)
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | / | Get wallet & transactions | API Key / JWT |
| POST | /withdraw | Request withdrawal | API Key / JWT |
GET /v1/wallet
Query Parameters:
page- Page number (default: 1)limit- Items per page (default: 20)
Response (200):
{
"success": true,
"data": {
"balance": 1250.50,
"currency": "USD",
"transactions": [
{
"id": "clentry...",
"type": "CREDIT",
"amount": 150.00,
"description": "Escrow completed: clzzz...",
"reference": "clzzz...",
"createdAt": "2026-03-19T14:30:00.000Z"
}
],
"total": 15,
"page": 1,
"limit": 20,
"pages": 1
}
}POST /v1/wallet/withdraw
Request:
{
"amount": 500.00,
"bankDetails": {
"bankName": "First National Bank",
"accountNumber": "1234567890",
"accountName": "Acme Inc"
}
}Response (200):
{
"success": true,
"message": "Withdrawal request submitted",
"data": {
"balance": 750.50
}
}6.5 Admin Endpoints (/v1/admin)
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /escrows | List all escrows | Admin Key |
| GET | /merchants | List all merchants | Admin Key |
| GET | /stats | Platform statistics | Admin Key |
GET /v1/admin/escrows
Query Parameters:
status- Filter by statuspage- Page numberlimit- Items per page
Response (200):
{
"success": true,
"data": {
"escrows": [
{
"id": "clzzz...",
"reference": "clabc...",
"amount": "150.00",
"status": "COMPLETED",
"merchant": {
"id": "clxxx...",
"name": "Acme Marketplace",
"email": "admin@acme.com"
}
}
],
"total": 156,
"page": 1,
"limit": 20,
"pages": 8
}
}GET /v1/admin/merchants
Response (200):
{
"success": true,
"data": {
"merchants": [
{
"id": "clxxx...",
"name": "Acme Marketplace",
"email": "admin@acme.com",
"isActive": true,
"createdAt": "2026-03-19T10:00:00.000Z",
"wallet": {
"balance": "1250.50",
"currency": "USD"
},
"_count": {
"escrows": 42
}
}
],
"total": 15,
"page": 1,
"limit": 20,
"pages": 1
}
}GET /v1/admin/stats
Response (200):
{
"success": true,
"data": {
"totalEscrows": 1542,
"completedEscrows": 1287,
"activeEscrows": 89,
"merchantCount": 45,
"totalVolume": 256789.50,
"completionRate": 83
}
}7. Escrow Lifecycle State Machine
┌─────────────┐
│ PENDING │
│ (created) │
└──────┬──────┘
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ ACCEPTED │ │ REFUSED │ │CANCELLED │
│(seller ok)│ │(seller no)│ │ (abort) │
└────┬─────┘ └──────────┘ └──────────┘
│
┌────┼────────────┐
│ │ │
▼ ▼ ▼
┌───────────┐ ┌──────────┐
│ COMPLETED │ │ DISPUTED │
│(code used)│ │ (issue) │
└───────────┘ └────┬─────┘
│
┌────────┼────────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ REFUNDED │ │ RESOLVED │
│ (future) │ │ (future) │
└──────────┘ └──────────┘State Transitions
| From | To | Action | Requirements |
|---|---|---|---|
| PENDING | ACCEPTED | accept | Merchant decides to fulfill |
| PENDING | REFUSED | refuse | Reason required |
| PENDING | CANCELLED | cancel | Reason required |
| PENDING | DISPUTED | dispute | Reason required |
| ACCEPTED | COMPLETED | complete | Valid completion code |
| ACCEPTED | DISPUTED | dispute | Reason required |
| ACCEPTED | CANCELLED | cancel | Reason required |
Terminal States
- COMPLETED: Funds released, escrow closed
- REFUSED: Merchant declined, no action needed
- CANCELLED: Transaction aborted
- DISPUTED: Requires manual resolution
- REFUNDED: Funds returned (future implementation)
8. Payment Integration
8.1 Current Implementation
YeboSafe currently operates as an escrow ledger system:
- No direct payment collection - Funds are assumed to be collected by the integrating platform
- Wallet-based accounting - Completed escrows credit the merchant's virtual wallet
- Manual withdrawals - Withdrawal requests create debit entries; actual payouts handled externally
8.2 Integration Pattern
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Platform │────▶│ YeboSafe │────▶│ Merchant │
│ (collects │ │ (escrow │ │ (receives │
│ payment) │ │ ledger) │ │ payout) │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ ▲
│ │ │
▼ ▼ │
Stripe/PayStack Wallet Balance Manual/API Payout8.3 Future Integration Points
| Provider | Region | Status |
|---|---|---|
| Stripe | Global | Planned |
| PayStack | Nigeria, Ghana, SA | Planned |
| Flutterwave | Pan-African | Planned |
| MTN MoMo | Uganda, Rwanda, etc. | Planned |
| M-Pesa | Kenya, Tanzania | Planned |
Future Flow:
1. Escrow Created → YeboSafe initiates payment hold via provider
2. Escrow Accepted → Funds confirmed in escrow account
3. Escrow Completed → Funds released to merchant's external account
4. Escrow Refunded → Funds returned to payer9. Dispute Resolution
9.1 Current Implementation
- Dispute Flag: Any party can mark escrow as
DISPUTED - Reason Required: Dispute reason captured and logged
- Manual Resolution: Admin reviews via dashboard or direct database access
- Audit Trail: Full log of all actions available for review
9.2 Dispute Workflow
1. Party raises dispute (POST /escrow/:id/dispute)
2. Escrow status → DISPUTED
3. Webhook fires: escrow.disputed
4. Admin reviews:
- Transaction details
- Payer information
- Full audit log
- Communication history (via integrated platform)
5. Resolution options:
- Release to merchant (manual COMPLETED)
- Refund to payer (manual REFUNDED - future)
- Partial settlement (future)9.3 Future Enhancements
| Feature | Description | Status |
|---|---|---|
| Dispute Dashboard | Dedicated UI for reviewing disputes | Planned |
| Evidence Upload | Allow parties to submit evidence | Planned |
| Resolution Actions | API endpoints for admin resolutions | Planned |
| Auto-Escalation | Time-based escalation rules | Planned |
| Arbitration | Third-party arbitration integration | Future |
10. Webhooks
10.1 Event Types
| Event | Description | Trigger |
|---|---|---|
escrow.created | New escrow created | POST /escrow |
escrow.accepted | Merchant accepted escrow | POST /escrow/:id/accept |
escrow.refused | Merchant refused escrow | POST /escrow/:id/refuse |
escrow.completed | Escrow completed, funds released | POST /escrow/:id/complete |
escrow.disputed | Dispute raised | POST /escrow/:id/dispute |
escrow.cancelled | Escrow cancelled | POST /escrow/:id/cancel |
10.2 Webhook Payload
{
"event": "escrow.completed",
"data": {
"id": "clzzz...",
"reference": "clabc...",
"amount": "150.00",
"currency": "USD",
"status": "COMPLETED"
},
"timestamp": "2026-03-19T14:30:00.000Z"
}10.3 Webhook Security
Signature Verification:
// Header: X-YeboSafe-Signature
const signature = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(requestBody)
.digest('hex');
if (req.headers['x-yebosafe-signature'] !== signature) {
throw new Error('Invalid signature');
}10.4 Delivery & Retry
| Attempt | Delay | Timeout |
|---|---|---|
| 1 | Immediate | 10s |
| 2 | 2 seconds | 10s |
| 3 | 4 seconds | 10s |
Delivery Status Tracking:
WebhookEventmodel tracks delivery status- Failed attempts logged with error message
- Retry up to 3 times with exponential backoff
10.5 Webhook URL Override
Per-escrow webhook URL can be specified:
{
"amount": 100.00,
"webhookUrl": "https://acme.com/webhooks/order-12345"
}11. Service Architecture
11.1 Component Overview
┌─────────────────────────────────────────────────────────────────┐
│ API Layer │
│ /v1/merchants /v1/escrow /v1/wallet /v1/admin │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Middleware │
│ apiKeyAuth | jwtAuth | flexAuth | adminAuth | rateLimiter │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Controllers │
│ merchant.controller | escrow.controller | wallet.controller │
│ admin.controller │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Services │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ MerchantService │ │ EscrowService │ │
│ │ - register │ │ - createEscrow │ │
│ │ - login │ │ - acceptEscrow │ │
│ │ - generateApiKey │ │ - completeEscrow │ │
│ │ - revokeApiKey │ │ - disputeEscrow │ │
│ └──────────────────┘ │ - cancelEscrow │ │
│ │ - adminStats │ │
│ └──────────────────┘ │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ WalletService │ │ WebhookService │ │
│ │ - ensureWallet │ │ - fire │ │
│ │ - creditWallet │ │ - deliver │ │
│ │ - debitWallet │ │ - sign │ │
│ │ - getWallet │ │ - retry │ │
│ └──────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Data Layer (Prisma) │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ PostgreSQL (Neon) │ │
│ │ Merchant | ApiKey | EscrowTransaction | EscrowLog │ │
│ │ MerchantWallet | WalletEntry | WebhookEvent │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘11.2 Request Flow
API Request
│
▼
Express App (app.ts)
│
├─→ Helmet (security headers)
├─→ CORS
├─→ JSON Parser
├─→ Morgan (logging)
├─→ Rate Limiter
│
▼
Route Handler
│
├─→ Auth Middleware (apiKeyAuth / jwtAuth / flexAuth)
├─→ Validation (express-validator)
│
▼
Controller
│
├─→ Input validation
├─→ Service method call
│
▼
Service
│
├─→ Business logic
├─→ Prisma queries
├─→ Cross-service calls (Wallet, Webhook)
│
▼
Response11.3 Service Dependencies
EscrowService
├─→ WalletService (on completion)
└─→ WebhookService (on status change)
MerchantService
└─→ WalletService (create wallet on register)
WebhookService
└─→ Prisma (log delivery status)12. Authentication & Security
12.1 Authentication Methods
| Method | Use Case | Header | Validity |
|---|---|---|---|
| API Key | Server-to-server | X-API-Key | Until revoked |
| JWT | Dashboard sessions | Authorization: Bearer | 7 days |
| Admin Key | Platform administration | X-Admin-Key | Static |
12.2 Flexible Auth
Routes using flexAuth middleware accept either API Key or JWT:
const flexAuth = (req, res, next) => {
if (req.headers['x-api-key']) {
return apiKeyAuth(req, res, next);
} else if (req.headers['authorization']) {
return jwtAuth(req, res, next);
}
return res.status(401).json({ message: 'Authentication required' });
};12.3 Rate Limiting
- Limit: 200 requests per minute per IP
- Implementation: In-memory (should use Redis for production scale)
- Response when exceeded: 429 Too Many Requests
12.4 Security Headers
Via Helmet middleware:
- Content Security Policy
- X-Frame-Options
- X-Content-Type-Options
- Strict-Transport-Security
12.5 Password Security
- Hashing: bcrypt with cost factor 12
- Minimum length: 8 characters (enforced at registration)
12.6 API Key Format
ys_live_{uuid-without-dashes}
Example: ys_live_abc123def456ghi789jkl012mno345- Prefix identifies the key type
- First 12 characters stored as
prefixfor safe display - Full key shown only once at creation
13. Environment Configuration
13.1 Required Variables
# Database
DATABASE_URL=postgresql://user:pass@host:5432/yebosafe
# Authentication
JWT_SECRET=your-secure-jwt-secret
# Admin Access
ADMIN_API_KEY=your-admin-api-key
# Runtime
NODE_ENV=production
PORT=808013.2 Optional Variables
# Logging
LOG_LEVEL=info
# External Services (future)
STRIPE_SECRET_KEY=sk_live_xxx
PAYSTACK_SECRET_KEY=sk_live_xxx14. Gaps & Missing Features
14.1 Critical Missing
| Feature | Description | Priority |
|---|---|---|
| Payment Gateway Integration | Direct payment collection/disbursement | P0 |
| Dashboard Routing | React Router setup in App.tsx | P0 |
| Payout System | Automated withdrawals to bank/mobile money | P0 |
| Dispute Resolution UI | Admin interface for dispute handling | P1 |
| Email Notifications | Transactional emails to merchants/payers | P1 |
14.2 Security Enhancements
| Feature | Description | Priority |
|---|---|---|
| Redis Rate Limiter | Replace in-memory rate limiter | P1 |
| API Key Hashing | Store hashed keys instead of plaintext | P1 |
| Webhook Secret per Merchant | Individual signing secrets | P2 |
| Audit Log API | Expose logs via API | P2 |
| 2FA for Dashboard | Two-factor authentication | P2 |
14.3 Operational Features
| Feature | Description | Priority |
|---|---|---|
| Health Check Dashboard | Monitoring and alerting | P1 |
| Metrics/Analytics | Prometheus/Grafana integration | P2 |
| API Documentation (OpenAPI) | Swagger/OpenAPI spec | P1 |
| SDK Libraries | Node.js, Python, PHP SDKs | P2 |
| Sandbox/Test Mode | Test environment without real transactions | P1 |
14.4 Business Features
| Feature | Description | Priority |
|---|---|---|
| Platform Fees | Automatic fee deduction | P1 |
| Multi-Currency Wallets | Separate balances per currency | P2 |
| Partial Releases | Release portion of escrow | P2 |
| Scheduled Releases | Time-based auto-release | P3 |
| Escrow Templates | Reusable escrow configurations | P3 |
| Sub-Merchant Support | Hierarchical merchant structure | P3 |
14.5 Dashboard Gaps
| Issue | Description |
|---|---|
| No routing configured | App.tsx shows Vite boilerplate, not routes |
| Settings page missing | Settings nav link leads nowhere |
| No deployment | Dashboard not deployed to Cloudflare Pages |
| Missing responsive design | Mobile layout not optimized |
15. Comparison: YeboShops vs YeboSafe
| Aspect | YeboShops SecurePayment | YeboSafe Escrow |
|---|---|---|
| Target User | End consumers (buyers/sellers) | Platform developers |
| Integration | Built-in to YeboShops | Standalone API |
| Auth | User JWT | Merchant API Key + JWT |
| Wallet Owner | Individual users | Merchants (platforms) |
| Completion Code | Buyer holds, shares with seller | Platform controls distribution |
| Webhooks | Internal notifications | External HTTP callbacks |
| Multi-Tenancy | Single platform | Multiple platforms |
| Admin | YeboShops admin | YeboSafe admin |
16. Integration Example
16.1 Creating an Escrow (Node.js)
const YEBOSAFE_API = 'https://yebosafe-api-1009635282906.europe-west1.run.app';
const API_KEY = 'ys_live_your_key_here';
async function createEscrow(orderDetails) {
const response = await fetch(`${YEBOSAFE_API}/v1/escrow`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY,
},
body: JSON.stringify({
amount: orderDetails.total,
currency: 'USD',
description: `Order #${orderDetails.orderId}`,
payerName: orderDetails.buyerName,
payerEmail: orderDetails.buyerEmail,
metadata: {
orderId: orderDetails.orderId,
items: orderDetails.items,
},
}),
});
const { data } = await response.json();
// Store completion code securely
// Share code with buyer via your platform
return data;
}16.2 Handling Webhooks
const crypto = require('crypto');
app.post('/webhooks/yebosafe', express.json(), (req, res) => {
// Verify signature
const signature = crypto
.createHmac('sha256', process.env.YEBOSAFE_WEBHOOK_SECRET)
.update(JSON.stringify(req.body))
.digest('hex');
if (req.headers['x-yebosafe-signature'] !== signature) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { event, data } = req.body;
switch (event) {
case 'escrow.accepted':
// Notify seller to fulfill order
notifySellerToShip(data.reference);
break;
case 'escrow.completed':
// Mark order as complete
markOrderComplete(data.reference);
break;
case 'escrow.disputed':
// Alert support team
alertSupportTeam(data.reference, data.disputeReason);
break;
}
res.json({ received: true });
});16.3 Completing an Escrow
async function completeEscrow(completionCode) {
const response = await fetch(`${YEBOSAFE_API}/v1/escrow/${escrowId}/complete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY,
},
body: JSON.stringify({ completionCode }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
return response.json();
}Appendix A: Error Codes
| Code | HTTP Status | Description |
|---|---|---|
| ESCROW_NOT_FOUND | 404 | Escrow does not exist or not owned by merchant |
| INVALID_AMOUNT | 400 | Amount must be greater than 0 |
| INVALID_CODE | 404 | Completion code does not match any escrow |
| ALREADY_COMPLETED | 400 | Escrow already completed |
| ALREADY_DISPUTED | 400 | Escrow already disputed |
| ALREADY_CANCELLED | 400 | Escrow already cancelled |
| ESCROW_NOT_PENDING | 400 | Escrow is not in PENDING status |
| ESCROW_NOT_ACCEPTED | 400 | Escrow must be ACCEPTED before completing |
| ESCROW_DISPUTED | 400 | Cannot act on a disputed escrow |
| ESCROW_CANCELLED | 400 | Escrow is cancelled |
| ESCROW_ALREADY_CLOSED | 400 | Escrow is in terminal state |
| EMAIL_TAKEN | 409 | Email already registered |
| KEY_NOT_FOUND | 404 | API key not found or not owned |
| INSUFFICIENT_BALANCE | 400 | Wallet balance insufficient for withdrawal |
| INVALID_CREDENTIALS | 401 | Login email or password incorrect |
Appendix B: Webhook Events Reference
interface WebhookPayload {
event: 'escrow.created' | 'escrow.accepted' | 'escrow.completed'
| 'escrow.refused' | 'escrow.disputed' | 'escrow.cancelled';
data: {
id: string;
reference: string;
amount: string;
currency: string;
status: EscrowStatus;
};
timestamp: string; // ISO 8601
}Appendix C: Dashboard Pages
| Page | Route | Description |
|---|---|---|
| Login | /login | Merchant authentication |
| Register | /register | Merchant registration |
| Dashboard | /dashboard | Overview with stats |
| Escrows | /escrows | Escrow list with filters |
| Escrow Detail | /escrows/:id | Single escrow view with actions |
| Wallet | /wallet | Balance and transactions |
| Developers | /developers | API keys and webhooks |
| Settings | /settings | Account settings (not implemented) |
Document generated from YeboSafe codebase analysis (yebosafe-api, yebosafe-dashboard) on 2026-03-19.