Skip to content

Latest commit

Β 

History

History
1521 lines (1185 loc) Β· 26.4 KB

File metadata and controls

1521 lines (1185 loc) Β· 26.4 KB

API Reference

Skeleton CRM REST API Documentation

πŸ” Authentication

All API requests require authentication (except registration/login):

Authorization: Bearer <access_token>

Get 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"]
  }
}

Refresh Token

POST /api/v1/auth/refresh
Content-Type: application/json

{
  "refresh_token": "eyJhbGciOiJIUzI1NiIs..."
}

πŸ“Š Common Patterns

Pagination

GET /api/v1/customers?page=2&limit=20&sort_by=name&sort_order=asc

Response:

{
  "items": [...],
  "total": 150,
  "page": 2,
  "limit": 20,
  "has_more": true
}

Money Format

All monetary values use int64 cents internally:

{
  "total": 10000,      // $100.00 USD (in cents)
  "currency": "USD"
}

Partial Updates

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 Responses

{
  "error": "validation_error",
  "message": "Invalid input",
  "details": {
    "email": "Invalid email format"
  }
}

🎯 API Endpoints

Authentication

Register User

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": { ... }
}

Login

POST /api/v1/auth/login
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "SecurePass123!"
}

Refresh Token

POST /api/v1/auth/refresh
Content-Type: application/json

{
  "refresh_token": "..."
}

Logout

POST /api/v1/auth/logout
Authorization: Bearer <token>

Get Current User

GET /api/v1/auth/me
Authorization: Bearer <token>

Users

List Users

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 User

GET /api/v1/users/:id
Authorization: Bearer <token>

Assign Role

POST /api/v1/users/:id/roles
Authorization: Bearer <token>
Content-Type: application/json

{
  "role_id": "..."
}

Required Permission: roles:manage

Deactivate User

PATCH /api/v1/users/:id/deactivate
Authorization: Bearer <token>

Required Permission: users:write

Activate User

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.

Update User

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.

List Roles

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"
    }
  ]
}

Customers

List Customers

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
}

Create Customer

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 Customer

GET /api/v1/customers/:id
Authorization: Bearer <token>

Update Customer

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 like tax_id, phone, website, and all address fields) β†’ clear the field

Required fields: name, email (if provided, must be non-empty).


Suppliers

List Suppliers

GET /api/v1/suppliers?status=active&search=acme&limit=20
Authorization: Bearer <token>

Create Supplier

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 Supplier

GET /api/v1/suppliers/:id
Authorization: Bearer <token>

Update Supplier

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.

Activate/Deactivate/Blacklist Supplier

PATCH /api/v1/suppliers/:id/activate
PATCH /api/v1/suppliers/:id/deactivate
PATCH /api/v1/suppliers/:id/blacklist
Authorization: Bearer <token>

Partners

List Partners

GET /api/v1/partners?status=active&limit=20
Authorization: Bearer <token>

Create Partner

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 Partner

GET /api/v1/partners/:id
Authorization: Bearer <token>

Update Partner

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.

Activate/Deactivate/Blacklist Partner

PATCH /api/v1/partners/:id/activate
PATCH /api/v1/partners/:id/deactivate
PATCH /api/v1/partners/:id/blacklist
Authorization: Bearer <token>

Employees

List Employees

GET /api/v1/employees?status=active&limit=20
Authorization: Bearer <token>

Create Employee

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 Employee

GET /api/v1/employees/:id
Authorization: Bearer <token>

Update Employee

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.

Activate/Deactivate/Blacklist Employee

PATCH /api/v1/employees/:id/activate
PATCH /api/v1/employees/:id/deactivate
PATCH /api/v1/employees/:id/blacklist
Authorization: Bearer <token>

Countries

List Countries

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 Country

GET /api/v1/countries/:id
Authorization: Bearer <token>

Returns the same country object as in the list response.

Create Country

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

Update Country

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

Activate/Deactivate Country

PATCH /api/v1/countries/:id/activate
PATCH /api/v1/countries/:id/deactivate
Authorization: Bearer <token>

Invoices

List Invoices

GET /api/v1/invoices?status=draft&page=1&limit=20
Authorization: Bearer <token>

Query Parameters:

  • status - Filter by status: draft, sent, paid, cancelled
  • customer_id - Filter by customer
  • from_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
}

Create Invoice

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 Invoice

GET /api/v1/invoices/:id
Authorization: Bearer <token>

Add Invoice Line

POST /api/v1/invoices/:id/lines
Authorization: Bearer <token>
Content-Type: application/json

{
  "description": "Additional service",
  "quantity": 5,
  "unit_price": 2000,
  "currency": "USD"
}

Send Invoice

POST /api/v1/invoices/:id/send
Authorization: Bearer <token>

Changes status from draft to sent.

Record Payment

POST /api/v1/invoices/:id/payments
Authorization: Bearer <token>
Content-Type: application/json

{
  "amount": 50000,
  "currency": "USD",
  "method": "bank_transfer",
  "reference": "TXN-12345"
}

Cancel Invoice

POST /api/v1/invoices/:id/cancel
Authorization: Bearer <token>

Orders

List Orders

GET /api/v1/orders?status=pending&page=1&limit=20
Authorization: Bearer <token>

Query Parameters:

  • status - Filter by status: draft, pending, confirmed, shipped, delivered, cancelled
  • customer_id - Filter by customer

Create Order

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"
    }
  ]
}

Update Order Status

PATCH /api/v1/orders/:id/status
Authorization: Bearer <token>
Content-Type: application/json

{
  "status": "confirmed"
}

Accounting

List Accounts

GET /api/v1/accounts?type=asset&page=1&limit=20
Authorization: Bearer <token>

Query Parameters:

  • type - Filter by type: asset, liability, equity, revenue, expense

Create Account

POST /api/v1/accounts
Authorization: Bearer <token>
Content-Type: application/json

{
  "code": "1010",
  "name": "Cash",
  "type": "asset",
  "parent_id": "..."  // optional for hierarchy
}

Record Transaction

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).


Contracts

List Contracts

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, terminated
  • contract_type - Filter by type: supply, service, employment, partnership, lease, license
  • party_id - Filter by party ID
  • cursor - 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
  }
}

Create Contract

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 Contract

GET /api/v1/contracts/:id
Authorization: Bearer <token>

Activate Contract

POST /api/v1/contracts/:id/activate
Authorization: Bearer <token>
Content-Type: application/json

{
  "signed_at": "2026-01-20T10:00:00+02:00"
}

Terminate Contract

POST /api/v1/contracts/:id/terminate
Authorization: Bearer <token>
Content-Type: application/json

{
  "reason": "Breach of terms"
}

Renew Contract

POST /api/v1/contracts/:id/renew
Authorization: Bearer <token>
Content-Type: application/json

{
  "new_end_date": "2028-01-14"
}

Update Payment Terms

PUT /api/v1/contracts/:id/payment-terms
Authorization: Bearer <token>
Content-Type: application/json

{
  "payment_type": "credit",
  "credit_days": 45,
  "currency": "UAH"
}

Update Delivery Terms

PUT /api/v1/contracts/:id/delivery-terms
Authorization: Bearer <token>
Content-Type: application/json

{
  "delivery_type": "delivery",
  "estimated_days": 7
}

Notes:

  • credit_limit is stored as BIGINT cents in the database (0 = no limit)
  • shipping_cost in delivery_terms is also in cents
  • payment_type values: prepaid, postpaid, credit
  • delivery_type values: delivery, pickup, digital
  • validity_period is a PostgreSQL DATERANGE, returned as {start_date, end_date} object

Inventory

List Warehouses

GET /api/v1/warehouses
Authorization: Bearer <token>

Create Warehouse

POST /api/v1/warehouses
Authorization: Bearer <token>
Content-Type: application/json

{
  "name": "Main Warehouse",
  "location": "New York, NY",
  "capacity": 10000
}

List Stock

GET /api/v1/stock?warehouse_id=...&item_id=...
Authorization: Bearer <token>

Adjust Stock

POST /api/v1/stock/:id/adjust
Authorization: Bearer <token>
Content-Type: application/json

{
  "quantity": 100,
  "reason": "Inventory count adjustment"
}

Transfer Stock

POST /api/v1/stock/transfer
Authorization: Bearer <token>
Content-Type: application/json

{
  "item_id": "...",
  "from_warehouse_id": "...",
  "to_warehouse_id": "...",
  "quantity": 50
}

Catalog

List Items

GET /api/v1/catalog/items?category_id=...
Authorization: Bearer <token>

Create Item

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"
}

Files

Upload File

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": "..."
}

List Files

GET /api/v1/files?page=1&limit=20
Authorization: Bearer <token>

Delete File

DELETE /api/v1/files/:id
Authorization: Bearer <token>

Notifications

List Notifications

GET /api/v1/notifications?unread_only=true
Authorization: Bearer <token>

Get Notification

GET /api/v1/notifications/:id
Authorization: Bearer <token>

Get Notification Preferences

GET /api/v1/notifications/preferences
Authorization: Bearer <token>

Update Preferences

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"
}

Messenger

Create Conversation

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)

List Conversations

GET /api/v1/conversations?type=group&limit=20
Authorization: Bearer <token>

Get Conversation

GET /api/v1/conversations/:id
Authorization: Bearer <token>

Update Group Info

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.

Archive Conversation

PATCH /api/v1/conversations/:id/archive
Authorization: Bearer <token>

Add/Remove Member

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": "..." }

Send Message

POST /api/v1/conversations/:id/messages
Authorization: Bearer <token>
Content-Type: application/json

{ "content": "Hello!", "attachments": [] }

πŸ”’ Permissions

Permission Format

<resource>:<action>

Examples:
- users:read
- users:write
- users:delete
- invoices:*          // all actions on invoices
- *:*                  // super admin (all permissions)

Default Roles

super_admin

*:*  // Full access to everything

admin

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

viewer

users:read
invoices:read, orders:read
inventory:read, catalog:read
files:read
notifications:read
parties:read
countries:read
messenger:read

πŸ“ Request/Response Examples

Example: Create Customer with Invoice

# 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"
  }'

🚫 Error Codes

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

πŸ“š OpenAPI/Swagger

Interactive API documentation available at:


API Version: 0.1.0
Base URL: /api/v1
Content-Type: application/json