Skeleton CRM REST API Documentation
All API requests require authentication (except registration/login):
Authorization: Bearer <access_token>POST /api/v1/auth/login
Content-Type: application/json
{
"email": "admin@skeleton.local",
"password": "03041965"
}Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": "01234567-...",
"email": "admin@skeleton.local",
"name": "Admin User",
"roles": ["super_admin"]
}
}POST /api/v1/auth/refresh
Content-Type: application/json
{
"refresh_token": "eyJhbGciOiJIUzI1NiIs..."
}GET /api/v1/customers?page=2&limit=20&sort_by=name&sort_order=ascResponse:
{
"items": [...],
"total": 150,
"page": 2,
"limit": 20,
"has_more": true
}All monetary values use int64 cents internally:
{
"total": 10000, // $100.00 USD (in cents)
"currency": "USD"
}All PUT and PATCH update endpoints use partial update (nullable pointer) semantics:
- Omit a field from the request body β the field is left unchanged (not cleared)
- Send a value β the field is updated to that value
- Send
null(for nullable fields) β the field is explicitly cleared/set to empty
For example, to update only the phone number of a customer:
{
"phone": "+1-555-0200"
}All other fields (name, email, address, etc.) remain unchanged.
To clear an optional field:
{
"phone": null
}Address fields follow the same pattern within the address sub-object:
{
"address": {
"city": "Boston",
"region": null
}
}This updates city to "Boston" and clears region, while keeping street, postal_code, and country unchanged.
Frontend should use a Money utility that handles cents-based arithmetic to avoid floating-point errors:
// web/shared/types/money.ts
const total = Money.fromCents(10000, 'USD')
total.toFloat() // 100.00
total.format() // "$100.00"{
"error": "validation_error",
"message": "Invalid input",
"details": {
"email": "Invalid email format"
}
}POST /api/v1/auth/register
Content-Type: application/json
{
"email": "user@example.com",
"password": "SecurePass123!",
"name": "John Doe"
}Response: 201 Created
{
"access_token": "...",
"refresh_token": "...",
"user": { ... }
}POST /api/v1/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "SecurePass123!"
}POST /api/v1/auth/refresh
Content-Type: application/json
{
"refresh_token": "..."
}POST /api/v1/auth/logout
Authorization: Bearer <token>GET /api/v1/auth/me
Authorization: Bearer <token>GET /api/v1/users?page=1&limit=20
Authorization: Bearer <token>Required Permission: users:read
Response:
{
"items": [
{
"id": "01234567-...",
"email": "user@example.com",
"name": "John Doe",
"active": true,
"created_at": "2026-01-01T00:00:00Z"
}
],
"total": 50,
"page": 1,
"limit": 20
}GET /api/v1/users/:id
Authorization: Bearer <token>POST /api/v1/users/:id/roles
Authorization: Bearer <token>
Content-Type: application/json
{
"role_id": "..."
}Required Permission: roles:manage
PATCH /api/v1/users/:id/deactivate
Authorization: Bearer <token>Required Permission: users:write
PATCH /api/v1/users/:id/activate
Authorization: Bearer <token>Required Permission: users:write
Reactivates a previously deactivated user account. Returns 409 Conflict if the user is already active.
PATCH /api/v1/users/:id
Authorization: Bearer <token>
Content-Type: application/json
{
"email": "newemail@example.com"
}Required Permission: users:write
Updates user profile fields. All fields are optional β omit a field to leave it unchanged. The email field uses nullable-pointer semantics: send a value to update it, omit it to keep the current email. The email must be unique β returns 409 Conflict if another user already has the same email. Returns the updated user object on success.
GET /api/v1/roles
Authorization: Bearer <token>Required Permission: roles:read
Response:
{
"data": [
{
"id": "...",
"name": "admin",
"description": "Administrator",
"permissions": ["users:read", "users:write", "roles:read", "roles:manage"],
"created_at": "2026-01-01T00:00:00Z"
}
]
}GET /api/v1/customers?page=1&limit=20&sort_by=name&sort_order=asc
Authorization: Bearer <token>Response:
{
"items": [
{
"id": "01234567-...",
"name": "Acme Corp",
"email": "contact@acme.com",
"phone": "+1-555-0100",
"credit_limit": 50000,
"total_purchases": 125000,
"currency": "USD",
"active": true,
"created_at": "2026-01-01T00:00:00Z"
}
],
"total": 250,
"page": 1,
"limit": 20
}POST /api/v1/customers
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "Acme Corporation",
"email": "contact@acme.com",
"phone": "+1-555-0100",
"credit_limit": 50000,
"currency": "USD",
"addresses": [
{
"type": "billing",
"street": "123 Main St",
"city": "New York",
"state": "NY",
"postal_code": "10001",
"country": "US"
}
]
}GET /api/v1/customers/:id
Authorization: Bearer <token>PUT /api/v1/customers/:id
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "Acme Corporation Updated",
"email": "new-contact@acme.com",
"tax_id": "US-123456789",
"phone": "+1-555-0200",
"address": {
"street": "456 Oak Ave",
"city": "Boston",
"region": "MA",
"postal_code": "02101",
"country": "01972b15-8000-7000-8000-000000000001"
},
"website": "https://acme.example.com",
"social_media": { "twitter": "@acme" }
}Required Permission: customers:write
All fields use partial update semantics:
- Omit a field β leave it unchanged
- Send a value β update it
- Send
null(for nullable fields liketax_id,phone,website, and all address fields) β clear the field
Required fields: name, email (if provided, must be non-empty).
GET /api/v1/suppliers?status=active&search=acme&limit=20
Authorization: Bearer <token>POST /api/v1/suppliers
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "SupplyCo",
"tax_id": "US-987654321",
"email": "sales@supplyco.com",
"phone": "+1-555-0300",
"address": {
"street": "789 Industrial Rd",
"city": "Chicago",
"region": "IL",
"postal_code": "60601",
"country": "01972b15-8000-7000-8000-000000000001"
},
"website": "https://supplyco.example.com"
}Required fields: name, email, address.street, address.city, address.country
GET /api/v1/suppliers/:id
Authorization: Bearer <token>PUT /api/v1/suppliers/:id
Authorization: Bearer <token>
Content-Type: application/json
{
"email": "new-sales@supplyco.com",
"phone": "+1-555-0399",
"address": {
"city": "Indianapolis"
}
}Required Permission: parties:write
Same partial update semantics as Update Customer. All fields except email are nullable β omit to keep unchanged, send null to clear.
PATCH /api/v1/suppliers/:id/activate
PATCH /api/v1/suppliers/:id/deactivate
PATCH /api/v1/suppliers/:id/blacklist
Authorization: Bearer <token>GET /api/v1/partners?status=active&limit=20
Authorization: Bearer <token>POST /api/v1/partners
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "PartnerInc",
"tax_id": "US-111222333",
"email": "biz@partnerinc.com",
"phone": "+1-555-0400",
"address": {
"street": "321 Partnership Blvd",
"city": "Seattle",
"region": "WA",
"postal_code": "98101",
"country": "01972b15-8000-7000-8000-000000000001"
}
}Required fields: name, email, address.street, address.city, address.country
GET /api/v1/partners/:id
Authorization: Bearer <token>PUT /api/v1/partners/:id
Authorization: Bearer <token>
Content-Type: application/json
{
"email": "new-biz@partnerinc.com",
"tax_id": "US-444555666"
}Required Permission: parties:write
Same partial update semantics as Update Customer.
PATCH /api/v1/partners/:id/activate
PATCH /api/v1/partners/:id/deactivate
PATCH /api/v1/partners/:id/blacklist
Authorization: Bearer <token>GET /api/v1/employees?status=active&limit=20
Authorization: Bearer <token>POST /api/v1/employees
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "John Smith",
"tax_id": "SS-123456789",
"position": "Sales Manager",
"email": "john@acme.com",
"phone": "+1-555-0500",
"address": {
"street": "555 Employee St",
"city": "Portland",
"region": "OR",
"postal_code": "97201",
"country": "01972b15-8000-7000-8000-000000000001"
}
}Required fields: name, position, email, address.street, address.city, address.country
GET /api/v1/employees/:id
Authorization: Bearer <token>PUT /api/v1/employees/:id
Authorization: Bearer <token>
Content-Type: application/json
{
"position": "Senior Sales Manager",
"phone": "+1-555-0599",
"address": {
"city": "Eugene"
}
}Required Permission: parties:write
Same partial update semantics. position, tax_id, phone, website, and all address fields are nullable β omit to keep unchanged, send null to clear.
PATCH /api/v1/employees/:id/activate
PATCH /api/v1/employees/:id/deactivate
PATCH /api/v1/employees/:id/blacklist
Authorization: Bearer <token>GET /api/v1/countries
Authorization: Bearer <token>Response:
{
"data": [
{
"id": "01972b15-8000-7000-8000-000000000001",
"code": "UA",
"alpha3": "UKR",
"numeric_code": "804",
"name": { "en": "Ukraine", "uk": "Π£ΠΊΡΠ°ΡΠ½Π°", "de": "Ukraine" },
"phone_code": "+380",
"currency_code": "UAH",
"region": "Europe",
"flag_emoji": "πΊπ¦",
"is_active": true,
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-01T00:00:00Z"
}
]
}Note: List endpoint returns all countries as a flat array (not paginated).
GET /api/v1/countries/:id
Authorization: Bearer <token>Returns the same country object as in the list response.
POST /api/v1/countries
Authorization: Bearer <token>
Content-Type: application/json
{
"code": "PL",
"name": { "en": "Poland", "uk": "ΠΠΎΠ»ΡΡΠ°", "de": "Polen" },
"alpha3": "POL",
"numeric_code": "616",
"phone_code": "+48",
"currency_code": "PLN",
"region": "Europe",
"flag_emoji": "π΅π±"
}Required fields: code (ISO 3166-1 alpha-2), name (object with at least en key)
Optional fields: alpha3, numeric_code, phone_code, currency_code, region, flag_emoji
PUT /api/v1/countries/:id
Authorization: Bearer <token>
Content-Type: application/json
{
"name": { "en": "Poland", "uk": "ΠΠΎΠ»ΡΡΠ°", "de": "Polen" },
"alpha3": "POL",
"phone_code": "+48"
}Uses partial update semantics: only fields present in the request body are updated. Omitted fields retain their current values. String fields use pointer pattern (null = not changed, empty string = clear field).
Required Permission: countries:write
PATCH /api/v1/countries/:id/activate
PATCH /api/v1/countries/:id/deactivate
Authorization: Bearer <token>GET /api/v1/invoices?status=draft&page=1&limit=20
Authorization: Bearer <token>Query Parameters:
status- Filter by status:draft,sent,paid,cancelledcustomer_id- Filter by customerfrom_date,to_date- Filter by date range
Response:
{
"items": [
{
"id": "01234567-...",
"number": "INV-2026-001",
"customer_id": "...",
"customer_name": "Acme Corp",
"total": 10000,
"currency": "USD",
"status": "sent",
"due_date": "2026-02-15",
"created_at": "2026-01-15T00:00:00Z"
}
],
"total": 45,
"page": 1,
"limit": 20
}POST /api/v1/invoices
Authorization: Bearer <token>
Content-Type: application/json
{
"customer_id": "01234567-...",
"due_date": "2026-02-15",
"notes": "Thank you for your business",
"lines": [
{
"description": "Consulting services",
"quantity": 10,
"unit_price": 15000,
"currency": "USD"
},
{
"description": "Software license",
"quantity": 1,
"unit_price": 5000,
"currency": "USD"
}
]
}Response: 201 Created
{
"id": "...",
"number": "INV-2026-002",
"customer_id": "...",
"total": 155000,
"subtotal": 155000,
"currency": "USD",
"status": "draft",
"lines": [ ... ]
}GET /api/v1/invoices/:id
Authorization: Bearer <token>POST /api/v1/invoices/:id/lines
Authorization: Bearer <token>
Content-Type: application/json
{
"description": "Additional service",
"quantity": 5,
"unit_price": 2000,
"currency": "USD"
}POST /api/v1/invoices/:id/send
Authorization: Bearer <token>Changes status from draft to sent.
POST /api/v1/invoices/:id/payments
Authorization: Bearer <token>
Content-Type: application/json
{
"amount": 50000,
"currency": "USD",
"method": "bank_transfer",
"reference": "TXN-12345"
}POST /api/v1/invoices/:id/cancel
Authorization: Bearer <token>GET /api/v1/orders?status=pending&page=1&limit=20
Authorization: Bearer <token>Query Parameters:
status- Filter by status:draft,pending,confirmed,shipped,delivered,cancelledcustomer_id- Filter by customer
POST /api/v1/orders
Authorization: Bearer <token>
Content-Type: application/json
{
"customer_id": "...",
"shipping_address": {
"street": "456 Oak St",
"city": "Los Angeles",
"state": "CA",
"postal_code": "90001",
"country": "US"
},
"lines": [
{
"item_id": "...",
"quantity": 5,
"unit_price": 10000,
"currency": "USD"
}
]
}PATCH /api/v1/orders/:id/status
Authorization: Bearer <token>
Content-Type: application/json
{
"status": "confirmed"
}GET /api/v1/accounts?type=asset&page=1&limit=20
Authorization: Bearer <token>Query Parameters:
type- Filter by type:asset,liability,equity,revenue,expense
POST /api/v1/accounts
Authorization: Bearer <token>
Content-Type: application/json
{
"code": "1010",
"name": "Cash",
"type": "asset",
"parent_id": "..." // optional for hierarchy
}POST /api/v1/transactions
Authorization: Bearer <token>
Content-Type: application/json
{
"description": "Invoice payment",
"lines": [
{
"account_id": "1010", // Cash
"debit": 10000
},
{
"account_id": "4000", // Revenue
"credit": 10000
}
]
}Note: Debits must equal credits (double-entry bookkeeping).
GET /api/v1/contracts?status=active&contract_type=supply&limit=20
Authorization: Bearer <token>Query Parameters:
status- Filter by status:draft,pending_approval,active,expired,terminatedcontract_type- Filter by type:supply,service,employment,partnership,lease,licenseparty_id- Filter by party IDcursor- Pagination cursor (UUID v7)limit- Items per page (default: 20, max: 100)
Response:
{
"data": {
"items": [
{
"id": "019d75e8-...",
"contract_type": "supply",
"status": "active",
"party_id": "019d75e7-...",
"payment_terms": {
"payment_type": "credit",
"credit_days": 30,
"penalty_rate": 0.5,
"discount_rate": 2,
"currency": "UAH"
},
"delivery_terms": {
"delivery_type": "delivery",
"estimated_days": 5,
"shipping_cost": 5000000,
"insurance": true,
"shipping_currency": "UAH"
},
"validity_period": {
"start_date": "2026-01-15",
"end_date": "2027-01-14"
},
"credit_limit": 500000000,
"currency": "UAH",
"auto_renewal": true,
"renewal_period_days": 30,
"renewal_count": 0,
"max_renewals": 3,
"version": 1,
"created_by": "019d75e7-...",
"created_at": "2026-01-20T10:00:00+02:00",
"updated_at": "2026-01-20T10:00:00+02:00",
"signed_at": "2026-01-20T10:00:00+02:00"
}
],
"next_cursor": "019d75e8-...",
"has_more": true,
"limit": 20
}
}POST /api/v1/contracts
Authorization: Bearer <token>
Content-Type: application/json
{
"contract_type": "supply",
"party_id": "019d75e7-...",
"payment_type": "credit",
"credit_days": 30,
"currency": "UAH",
"delivery_type": "delivery",
"estimated_days": 5,
"start_date": "2026-01-15",
"end_date": "2027-01-14",
"credit_limit": 500000000
}Response (201):
{
"data": {
"id": "019d75e8-..."
}
}GET /api/v1/contracts/:id
Authorization: Bearer <token>POST /api/v1/contracts/:id/activate
Authorization: Bearer <token>
Content-Type: application/json
{
"signed_at": "2026-01-20T10:00:00+02:00"
}POST /api/v1/contracts/:id/terminate
Authorization: Bearer <token>
Content-Type: application/json
{
"reason": "Breach of terms"
}POST /api/v1/contracts/:id/renew
Authorization: Bearer <token>
Content-Type: application/json
{
"new_end_date": "2028-01-14"
}PUT /api/v1/contracts/:id/payment-terms
Authorization: Bearer <token>
Content-Type: application/json
{
"payment_type": "credit",
"credit_days": 45,
"currency": "UAH"
}PUT /api/v1/contracts/:id/delivery-terms
Authorization: Bearer <token>
Content-Type: application/json
{
"delivery_type": "delivery",
"estimated_days": 7
}Notes:
credit_limitis stored as BIGINT cents in the database (0 = no limit)shipping_costin delivery_terms is also in centspayment_typevalues:prepaid,postpaid,creditdelivery_typevalues:delivery,pickup,digitalvalidity_periodis a PostgreSQL DATERANGE, returned as{start_date, end_date}object
GET /api/v1/warehouses
Authorization: Bearer <token>POST /api/v1/warehouses
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "Main Warehouse",
"location": "New York, NY",
"capacity": 10000
}GET /api/v1/stock?warehouse_id=...&item_id=...
Authorization: Bearer <token>POST /api/v1/stock/:id/adjust
Authorization: Bearer <token>
Content-Type: application/json
{
"quantity": 100,
"reason": "Inventory count adjustment"
}POST /api/v1/stock/transfer
Authorization: Bearer <token>
Content-Type: application/json
{
"item_id": "...",
"from_warehouse_id": "...",
"to_warehouse_id": "...",
"quantity": 50
}GET /api/v1/catalog/items?category_id=...
Authorization: Bearer <token>POST /api/v1/catalog/items
Authorization: Bearer <token>
Content-Type: application/json
{
"sku": "PROD-001",
"name": "Product Name",
"description": "Product description",
"category_id": "...",
"unit_price": 5000,
"currency": "USD",
"status": "active"
}POST /api/v1/files
Authorization: Bearer <token>
Content-Type: multipart/form-data
file: <binary>Response:
{
"id": "...",
"name": "document.pdf",
"mime_type": "application/pdf",
"size": 102400,
"url": "/api/v1/files/.../download",
"created_at": "..."
}GET /api/v1/files?page=1&limit=20
Authorization: Bearer <token>DELETE /api/v1/files/:id
Authorization: Bearer <token>GET /api/v1/notifications?unread_only=true
Authorization: Bearer <token>GET /api/v1/notifications/:id
Authorization: Bearer <token>GET /api/v1/notifications/preferences
Authorization: Bearer <token>PATCH /api/v1/notifications/preferences
Authorization: Bearer <token>
Content-Type: application/json
{
"email_enabled": true,
"sms_enabled": false,
"push_enabled": true,
"quiet_hours_start": "22:00",
"quiet_hours_end": "08:00"
}POST /api/v1/conversations
Authorization: Bearer <token>
Content-Type: application/json
{
"type": "group",
"name": "Project Team",
"member_ids": ["user-id-1", "user-id-2"]
}Required fields: type (direct/group), member_ids (at least 1)
GET /api/v1/conversations?type=group&limit=20
Authorization: Bearer <token>GET /api/v1/conversations/:id
Authorization: Bearer <token>PUT /api/v1/conversations/:id/group
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "Updated Group Name",
"description": "Updated description"
}Partial update semantics: Omit name or description to leave it unchanged. Send null to clear the description.
PATCH /api/v1/conversations/:id/archive
Authorization: Bearer <token>POST /api/v1/conversations/:id/members
Authorization: Bearer <token>
Content-Type: application/json
{ "user_id": "..." }
DELETE /api/v1/conversations/:id/members
Authorization: Bearer <token>
Content-Type: application/json
{ "user_id": "..." }POST /api/v1/conversations/:id/messages
Authorization: Bearer <token>
Content-Type: application/json
{ "content": "Hello!", "attachments": [] }<resource>:<action>
Examples:
- users:read
- users:write
- users:delete
- invoices:* // all actions on invoices
- *:* // super admin (all permissions)
*:* // Full access to everything
users:read, users:write
roles:read, roles:manage
invoices:*, orders:*
accounting:*, inventory:*
catalog:*, files:*
notifications:*, audit:read
parties:read, parties:write
countries:read, countries:write
messenger:read, messenger:write
users:read
invoices:read, orders:read
inventory:read, catalog:read
files:read
notifications:read
parties:read
countries:read
messenger:read
# 1. Create customer
curl -X POST http://localhost:8080/api/v1/customers \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Corp",
"email": "contact@acme.com",
"credit_limit": 100000,
"currency": "USD"
}'
# Response: {"id": "cust-123", ...}
# 2. Create invoice
curl -X POST http://localhost:8080/api/v1/invoices \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "cust-123",
"due_date": "2026-02-15",
"lines": [
{
"description": "Services",
"quantity": 1,
"unit_price": 50000,
"currency": "USD"
}
]
}'
# Response: {"id": "inv-456", "number": "INV-2026-001", ...}
# 3. Send invoice
curl -X POST http://localhost:8080/api/v1/invoices/inv-456/send \
-H "Authorization: Bearer <token>"
# 4. Record payment
curl -X POST http://localhost:8080/api/v1/invoices/inv-456/payments \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"amount": 50000,
"currency": "USD",
"method": "bank_transfer"
}'| Code | Description |
|---|---|
| 400 | Bad Request - Invalid input |
| 401 | Unauthorized - Authentication required |
| 403 | Forbidden - Insufficient permissions |
| 404 | Not Found - Resource doesn't exist |
| 409 | Conflict - Resource already exists |
| 422 | Unprocessable Entity - Validation failed |
| 429 | Too Many Requests - Rate limit exceeded |
| 500 | Internal Server Error |
| 503 | Service Unavailable |
Interactive API documentation available at:
- Swagger UI: http://localhost:8080/swagger/index.html
- OpenAPI Spec: http://localhost:8080/swagger/doc.json
API Version: 0.1.0
Base URL: /api/v1
Content-Type: application/json