API Conventions
This page documents the common patterns, error model, and status codes used across all Ledgerline API endpoints.
Internal endpoints: GET /info and GET /api/info return deployment metadata (masked env, build info) for ops verification. They are not part of the customer-facing API contract.
Prerequisites
- Familiarity with RESTful APIs and JSON.
- An authenticated session and tenant context (see Authentication and Tenant Scoping).
Admin-only Fields
Some resource fields are stripped from customer-facing API output. A field marked admin-only will appear in the admin panel and in internal responses, but never in public endpoints.
Current admin-only fields:
product.costCents— cost basis.productPublicSchemaomits this; any endpoint intended for non-admin consumers must serialize through that schema or explicitly omit the field.
When designing a new resource with sensitive fields, expose a {resource}PublicSchema alongside the main schema so downstream consumers can pick the right shape.
CRUD Pattern
All data resources follow a consistent CRUD pattern:
| Operation | Method | Path | Description |
|---|---|---|---|
| Create | POST | /api/data/{resource} | Create a new record. |
| Read | GET | /api/data/{resource}/:id | Retrieve a single record by ID. |
| Update | PATCH | /api/data/{resource}/:id | Partially update a record. |
| Delete | DELETE | /api/data/{resource}/:id | Remove a record. |
| List | GET | /api/data/{resource} | List all records for the current tenant. |
Example: Create an Order
POST /api/data/order
Content-Type: application/json
x-tenant-id: default
Cookie: ledgerline_auth=...
{
"orderNumber": "ORD-001",
"items": [
{
"productId": "prod_abc",
"name": "Widget",
"sku": "WDG-001",
"quantity": 2,
"unitPriceCents": 1500
}
],
"currency": "USD",
"customer": {
"name": "Jane Doe",
"discordId": "123456789"
}
}
Response (201):
{
"id": "order_xyz",
"tenantId": "default",
"orderNumber": "ORD-001",
"status": "PENDING_PAYMENT",
"items": [...],
"subtotalCents": 3000,
"totalCents": 3000,
"discountAmountCents": 0,
"currency": "USD",
"customer": { "name": "Jane Doe", "discordId": "123456789" },
"shippingStatus": "PENDING",
"createdAt": "2025-01-15T12:00:00.000Z",
"updatedAt": "2025-01-15T12:00:00.000Z"
}
Example: Update an Order
PATCH /api/data/order/order_xyz
Content-Type: application/json
x-tenant-id: default
Cookie: ledgerline_auth=...
{
"status": "PAID",
"notes": "Payment verified via Cash App"
}
Example: Delete an Order
DELETE /api/data/order/order_xyz
x-tenant-id: default
Cookie: ledgerline_auth=...
Response (204): No content.
Request Headers
| Header | Required | Description |
|---|---|---|
Content-Type | Yes (for POST/PATCH) | Must be application/json. |
x-tenant-id | Yes | Tenant identifier. See Tenant Scoping. |
Cookie | Yes | Session cookie (ledgerline_auth). |
Server-Side Computed Fields
Certain fields are computed by the server and should not be provided by the client on create:
subtotalCents— Sum ofquantity × unitPriceCentsfor all items.taxAmountCents— Sales tax computed via TaxJar using the shipping address. Defaults to0when TaxJar is not configured or no shipping address is present.totalCents—(subtotal − discount) + shipping + tax.discountAmountCents— Calculated from the applied discount.tenantId— Injected from the request context.createdAt/updatedAt— Managed by the server.
Error Model
Error responses return a JSON object with an error message:
{
"error": "Validation failed",
"details": [
{ "field": "items", "message": "Array must contain at least 1 element(s)" }
]
}
The structure may vary by endpoint, but all errors include at least an error or message field.
Status Codes
| Code | Meaning |
|---|---|
200 | Success (read, update, list). |
201 | Created (successful create). |
204 | No content (successful delete). |
400 | Bad request — validation error, missing fields, or business rule violation. |
401 | Unauthorized — no active session. |
404 | Not found — record does not exist or belongs to another tenant. |
500 | Internal server error. |
Validation
Request bodies are validated using Zod schemas. Invalid input returns 400 with details about which fields failed validation.
Common validation rules:
- String fields must be non-empty where required.
priceCents/amountCentsmust be non-negative integers.currencymust be a 3-character ISO 4217 code.- Enum fields (e.g.,
status,type) must be one of the allowed values. - Email fields must be valid email addresses.
Pagination
List endpoints currently return all records for the tenant without pagination. For tenants with large datasets, use filtering or query parameters if available.
Idempotency
POST /api/data/customeris idempotent bydiscordId: if a customer with the same Discord ID exists, the existing record is returned with200instead of creating a duplicate.POST /api/data/ordergenerates a unique order; callers should avoid submitting the same order payload twice.
Analytics share-link tokens
Analytics share links use HMAC-signed tokens — not session cookies. They are public endpoints and require no Authorization header.
| Endpoint | Auth | Notes |
|---|---|---|
POST /api/admin/analytics/:report/share | Admin session | Generates a token |
GET /api/reports/:token | None (HMAC) | HTML report page |
GET /api/reports/:token/csv | None (HMAC) | CSV download |
Token format: base64url(JSON payload) + '.' + base64url(HMAC-SHA256 sig)
The token is opaque to callers. Do not attempt to parse or forge it. Generate tokens via POST /api/admin/analytics/:report/share from an authenticated admin session.