Worked examples combining the conventions from API Conventions, API Authentication, and Authorization. All examples use sanitized/placeholder values — substitute your own token and tenant id.

1. Log in and obtain a token

curl -X POST https://bareledger.app/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "password": "<PASSWORD>"}'

Response (no 2FA enrolled):

{
  "token": "<ACCESS_TOKEN>",
  "user": { "uuid": "...", "email": "user@example.com" }
}

If the account has 2FA enrolled, the response instead returns a challenge; resubmit with a totp_code field to receive the token.

2. Make an authenticated, tenant-scoped request

curl https://bareledger.app/api/v1/journals?status=submitted \
  -H "Authorization: Bearer <ACCESS_TOKEN>" \
  -H "X-Tenant: <TENANT_UUID>"

X-Tenant is required on every tenant-scoped endpoint — omitting it, or sending one you don't belong to, returns 409.

3. Create a resource and handle validation errors

curl -X POST https://bareledger.app/api/v1/journals \
  -H "Authorization: Bearer <ACCESS_TOKEN>" \
  -H "X-Tenant: <TENANT_UUID>" \
  -H "Content-Type: application/json" \
  -d '{
    "transaction_date": "2026-08-01",
    "currency": "PHP",
    "lines": [
      { "account_id": "<ACCOUNT_UUID_1>", "debit": 1000.00, "credit": 0 },
      { "account_id": "<ACCOUNT_UUID_2>", "debit": 0, "credit": 1000.00 }
    ]
  }'

A validation failure (e.g. unbalanced lines on submit, not on draft save) returns:

422 Unprocessable Entity
{
  "message": "The given data was invalid.",
  "errors": { "lines": ["Total debits must equal total credits."] }
}

4. Handle pagination

Pass page and per_page to get one page and a meta block; keep requesting the next page until current_page reaches last_page. Without them, most lists return every matching row.

curl "https://bareledger.app/api/v1/invoices?page=2&per_page=25" \
  -H "Authorization: Bearer <ACCESS_TOKEN>" \
  -H "X-Tenant: <TENANT_UUID>"
{ "data": [ /* 25 items */ ], "meta": { "current_page": 2, "last_page": 6, "per_page": 25, "total": 137 } }

5. Handle a permission denial

403 Forbidden
{ "message": "You do not have permission to perform this action.", "permission": "sales.approve" }

The permission field tells you exactly which key was missing — useful for surfacing a precise "you need X permission" message in a client app rather than a generic error.

6. Handle a lapsed subscription

402 Payment Required
{ "message": "An active subscription is required to perform this action." }

Reads and */billing/* endpoints remain available even in this state — a client can still show data and offer a way to resubscribe.

7. Verify a billing webhook (server-side integration pattern)

Both Paddle and PayMongo webhooks are verified via HMAC signature, not a bearer token — see Integration — Paddle / Integration — PayMongo for the exact header names and algorithm. Pseudocode:

import hmac, hashlib

def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_header)

Never trust a webhook payload whose signature doesn't verify — both gateways fail closed in this system's own implementation, and any external consumer replicating similar logic should do the same.