API Reference
Base URL
| Environment | URL |
|---|---|
| Production | https://loftygoldenoil.com/api |
| Staging | http://170.9.14.60:8443/api |
All endpoints are relative to the base URL above. The API follows REST conventions: resources are nouns, HTTP methods indicate operations.
Content-Type
All request and response bodies use application/json.
Authentication
The API uses JSON Web Token (JWT) Bearer tokens for authentication.
- Authenticate via
POST /api/auth/loginwith email and password. - Include the returned token in all subsequent requests:
Authorization: Bearer <token>
- Tokens expire after 24 hours. Re-authentication is required upon expiry (no refresh tokens).
Response Format
All responses return JSON with the following structure:
Success (single resource):
{
"id": 1,
"name": "Item Name",
"created_at": "2026-01-15T10:30:00.000Z"
}
Success (collection):
[
{ "id": 1, "name": "Item 1" },
{ "id": 2, "name": "Item 2" }
]
Error:
{
"error": "Error message describing what went wrong"
}
HTTP Status Codes
| Code | Meaning |
|---|---|
| 200 | OK — Request succeeded |
| 201 | Created — Resource created successfully |
| 400 | Bad Request — Invalid input or missing required fields |
| 401 | Unauthorized — Missing or invalid authentication token |
| 403 | Forbidden — Authenticated but insufficient permissions |
| 404 | Not Found — Resource does not exist |
| 409 | Conflict — Duplicate resource or constraint violation |
| 500 | Internal Server Error — Unexpected server failure |
Rate Limiting
Rate limiting is not currently implemented. It is recommended for production hardening. A suggested configuration:
- Authenticated requests: 100 requests per minute per user
- Login endpoint: 10 attempts per minute per IP
Authentication Endpoints
POST /api/auth/login
Authenticate a user and receive a JWT token.
| Property | Value |
|---|---|
| Auth Required | No |
| Rate Limit | Recommended: 10 req/min/IP |
Request Body:
{
"email": "user@example.com",
"password": "securepassword"
}
| Field | Type | Required | Description |
|---|---|---|---|
| string | Yes | User's login email | |
| password | string | Yes | User's password |
Success Response (200):
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": 1,
"name": "John Doe",
"email": "user@example.com",
"role": "admin"
}
}
Error Response (401):
{
"error": "Invalid email or password"
}
GET /api/auth/me
Retrieve the currently authenticated user's profile.
| Property | Value |
|---|---|
| Auth Required | Yes (any role) |
Request Headers:
Authorization: Bearer <token>
Success Response (200):
{
"id": 1,
"name": "John Doe",
"email": "user@example.com",
"role": "admin",
"status": "active",
"created_at": "2026-01-15T10:30:00.000Z"
}
Error Response (401):
{
"error": "Unauthorized"
}
POST /api/auth/forgot-password
Send a password reset email to the specified address.
| Property | Value |
|---|---|
| Auth Required | No |
Request Body:
{
"email": "user@example.com"
}
| Field | Type | Required | Description |
|---|---|---|---|
| string | Yes | Registered email address |
Success Response (200):
{
"message": "If an account exists with that email, a password reset link has been sent"
}
Notes:
- Always returns success message regardless of whether the email exists (prevents email enumeration).
- Reset email is sent via Resend HTTP API from
noreply@loftygoldenoil.com. - Reset tokens are time-limited and single-use.
POST /api/auth/reset-password
Reset a user's password using a valid reset token.
| Property | Value |
|---|---|
| Auth Required | No |
Request Body:
{
"token": "reset-token-from-email",
"password": "newsecurepassword"
}
| Field | Type | Required | Description |
|---|---|---|---|
| token | string | Yes | Password reset token from email |
| password | string | Yes | New password |
Success Response (200):
{
"message": "Password has been reset successfully"
}
Error Response (400):
{
"error": "Invalid or expired reset token"
}
User Management Endpoints
GET /api/users
List all users.
| Property | Value |
|---|---|
| Auth Required | Yes (admin only) |
Request Headers:
Authorization: Bearer <token>
Success Response (200):
[
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"role": "admin",
"status": "active",
"created_at": "2026-01-15T10:30:00.000Z"
}
]
POST /api/users
Create a new user account.
| Property | Value |
|---|---|
| Auth Required | Yes (admin only) |
Request Body:
{
"name": "New User",
"email": "newuser@example.com",
"password": "securepassword",
"role": "sales",
"status": "active"
}
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Full name |
| string | Yes | Unique email address | |
| password | string | Yes | Initial password (min 6 chars recommended) |
| role | string | Yes | One of: admin, production, sales, accountant |
| status | string | No | active (default) or inactive |
Success Response (201):
{
"id": 5,
"name": "New User",
"email": "newuser@example.com",
"role": "sales",
"status": "active"
}
Error Response (409):
{
"error": "Email already exists"
}
PUT /api/users/:id
Update an existing user account.
| Property | Value |
|---|---|
| Auth Required | Yes (admin only) |
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | integer | User ID |
Request Body:
{
"name": "Updated Name",
"role": "accountant",
"status": "inactive",
"password": "newpassword"
}
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | No | Updated name |
| string | No | Updated email | |
| role | string | No | Updated role |
| status | string | No | active or inactive |
| password | string | No | New password (only if changing) |
Success Response (200):
{
"id": 5,
"name": "Updated Name",
"email": "newuser@example.com",
"role": "accountant",
"status": "inactive"
}
Error Response (404):
{
"error": "User not found"
}
DELETE /api/users/:id
Delete a user account. Cannot delete your own account.
| Property | Value |
|---|---|
| Auth Required | Yes (admin only) |
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | integer | User ID |
Success Response (200):
{
"message": "User deleted successfully"
}
Error Response (400):
{
"error": "Cannot delete your own account"
}
Inventory Endpoints
GET /api/inventory
List inventory items with optional filtering.
| Property | Value |
|---|---|
| Auth Required | Yes (any role) |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| search | string | Search by name or SKU |
| category | string | Filter by category |
| status | string | Active or Inactive |
Example: GET /api/inventory?search=oil&category=Edible%20Oil&status=Active
Success Response (200):
[
{
"id": 1,
"name": "Palm Oil 5L",
"sku": "EO-PLM-005L",
"category": "Edible Oil",
"unit_of_measure": "Litres",
"current_stock": 1250.00,
"minimum_stock_level": 500.00,
"unit_cost": 3200.00,
"selling_price": 4500.00,
"status": "Active",
"description": "Premium refined palm oil",
"created_at": "2026-01-15T10:30:00.000Z",
"updated_at": "2026-01-20T14:22:00.000Z"
}
]
POST /api/inventory
Create a new inventory item.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, production) |
Request Body:
{
"name": "Soybean Oil 1L",
"sku": "EO-SOY-001L",
"category": "Edible Oil",
"unit_of_measure": "Litres",
"current_stock": 500.00,
"minimum_stock_level": 200.00,
"unit_cost": 2800.00,
"selling_price": 3800.00,
"status": "Active",
"description": "Refined soybean oil"
}
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Item name |
| sku | string | Yes | Unique SKU code |
| category | string | Yes | One of: Edible Oil, Packaging, Raw Material, Equipment, Supplies |
| unit_of_measure | string | Yes | One of: Litres, Kg, Cartons, Pieces, Rolls, Sachets |
| current_stock | number | No | Initial stock (default 0) |
| minimum_stock_level | number | No | Reorder threshold (default 0) |
| unit_cost | number | No | Cost per unit (default 0) |
| selling_price | number | No | Selling price (default 0) |
| status | string | No | Active (default) or Inactive |
| description | string | No | Item description |
Success Response (201):
{
"id": 3,
"name": "Soybean Oil 1L",
"sku": "EO-SOY-001L",
"category": "Edible Oil",
"unit_of_measure": "Litres",
"current_stock": 500.00,
"minimum_stock_level": 200.00,
"unit_cost": 2800.00,
"selling_price": 3800.00,
"status": "Active"
}
Error Response (409):
{
"error": "SKU already exists"
}
PUT /api/inventory/:id
Update an existing inventory item.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, production) |
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | integer | Inventory item ID |
Request Body: Same fields as POST, all optional.
Success Response (200): Updated inventory object.
Error Response (404):
{
"error": "Inventory item not found"
}
DELETE /api/inventory/:id
Delete an inventory item. Fails if item has linked purchases, sales, or adjustments.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, production) |
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | integer | Inventory item ID |
Success Response (200):
{
"message": "Inventory item deleted successfully"
}
Error Response (409):
{
"error": "Cannot delete inventory item with existing transactions"
}
POST /api/inventory/:id/adjust
Create a manual inventory adjustment.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, production) |
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | integer | Inventory item ID |
Request Body:
{
"adjustment_quantity": -25.50,
"reason": "Damaged goods during transport",
"notes": "Photos documented",
"adjustment_date": "2026-03-15"
}
| Field | Type | Required | Description |
|---|---|---|---|
| adjustment_quantity | number | Yes | Positive to add stock, negative to reduce |
| reason | string | Yes | Reason for adjustment |
| notes | string | No | Additional details |
| adjustment_date | string | No | Date (ISO format, default today) |
Success Response (201):
{
"message": "Inventory adjustment recorded successfully",
"adjustment": {
"id": 12,
"inventory_id": 1,
"adjustment_quantity": -25.50,
"reason": "Damaged goods during transport",
"adjustment_date": "2026-03-15",
"created_by": 1
},
"new_stock": 1224.50
}
GET /api/inventory/alerts
Get all inventory items currently at or below their minimum stock level.
| Property | Value |
|---|---|
| Auth Required | Yes (any role) |
Success Response (200):
[
{
"id": 3,
"name": "Soybean Oil 1L",
"sku": "EO-SOY-001L",
"current_stock": 150.00,
"minimum_stock_level": 200.00,
"unit_of_measure": "Litres",
"shortage": 50.00
}
]
Purchases Endpoints
GET /api/purchases
List all purchase records.
| Property | Value |
|---|---|
| Auth Required | Yes (any role) |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| inventory_id | integer | Filter by inventory item |
| start_date | string | Filter from date (ISO) |
| end_date | string | Filter to date (ISO) |
Success Response (200):
[
{
"id": 1,
"inventory_id": 1,
"inventory_name": "Palm Oil 5L",
"supplier_name": "West Africa Palm Products",
"quantity": 500.00,
"unit_cost": 3200.00,
"total_cost": 1600000.00,
"purchase_date": "2026-02-01",
"notes": "Monthly restock",
"created_at": "2026-02-01T09:00:00.000Z"
}
]
POST /api/purchases
Record a new purchase.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, production) |
Request Body:
{
"inventory_id": 1,
"supplier_name": "West Africa Palm Products",
"quantity": 500.00,
"unit_cost": 3200.00,
"total_cost": 1600000.00,
"purchase_date": "2026-02-01",
"notes": "Monthly restock"
}
| Field | Type | Required | Description |
|---|---|---|---|
| inventory_id | integer | Yes | Inventory item purchased |
| supplier_name | string | Yes | Supplier name |
| quantity | number | Yes | Quantity purchased |
| unit_cost | number | Yes | Cost per unit |
| total_cost | number | Yes | Total cost |
| purchase_date | string | No | ISO date string |
| notes | string | No | Notes |
Success Response (201):
{
"id": 25,
"inventory_id": 1,
"supplier_name": "West Africa Palm Products",
"quantity": 500.00,
"unit_cost": 3200.00,
"total_cost": 1600000.00,
"purchase_date": "2026-02-01"
}
Notes: Creating a purchase automatically increases inventory.current_stock by the purchased quantity.
PUT /api/purchases/:id
Update a purchase record.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, production) |
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | integer | Purchase ID |
Request Body: Same fields as POST, all optional.
Success Response (200): Updated purchase object.
DELETE /api/purchases/:id
Delete a purchase record.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, production) |
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | integer | Purchase ID |
Success Response (200):
{
"message": "Purchase deleted successfully"
}
Notes: Deleting a purchase reverses the stock increase on the associated inventory item.
Production Endpoints
GET /api/production
List production runs with optional date filtering.
| Property | Value |
|---|---|
| Auth Required | Yes (any role) |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| start_date | string | Filter from date (ISO) |
| end_date | string | Filter to date (ISO) |
| status | string | Filter by status |
Success Response (200):
[
{
"id": 10,
"product_name": "Refined Palm Oil",
"start_date": "2026-03-01",
"end_date": "2026-03-03",
"batches": 5,
"input_volume": 2500.00,
"output_volume": 2200.00,
"input_cost": 8000000.00,
"output_cost": 9900000.00,
"yield_percent": 88.00,
"cost_per_litre": 4500.00,
"status": "Completed",
"notes": "Batch processed successfully",
"created_at": "2026-03-01T08:00:00.000Z"
}
]
POST /api/production
Create a new production run.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, production) |
Request Body:
{
"product_name": "Refined Palm Oil",
"start_date": "2026-03-01",
"end_date": "2026-03-03",
"batches": 5,
"input_volume": 2500.00,
"output_volume": 2200.00,
"input_cost": 8000000.00,
"output_cost": 9900000.00,
"yield_percent": 88.00,
"cost_per_litre": 4500.00,
"status": "Completed",
"notes": "Batch processed successfully"
}
| Field | Type | Required | Description |
|---|---|---|---|
| product_name | string | Yes | Product name |
| start_date | string | Yes | ISO date |
| end_date | string | No | ISO date |
| batches | integer | No | Number of batches |
| input_volume | number | No | Input material volume |
| output_volume | number | No | Output product volume |
| input_cost | number | No | Total input cost |
| output_cost | number | No | Total output cost |
| yield_percent | number | No | Yield efficiency percentage |
| cost_per_litre | number | No | Cost per litre of output |
| status | string | No | One of: Planned, In Progress, Completed, Delayed |
| notes | string | No | Production notes |
Success Response (201): Created production run object.
PUT /api/production/:id
Update an existing production run.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, production) |
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | integer | Production run ID |
Request Body: Same fields as POST, all optional.
Success Response (200): Updated production run object.
DELETE /api/production/:id
Delete a production run.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, production) |
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | integer | Production run ID |
Success Response (200):
{
"message": "Production run deleted successfully"
}
Sales Endpoints
GET /api/sales
List sales with optional filtering.
| Property | Value |
|---|---|
| Auth Required | Yes (any role) |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| customer_id | integer | Filter by customer |
| inventory_id | integer | Filter by product |
| status | string | pending, completed, cancelled |
| start_date | string | Filter from date (ISO) |
| end_date | string | Filter to date (ISO) |
Success Response (200):
[
{
"id": 45,
"customer_id": 3,
"customer_name": "ABC Enterprises",
"inventory_id": 1,
"inventory_name": "Palm Oil 5L",
"quantity": 50.00,
"unit_price": 4500.00,
"total_amount": 225000.00,
"sale_date": "2026-03-10",
"status": "completed",
"notes": "Delivered to Ikeja warehouse",
"created_at": "2026-03-10T11:00:00.000Z"
}
]
POST /api/sales
Record a new sale.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, sales) |
Request Body:
{
"customer_id": 3,
"inventory_id": 1,
"quantity": 50.00,
"unit_price": 4500.00,
"total_amount": 225000.00,
"sale_date": "2026-03-10",
"status": "completed",
"notes": "Delivered to Ikeja warehouse"
}
| Field | Type | Required | Description |
|---|---|---|---|
| customer_id | integer | Yes | Customer ID |
| inventory_id | integer | Yes | Inventory item sold |
| quantity | number | Yes | Quantity sold |
| unit_price | number | Yes | Price per unit |
| total_amount | number | Yes | Total sale amount |
| sale_date | string | No | ISO date |
| status | string | No | pending, completed, cancelled |
| notes | string | No | Notes |
Success Response (201): Created sale object.
Notes: Creating a completed sale automatically decreases inventory.current_stock by the sold quantity.
PUT /api/sales/:id
Update an existing sale.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, sales) |
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | integer | Sale ID |
Request Body: Same fields as POST, all optional.
Success Response (200): Updated sale object.
DELETE /api/sales/:id
Delete a sale record.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, sales) |
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | integer | Sale ID |
Success Response (200):
{
"message": "Sale deleted successfully"
}
Notes: Deleting a sale with completed status reverses the stock deduction on the associated inventory item.
Customer Endpoints
GET /api/customers
List all customers.
| Property | Value |
|---|---|
| Auth Required | Yes (any role) |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| search | string | Search by company name or contact |
| status | string | Active or Inactive |
Success Response (200):
[
{
"id": 3,
"company_name": "ABC Enterprises",
"contact_person": "Adebayo Johnson",
"phone": "+234-801-234-5678",
"email": "adebayo@abc-enterprises.com",
"address": "15 Broad Street",
"city": "Lagos",
"state": "Lagos",
"credit_limit": 5000000.00,
"outstanding_balance": 1200000.00,
"status": "Active",
"notes": "Regular customer",
"created_at": "2026-01-15T10:30:00.000Z"
}
]
POST /api/customers
Create a new customer.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, sales) |
Request Body:
{
"company_name": "XYZ Distributors",
"contact_person": "Chioma Okafor",
"phone": "+234-802-345-6789",
"email": "chioma@xyz-dist.com",
"address": "22 Marina Road",
"city": "Lagos",
"state": "Lagos",
"credit_limit": 3000000.00,
"outstanding_balance": 0.00,
"status": "Active",
"notes": "New distributor"
}
| Field | Type | Required | Description |
|---|---|---|---|
| company_name | string | Yes | Company name |
| contact_person | string | No | Contact name |
| phone | string | No | Phone number |
| string | No | ||
| address | string | No | Address |
| city | string | No | City |
| state | string | No | State |
| credit_limit | number | No | Credit limit (default 0) |
| outstanding_balance | number | No | Outstanding balance (default 0) |
| status | string | No | Active (default) or Inactive |
| notes | string | No | Notes |
Success Response (201): Created customer object.
PUT /api/customers/:id
Update an existing customer.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, sales) |
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | integer | Customer ID |
Request Body: Same fields as POST, all optional.
Success Response (200): Updated customer object.
DELETE /api/customers/:id
Delete a customer. Fails if customer has existing sales.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, sales) |
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | integer | Customer ID |
Success Response (200):
{
"message": "Customer deleted successfully"
}
Error Response (409):
{
"error": "Cannot delete customer with existing sales"
}
Supplier Endpoints
GET /api/suppliers
List all suppliers.
| Property | Value |
|---|---|
| Auth Required | Yes (any role) |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| search | string | Search by company name |
| status | string | Active or Inactive |
Success Response (200):
[
{
"id": 2,
"company_name": "West Africa Palm Products",
"contact_person": "Emeka Nwosu",
"phone": "+234-803-456-7890",
"email": "emeka@wapp.com",
"address": "Port Harcourt Industrial Zone",
"city": "Port Harcourt",
"state": "Rivers",
"status": "Active",
"notes": "Primary palm oil supplier",
"created_at": "2026-01-15T10:30:00.000Z"
}
]
POST /api/suppliers
Create a new supplier.
| Property | Value |
|---|---|
| Auth Required | Yes (admin) |
Request Body:
{
"company_name": "Nigerian Packaging Solutions",
"contact_person": "Funke Adeyemi",
"phone": "+234-804-567-8901",
"email": "funke@npack.com",
"address": "Ogba Industrial Estate",
"city": "Lagos",
"state": "Lagos",
"status": "Active",
"notes": "Packaging materials supplier"
}
| Field | Type | Required | Description |
|---|---|---|---|
| company_name | string | Yes | Company name |
| contact_person | string | No | Contact name |
| phone | string | No | Phone |
| string | No | ||
| address | string | No | Address |
| city | string | No | City |
| state | string | No | State |
| status | string | No | Active (default) or Inactive |
| notes | string | No | Notes |
Success Response (201): Created supplier object.
PUT /api/suppliers/:id
Update an existing supplier.
| Property | Value |
|---|---|
| Auth Required | Yes (admin) |
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | integer | Supplier ID |
Request Body: Same fields as POST, all optional.
Success Response (200): Updated supplier object.
DELETE /api/suppliers/:id
Delete a supplier. Fails if supplier has existing purchases.
| Property | Value |
|---|---|
| Auth Required | Yes (admin) |
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | integer | Supplier ID |
Success Response (200):
{
"message": "Supplier deleted successfully"
}
Expenses Endpoints
GET /api/expenses
List expenses with optional filtering.
| Property | Value |
|---|---|
| Auth Required | Yes (any role) |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| status | string | pending, approved, rejected |
| category | string | Filter by category |
| start_date | string | Filter from date (ISO) |
| end_date | string | Filter to date (ISO) |
Success Response (200):
[
{
"id": 8,
"category": "Transport",
"description": "Fuel for delivery truck",
"amount": 45000.00,
"payment_method": "Cash",
"status": "approved",
"vendor": "Total Fuel Station",
"reference_number": "FUEL-2026-001",
"notes": "Monthly fuel top-up",
"expense_date": "2026-03-12",
"created_at": "2026-03-12T08:30:00.000Z"
}
]
POST /api/expenses
Create a new expense.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, accountant) |
Request Body:
{
"category": "Utilities",
"description": "Electricity bill - March 2026",
"amount": 125000.00,
"payment_method": "Transfer",
"status": "pending",
"vendor": "Ikeja Electric",
"reference_number": "IE-2026-03-001",
"notes": "Factory electricity",
"expense_date": "2026-03-15"
}
| Field | Type | Required | Description |
|---|---|---|---|
| category | string | Yes | Expense category |
| description | string | Yes | Description |
| amount | number | Yes | Amount |
| payment_method | string | Yes | Cash, Transfer, Cheque, Card |
| status | string | No | pending (default), approved, rejected |
| vendor | string | No | Vendor name |
| reference_number | string | No | Transaction reference |
| notes | string | No | Notes |
| expense_date | string | No | ISO date |
Success Response (201): Created expense object.
PUT /api/expenses/:id
Update an existing expense.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, accountant) |
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | integer | Expense ID |
Request Body: Same fields as POST, all optional.
Success Response (200): Updated expense object.
DELETE /api/expenses/:id
Delete an expense.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, accountant) |
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | integer | Expense ID |
Success Response (200):
{
"message": "Expense deleted successfully"
}
Accounts Receivable Endpoints
GET /api/ar/invoices
List AR invoices with optional filtering.
| Property | Value |
|---|---|
| Auth Required | Yes (any role) |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| status | string | pending, paid, overdue, partial |
| customer_id | integer | Filter by customer |
| start_date | string | Filter from date |
| end_date | string | Filter to date |
Success Response (200):
[
{
"id": 15,
"sale_id": 45,
"invoice_number": "INV-2026-0015",
"amount": 225000.00,
"status": "pending",
"due_date": "2026-04-10",
"notes": "Net 30 terms",
"created_at": "2026-03-10T11:00:00.000Z",
"customer_name": "ABC Enterprises"
}
]
POST /api/ar/invoices
Create a new AR invoice.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, sales) |
Request Body:
{
"sale_id": 45,
"invoice_number": "INV-2026-0015",
"amount": 225000.00,
"status": "pending",
"due_date": "2026-04-10",
"notes": "Net 30 terms"
}
| Field | Type | Required | Description |
|---|---|---|---|
| sale_id | integer | Yes | Related sale ID |
| invoice_number | string | Yes | Unique invoice number |
| amount | number | Yes | Invoice amount |
| status | string | No | pending (default), paid, overdue, partial |
| due_date | string | Yes | Payment due date (ISO) |
| notes | string | No | Notes |
Success Response (201): Created invoice object.
PUT /api/ar/invoices/:id
Update an existing invoice.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, sales) |
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | integer | Invoice ID |
Request Body: Same fields as POST, all optional.
Success Response (200): Updated invoice object.
DELETE /api/ar/invoices/:id
Delete an invoice. Fails if payments have been recorded against it.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, sales) |
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | integer | Invoice ID |
Success Response (200):
{
"message": "Invoice deleted successfully"
}
POST /api/ar/invoices/:id/send-email
Send the invoice to the customer via email.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, sales) |
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | integer | Invoice ID |
Success Response (200):
{
"message": "Invoice sent successfully"
}
Notes: Uses Resend HTTP API to send a formatted invoice email from noreply@loftygoldenoil.com to the customer's email address.
GET /api/ar/payments
List all AR payments.
| Property | Value |
|---|---|
| Auth Required | Yes (any role) |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| invoice_id | integer | Filter by invoice |
| start_date | string | Filter from date |
| end_date | string | Filter to date |
Success Response (200):
[
{
"id": 20,
"invoice_id": 15,
"amount": 100000.00,
"payment_method": "Transfer",
"reference_number": "TRF-2026-001",
"payment_date": "2026-03-25",
"notes": "Partial payment",
"created_at": "2026-03-25T14:00:00.000Z"
}
]
POST /api/ar/payments
Record a new payment against an invoice.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, sales) |
Request Body:
{
"invoice_id": 15,
"amount": 100000.00,
"payment_method": "Transfer",
"reference_number": "TRF-2026-001",
"payment_date": "2026-03-25",
"notes": "Partial payment"
}
| Field | Type | Required | Description |
|---|---|---|---|
| invoice_id | integer | Yes | Invoice ID |
| amount | number | Yes | Payment amount |
| payment_method | string | Yes | Cash, Transfer, Cheque, Card |
| reference_number | string | No | Transaction reference |
| payment_date | string | No | ISO date |
| notes | string | No | Notes |
Success Response (201):
{
"id": 20,
"invoice_id": 15,
"amount": 100000.00,
"payment_method": "Transfer",
"reference_number": "TRF-2026-001",
"payment_date": "2026-03-25",
"message": "Payment recorded. Invoice status updated."
}
Notes: Recording a payment updates the related invoice's status (e.g., partial, paid) and the customer's outstanding_balance.
DELETE /api/ar/payments/:id
Delete a payment record.
| Property | Value |
|---|---|
| Auth Required | Yes (admin, sales) |
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | integer | Payment ID |
Success Response (200):
{
"message": "Payment deleted successfully"
}
Notes: Deleting a payment reverses the invoice status update and adjusts the customer's outstanding balance.
Accounting Endpoints
GET /api/accounting/gl
Retrieve General Ledger entries.
| Property | Value |
|---|---|
| Auth Required | Yes (accountant) |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| start_date | string | Filter from date (ISO) |
| end_date | string | Filter to date (ISO) |
| account_code | string | Filter by account code |
Success Response (200):
[
{
"id": 100,
"entry_date": "2026-03-15",
"account_code": "1001",
"account_name": "Cash",
"debit": 0.00,
"credit": 225000.00,
"description": "Sale to ABC Enterprises - INV-2026-0015",
"reference": "SALE-45",
"created_at": "2026-03-15T11:00:00.000Z"
}
]
GET /api/accounting/trial-balance
Generate a trial balance report.
| Property | Value |
|---|---|
| Auth Required | Yes (accountant) |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| as_of_date | string | Date for the trial balance (ISO) |
Success Response (200):
{
"as_of_date": "2026-03-31",
"accounts": [
{
"account_code": "1001",
"account_name": "Cash",
"debit": 1500000.00,
"credit": 0.00,
"balance": 1500000.00
},
{
"account_code": "2001",
"account_name": "Accounts Payable",
"debit": 0.00,
"credit": 800000.00,
"balance": -800000.00
}
],
"total_debit": 1500000.00,
"total_credit": 1500000.00,
"is_balanced": true
}
GET /api/accounting/profit-loss
Generate a Profit & Loss statement.
| Property | Value |
|---|---|
| Auth Required | Yes (accountant) |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| start_date | string | Period start (ISO) |
| end_date | string | Period end (ISO) |
Success Response (200):
{
"period": {
"start_date": "2026-01-01",
"end_date": "2026-03-31"
},
"revenue": 25000000.00,
"cost_of_goods_sold": 18000000.00,
"gross_profit": 7000000.00,
"operating_expenses": 3500000.00,
"net_profit": 3500000.00
}
GET /api/accounting/balance-sheet
Generate a Balance Sheet report.
| Property | Value |
|---|---|
| Auth Required | Yes (accountant) |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| as_of_date | string | Date for the balance sheet (ISO) |
Success Response (200):
{
"as_of_date": "2026-03-31",
"assets": {
"current_assets": 12000000.00,
"fixed_assets": 25000000.00,
"total_assets": 37000000.00
},
"liabilities": {
"current_liabilities": 5000000.00,
"long_term_liabilities": 10000000.00,
"total_liabilities": 15000000.00
},
"equity": 22000000.00,
"total_liabilities_and_equity": 37000000.00
}
POST /api/accounting/journal-entries
Create a new journal entry.
| Property | Value |
|---|---|
| Auth Required | Yes (accountant) |
Request Body:
{
"entry_date": "2026-03-15",
"description": "Record sale to ABC Enterprises",
"lines": [
{
"account_code": "1001",
"debit": 225000.00,
"credit": 0.00
},
{
"account_code": "4001",
"debit": 0.00,
"credit": 225000.00
}
]
}
| Field | Type | Required | Description |
|---|---|---|---|
| entry_date | string | Yes | Date of the entry |
| description | string | Yes | Entry description |
| lines | array | Yes | At least 2 lines (debit + credit) |
| lines[].account_code | string | Yes | Account code |
| lines[].debit | number | Yes | Debit amount |
| lines[].credit | number | Yes | Credit amount |
Success Response (201):
{
"id": 200,
"entry_date": "2026-03-15",
"description": "Record sale to ABC Enterprises",
"lines": [
{ "account_code": "1001", "debit": 225000.00, "credit": 0.00 },
{ "account_code": "4001", "debit": 0.00, "credit": 225000.00 }
],
"total_debit": 225000.00,
"total_credit": 225000.00
}
Error Response (400):
{
"error": "Total debits must equal total credits"
}
GET /api/accounting/journal-entries
List journal entries.
| Property | Value |
|---|---|
| Auth Required | Yes (accountant) |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| start_date | string | Filter from date |
| end_date | string | Filter to date |
Success Response (200): Array of journal entry objects with their lines.
POST /api/accounting/journal-entries/:id/reverse
Reverse an existing journal entry.
| Property | Value |
|---|---|
| Auth Required | Yes (accountant) |
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | integer | Journal entry ID |
Success Response (201): New reversing journal entry object.
Notes: Creates a new journal entry with debits and credits swapped from the original. The original entry is marked as reversed.
POST /api/accounting/vouchers
Create a new payment voucher.
| Property | Value |
|---|---|
| Auth Required | Yes (accountant) |
Request Body:
{
"voucher_number": "PV-2026-0042",
"voucher_type": "payment",
"payee": "West Africa Palm Products",
"amount": 1600000.00,
"payment_method": "Transfer",
"description": "Payment for palm oil purchase - PO-2026-025",
"voucher_date": "2026-03-20",
"approved_by": "John Doe"
}
| Field | Type | Required | Description |
|---|---|---|---|
| voucher_number | string | Yes | Unique voucher number |
| voucher_type | string | Yes | payment or receipt |
| payee | string | Yes | Payee name |
| amount | number | Yes | Voucher amount |
| payment_method | string | Yes | Payment method |
| description | string | Yes | Description |
| voucher_date | string | Yes | ISO date |
| approved_by | string | No | Approver name |
Success Response (201): Created voucher object.
GET /api/accounting/vouchers
List payment vouchers.
| Property | Value |
|---|---|
| Auth Required | Yes (accountant) |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| voucher_type | string | payment or receipt |
| start_date | string | Filter from date |
| end_date | string | Filter to date |
Success Response (200): Array of voucher objects.
Dashboard Endpoints
GET /api/dashboard/stats
Retrieve key dashboard statistics.
| Property | Value |
|---|---|
| Auth Required | Yes (any role) |
Success Response (200):
{
"total_sales_this_month": 8500000.00,
"total_sales_last_month": 7200000.00,
"total_purchases_this_month": 5000000.00,
"pending_invoices": 12,
"overdue_invoices": 3,
"low_stock_items": 5,
"total_customers": 45,
"total_inventory_value": 35000000.00
}
GET /api/dashboard/charts
Retrieve chart data for dashboard visualizations.
| Property | Value |
|---|---|
| Auth Required | Yes (any role) |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| period | string | daily, weekly, monthly |
Success Response (200):
{
"sales_trend": [
{ "date": "2026-03-01", "amount": 250000.00 },
{ "date": "2026-03-02", "amount": 320000.00 }
],
"production_output": [
{ "month": "2026-01", "volume": 12000.00 },
{ "month": "2026-02", "volume": 14500.00 }
],
"category_breakdown": [
{ "category": "Edible Oil", "value": 15000000.00 },
{ "category": "Packaging", "value": 5000000.00 }
]
}
GET /api/dashboard/alerts
Retrieve system alerts and notifications.
| Property | Value |
|---|---|
| Auth Required | Yes (any role) |
Success Response (200):
{
"low_stock_alerts": [
{
"item": "Palm Oil 5L",
"sku": "EO-PLM-005L",
"current_stock": 150,
"minimum_stock": 500,
"shortage": 350
}
],
"overdue_invoices": [
{
"invoice_number": "INV-2026-0012",
"customer": "XYZ Distributors",
"amount": 450000.00,
"due_date": "2026-03-01",
"days_overdue": 29
}
],
"pending_requisitions": 3
}
Activity Endpoints
GET /api/activity
Retrieve audit log entries.
| Property | Value |
|---|---|
| Auth Required | Yes (admin) |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| start_date | string | Filter from date (ISO) |
| end_date | string | Filter to date (ISO) |
| entity_type | string | Filter by entity type |
| action | string | Filter by action type |
| limit | integer | Number of entries (default 100) |
Success Response (200):
[
{
"id": 1500,
"user_id": 1,
"user_name": "John Doe",
"action": "CREATE",
"entity_type": "sale",
"entity_id": 45,
"details": "Created sale: 50 units of Palm Oil 5L to ABC Enterprises",
"ip_address": "192.168.1.100",
"created_at": "2026-03-10T11:00:00.000Z"
}
]
Reports Endpoints
GET /api/reports/consolidated
Generate a consolidated business report.
| Property | Value |
|---|---|
| Auth Required | Yes (any role) |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| start_date | string | Report period start (ISO) |
| end_date | string | Report period end (ISO) |
Success Response (200):
{
"period": { "start_date": "2026-01-01", "end_date": "2026-03-31" },
"sales_summary": { "total": 25000000.00, "count": 120 },
"purchase_summary": { "total": 18000000.00, "count": 35 },
"expense_summary": { "total": 3500000.00, "count": 48 },
"production_summary": { "total_runs": 15, "total_output": 45000.00 },
"accounts_receivable": { "total_outstanding": 5000000.00, "overdue": 1200000.00 },
"inventory_summary": { "total_value": 35000000.00, "items": 85 }
}
GET /api/customers/:id/statement
Generate a customer account statement.
| Property | Value |
|---|---|
| Auth Required | Yes (any role) |
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
| id | integer | Customer ID |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| start_date | string | Statement period start (ISO) |
| end_date | string | Statement period end (ISO) |
Success Response (200):
{
"customer": {
"id": 3,
"company_name": "ABC Enterprises",
"credit_limit": 5000000.00,
"outstanding_balance": 1200000.00
},
"period": { "start_date": "2026-01-01", "end_date": "2026-03-31" },
"opening_balance": 800000.00,
"transactions": [
{
"date": "2026-03-10",
"type": "sale",
"reference": "SALE-45",
"description": "Palm Oil 5L x 50",
"debit": 225000.00,
"credit": 0.00,
"balance": 1025000.00
},
{
"date": "2026-03-25",
"type": "payment",
"reference": "TRF-2026-001",
"description": "Bank transfer",
"debit": 0.00,
"credit": 100000.00,
"balance": 925000.00
}
],
"closing_balance": 925000.00
}