Introduction

Base URL and request format
Base URL:
https://yourdomain.com/repair-angel-v2/api

All requests must include the following headers:

// Install: composer require guzzlehttp/guzzle
use GuzzleHttp\Client;

$client = new Client([
    'base_uri' => 'https://yourdomain.com/repair-angel-v2/api/',
    'headers' => [
        'Accept'        => 'application/json',
        'Authorization' => 'Bearer ' . $token,
    ],
]);
# Install: pip install requests
import requests

BASE = 'https://yourdomain.com/repair-angel-v2/api'
headers = {
    'Accept': 'application/json',
    'Authorization': f'Bearer {token}',
}
const BASE = 'https://yourdomain.com/repair-angel-v2/api';
const headers = {
  'Accept': 'application/json',
  'Content-Type': 'application/json',
  'Authorization': `Bearer ${token}`,
};

Authentication

Token-based authentication via Laravel Sanctum
POST /auth/login Public Login

Returns a bearer token. Pass this token in the Authorization header for all subsequent requests.

FieldTypeDescription
email *stringUser email address
password *stringUser password
$res = $client->post('auth/login', [
    'json' => [
        'email'    => 'admin@example.com',
        'password' => 'secret',
    ],
]);
$token = json_decode($res->getBody(), true)['token'];
r = requests.post(f'{BASE}/auth/login', json={
    'email': 'admin@example.com',
    'password': 'secret',
})
token = r.json()['token']
const res = await fetch(`${BASE}/auth/login`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email: 'admin@example.com', password: 'secret' }),
});
const { token } = await res.json();

Response 200:

{
  "token": "1|abc123...",
  "user": { "id": 1, "name": "Admin", "email": "admin@example.com" }
}
POST /auth/logout Auth Logout

Revokes the current bearer token.

{ "message": "Logged out" }
GET /auth/me Auth Current user

Returns the authenticated user's profile.

{
  "id": 1, "name": "John Doe", "email": "john@example.com",
  "role": "admin", "company_id": 5
}

Error Codes

Standard HTTP status codes used across all endpoints
CodeMeaningCommon Cause
200OKSuccessful GET / PATCH
201CreatedSuccessful POST (resource created)
204No ContentSuccessful DELETE
401UnauthorizedMissing or invalid token
403ForbiddenInsufficient permission
404Not FoundResource ID does not exist
422Validation ErrorInvalid / missing required fields
500Server ErrorUnexpected server-side error
// 422 response example
{
  "message": "The given data was invalid.",
  "errors": {
    "email": ["The email field is required."]
  }
}

Work Orders

Core repair workflow — create, manage and track repair jobs
GET /work-orders Auth List work orders
Query ParamTypeDescription
statusstringFilter by status slug
customer_idintegerFilter by customer
assigned_tointegerFilter by employee
qstringFull-text search (ticket no, customer, device)
pageintegerPage number
$res = $client->get('work-orders', [
    'query' => ['status' => 'in_repair', 'page' => 1],
]);
$orders = json_decode($res->getBody(), true);
r = requests.get(f'{BASE}/work-orders',
    params={'status': 'in_repair'}, headers=headers)
orders = r.json()
const res = await fetch(`${BASE}/work-orders?status=in_repair`, { headers });
const orders = await res.json();
POST /work-orders Auth Create work order
FieldTypeDescription
customer_id *integerCustomer ID
device_brandstringBrand/make of device
device_modelstringModel of device
problem_descriptionstringReported fault description
prioritystringnormal | high | urgent
assigned_tointegerEmployee ID
expected_ready_atdatetimeEstimated completion date
$res = $client->post('work-orders', ['json' => [
    'customer_id'          => 42,
    'device_brand'         => 'Apple',
    'device_model'         => 'iPhone 15 Pro',
    'problem_description'  => 'Cracked screen',
    'priority'             => 'normal',
]]);
r = requests.post(f'{BASE}/work-orders', headers=headers, json={
    'customer_id': 42,
    'device_brand': 'Apple',
    'device_model': 'iPhone 15 Pro',
    'problem_description': 'Cracked screen',
})
const res = await fetch(`${BASE}/work-orders`, {
  method: 'POST', headers,
  body: JSON.stringify({
    customer_id: 42, device_brand: 'Apple',
    device_model: 'iPhone 15 Pro', problem_description: 'Cracked screen',
  }),
});
GET /work-orders/{id} Auth Get work order
{
  "id": 101, "ticket_no": "WO-2026-0101",
  "customer": { "id": 42, "name": "Jane Smith" },
  "device_brand": "Apple", "device_model": "iPhone 15 Pro",
  "status": { "slug": "in_repair", "label": "In Repair" },
  "lines": [], "notes": [], "created_at": "2026-03-27T10:00:00Z"
}
POST /work-orders/{id}/lines Auth Add work order line (part/labour)
FieldTypeDescription
description *stringLine description
product_idintegerOptional linked product
quantitynumberQuantity (default 1)
unit_price *numberPrice per unit excl. VAT
vat_ratenumberVAT % (default 21)
POST /work-orders/{id}/notes Auth Add internal note
FieldTypeDescription
note *stringNote content
is_internalbooleanHide from customer (default true)

Customers

Individual customer records
GET /customers Auth List customers
QueryTypeDescription
qstringSearch by name, email, or phone
company_idintegerFilter by company
POST /customers Auth Create customer
FieldTypeDescription
first_name *string
last_namestring
emailstring
phonestring
addressstring
citystring
postal_codestring
company_idintegerLink to company
PUT /customers/{id} Auth Update customer

Same fields as POST. All fields optional.

DELETE /customers/{id} Auth Delete customer

Returns 204 No Content on success.

Invoices

Customer invoices — create, update, send
GET /invoices Auth List invoices
QueryTypeDescription
statusstringdraft | sent | paid | overdue | cancelled
customer_idinteger
fromdateInvoice date from
todateInvoice date to
POST /invoices Auth Create invoice
FieldTypeDescription
customer_id *integer
work_order_idintegerLink to work order
invoice_date *date
due_datedatePayment due date
linesarrayArray of line objects {description, qty, unit_price, vat_rate}
$res = $client->post('invoices', ['json' => [
    'customer_id'   => 42,
    'invoice_date'  => '2026-03-27',
    'due_date'      => '2026-04-27',
    'lines'         => [[
        'description' => 'Screen replacement',
        'qty'         => 1,
        'unit_price'  => 89.00,
        'vat_rate'    => 21,
    ]],
]]);
r = requests.post(f'{BASE}/invoices', headers=headers, json={
    'customer_id': 42,
    'invoice_date': '2026-03-27',
    'lines': [{ 'description': 'Screen replacement', 'qty': 1, 'unit_price': 89.00, 'vat_rate': 21 }],
})
await fetch(`${BASE}/invoices`, {
  method: 'POST', headers,
  body: JSON.stringify({
    customer_id: 42, invoice_date: '2026-03-27',
    lines: [{ description: 'Screen replacement', qty: 1, unit_price: 89, vat_rate: 21 }],
  }),
});

Products

Product catalog — parts, accessories, services
GET /products Auth List products
QueryTypeDescription
qstringSearch by name or SKU
category_idinteger
in_stockbooleanOnly products with stock > 0
POST /products Auth Create product
FieldTypeDescription
name *string
skustringStock keeping unit
price_excl *numberSelling price excl. VAT
purchase_pricenumberCost price
vat_ratenumberDefault 21
stock_quantityintegerInitial stock
min_stockintegerReorder threshold

Stock

Stock levels and adjustments
GET /stock Auth Current stock levels
[
  { "product_id": 1, "sku": "SCR-IP15-BLK", "name": "iPhone 15 Screen", "quantity": 12, "min_stock": 3 },
  ...
]
POST /stock/adjust Auth Manual stock adjustment
FieldTypeDescription
product_id *integer
quantity *integerPositive (add) or negative (remove)
reasonstringAdjustment reason note

Warehouse (WMS)

Multi-location warehouse management — bins, receiving, putaway, picking
GET /bin-stock/by-bin/{bin_id} Auth Stock in a bin location
[ { "product_id": 5, "sku": "CAB-USB-C", "quantity": 30, "bin": "A-01-02" }, ... ]

Receiving

Receive goods against purchase orders or directly
POST /receiving/from-po/{purchase_order_id} Auth Receive against purchase order
FieldTypeDescription
lines *array[{po_line_id, received_qty}]
POST /receiving/direct Auth Direct receive (no PO)
FieldTypeDescription
product_id *integer
quantity *integer
supplier_idinteger
referencestringDelivery note / reference

Pick Lists

Generate and execute pick lists for work orders
POST /pick-lists/from-work-order Auth Generate pick list from work order
FieldTypeDescription
work_order_id *integer
POST /pick-lists/{id}/complete Auth Complete pick list

Marks the pick list as done and deducts stock from bins.

General Ledger

Chart of accounts, journal entries, and period closings
GET /gl-accounts Auth List GL accounts
[ { "id": 1, "code": "1000", "name": "Kas", "type": "asset" }, ... ]
POST /journal-entries Auth Create journal entry
FieldTypeDescription
date *dateBooking date
descriptionstring
lines *array[{account_id, debit, credit, description}]

BTW Aangifte

Dutch VAT return periods
GET /btw-periods Auth List BTW periods
[ { "id": 1, "year": 2026, "quarter": 1, "status": "open", "total_vat": 4821.00 } ]
POST /btw-periods/{id}/close Auth Close BTW period

Marks the period as filed. No further entries can be posted.

Employees

Employee records and HR data
GET /employees Auth List employees
[ { "id": 1, "name": "Alice B.", "role": "Technician", "email": "alice@shop.nl" }, ... ]

Leave Management

Leave requests and balances
POST /leave-requests Auth Submit leave request
FieldTypeDescription
employee_id *integer
leave_type_id *integer
start_date *date
end_date *date
notesstring

License Management

SaaS license provisioning and administration
GET /license/current Auth Get current tenant license
{
  "id": 1,
  "license_type": "SMR",
  "tier": "growth",
  "status": "active",
  "valid_until": "2027-03-27",
  "max_users": 15,
  "price_monthly": "149.00",
  "modules": [
    { "module_key": "work_orders", "is_active": true },
    { "module_key": "invoices", "is_active": true },
    ...
  ]
}
POST /licenses Auth Create license (admin)
FieldTypeDescription
license_type *stringSMR | AUTO | AVIATION | MARINE
tier *stringstarter | growth | enterprise
valid_from *date
valid_untildate
billing_cyclestringmonthly | annual
company_idintegerLink to company
POST /licenses/{id}/activate Auth Activate license

Changes status from trial to active.

POST /licenses/{id}/change-tier Auth Upgrade / downgrade tier
FieldTypeDescription
tier *stringstarter | growth | enterprise

License Modules

Activate or deactivate individual feature modules
GET /licenses/{id}/modules Auth List available modules
[
  { "module_key": "work_orders", "available": true, "is_active": true, "activated_at": "2026-01-01T00:00:00Z" },
  { "module_key": "accounting", "available": true, "is_active": false, "activated_at": null },
  ...
]
POST /licenses/{id}/modules/activate Auth Activate module
FieldTypeDescription
module_key *stringe.g. accounting, hrm, warehouse
GET /license/check-module?module={key} Auth Check if tenant has module
{ "has_module": true }

UPS Shipping

Create shipment labels and track parcels via UPS API
POST /ups/label Auth Create UPS shipping label
FieldTypeDescription
ship_to_name *string
ship_to_address *string
ship_to_city *string
ship_to_postal *string
ship_to_countrystringISO2, default NL
work_order_idintegerLinks label to work order
is_returnbooleanReturn label
weightnumberPackage weight in kg
GET /ups/track/{tracking_code} Auth Track shipment
{ "status": "In Transit", "activities": [ { "location": "Amsterdam", "description": "Departed facility", "date": "2026-03-27" } ] }

VIES VAT Check

Validate EU VAT numbers via the European Commission VIES service
POST /vies/validate Auth Validate VAT number
FieldTypeDescription
vat_number *stringe.g. NL123456789B01
country_codestringISO2, auto-detected if omitted
invoice_idintegerLog result against invoice
{ "is_valid": true, "name": "ACME BV", "vat_number": "NL123456789B01", "country_code": "NL" }

API Tokens

Manage programmatic access tokens
POST /api-tokens Auth Create API token
FieldTypeDescription
name *stringToken label (e.g. "Zapier Integration")
abilitiesarrayPermission scopes (default ["*"])
{ "token": "5|xKqz9...", "name": "Zapier Integration" }
Important: The plain-text token is only shown once. Store it securely immediately.
DELETE /api-tokens/{id} Auth Revoke token

Immediately revokes the token. Returns 204.

Notification Center

In-app, email and push notifications — prefix: /api/v1/notifications
GET /notifications Auth List notifications

Returns paginated notifications for the authenticated user.

Query ParamTypeDescription
unreadintegerPass 1 to return only unread notifications
typestringFilter by type: work_order | invoice | task | sla | customer | system
pageintegerPage number
{
  "data": [
    {
      "id": 55,
      "type": "work_order",
      "title": "WO-2026-0055 status changed",
      "body": "Work order moved to In Repair",
      "read_at": null,
      "created_at": "2026-03-27T09:12:00Z"
    }
  ],
  "current_page": 1, "last_page": 3, "total": 112
}
POST /notifications/{id}/read Auth Mark single notification read

Marks a specific notification as read. Returns 200 with updated notification.

POST /notifications/mark-all-read Auth Mark all notifications read
{ "message": "All notifications marked as read", "count": 34 }
DELETE /notifications/{id} Auth Delete notification

Permanently deletes the notification. Returns 204 No Content.

GET /notifications/unread Auth Unread count
{ "count": 7 }
GET /notifications/preferences Auth Get notification preferences

Returns the user's per-event notification channel preferences.

{
  "work_orders": [
    { "key": "wo_status_changed", "label": "Status changed", "channels": ["in_app", "email"] },
    { "key": "wo_assigned",       "label": "Engineer assigned", "channels": ["in_app"] }
  ],
  "invoices": [ { "key": "invoice_overdue", "label": "Invoice overdue", "channels": ["in_app", "email", "push"] } ],
  "tasks": [...],
  "sla": [...],
  "system": [...]
}
PUT /notifications/preferences Auth Update notification preferences
FieldTypeDescription
preferences *objectMap of event key → array of channels (in_app, email, push)
// Request body
{
  "preferences": {
    "wo_status_changed": ["in_app", "email"],
    "invoice_overdue":   ["in_app", "push"]
  }
}
POST /notifications/test Auth Send test notification
FieldTypeDescription
channel *stringin_app | email | push
{ "message": "Test notification sent via in_app" }

Tutorial System

Role-based onboarding tutorials and step tracking — prefix: /api/v1/tutorials
GET /tutorials Auth List tutorials

Returns tutorials available to the current user's role. Admins receive all tutorials.

[
  { "id": 1, "title": "Getting Started", "role": "all", "total_steps": 5 },
  { "id": 2, "title": "Creating a Work Order", "role": "technician", "total_steps": 7 }
]
GET /tutorials/{id} Auth Tutorial detail with steps
{
  "id": 1, "title": "Getting Started",
  "steps": [
    { "id": 1, "order": 1, "title": "Welcome", "body": "...", "completed": true },
    { "id": 2, "order": 2, "title": "Dashboard overview", "body": "...", "completed": false }
  ]
}
GET /tutorials/progress Auth All tutorial progress
[
  {
    "tutorial": { "id": 1, "title": "Getting Started" },
    "total_steps": 5, "completed_steps": 3,
    "percent": 60, "completed": false
  }
]
POST /tutorials/step Auth Mark step complete
FieldTypeDescription
tutorial_id *integerTutorial ID
step_id *integerStep ID to mark complete
{ "message": "Step marked complete", "percent": 40, "completed": false }
POST /tutorials/{id}/steps/complete Auth Mark step complete (alt route)
FieldTypeDescription
step_id *integerStep ID to mark complete
POST /tutorials/{id}/reset Auth Reset tutorial progress

Clears all completed steps for the given tutorial for the authenticated user.

{ "message": "Tutorial progress reset" }
GET /tutorials/settings Auth Get tutorial settings
{
  "tutorials_enabled": true,
  "completed_onboarding": false,
  "dismissed_hints": ["dashboard_hint", "wo_hint"]
}
PUT /tutorials/settings Auth Update tutorial settings
FieldTypeDescription
tutorials_enabledbooleanEnable or disable the tutorial system
completed_onboardingbooleanMark onboarding as finished
dismissed_hintsarrayArray of hint keys to dismiss
POST /tutorials/seed Auth Seed default tutorials (Admin only)
Admin only. Seeds the 14 default system tutorials. Existing tutorials are not duplicated.
{ "message": "14 tutorials seeded" }

Work Order Timeline / Audit

Immutable audit log and internal notes per work order — prefix: /api/v1/work-orders/{id}/timeline
GET /work-orders/{id}/timeline Auth Get timeline / audit log
Query ParamTypeDescription
eventstringFilter by event type: created | field_changed | note_added | engineer_assigned | status_changed | completed
pageintegerPage number
{
  "data": [
    {
      "id": 301,
      "event": "status_changed",
      "description": "Status changed from New to In Repair",
      "user": { "id": 3, "name": "Jan Bakker" },
      "created_at": "2026-03-27T08:45:00Z"
    }
  ],
  "current_page": 1, "last_page": 2, "total": 18
}
POST /work-orders/{id}/timeline/note Auth Add internal note
FieldTypeDescription
note *stringInternal note text (not visible to customer)
{
  "id": 302, "event": "note_added",
  "description": "Waiting for spare part from supplier.",
  "user": { "id": 3, "name": "Jan Bakker" },
  "created_at": "2026-03-27T09:00:00Z"
}

Quotations

Create and manage repair/service quotations — prefix: /api/v1/quotations
GET /quotations Auth List quotations
Query ParamTypeDescription
statusstringdraft | sent | accepted | rejected | expired
pageintegerPage number
POST /quotations Auth Create quotation
FieldTypeDescription
customer_id *integerCustomer ID
work_order_idintegerAssociated work order (optional)
valid_untildateExpiry date (YYYY-MM-DD)
lines *arrayArray of line items [{description, qty, unit_price, vat_pct}]
notesstringCustomer-facing notes
// Request body
{
  "customer_id": 42,
  "valid_until": "2026-04-30",
  "lines": [
    { "description": "Screen replacement", "qty": 1, "unit_price": 120.00, "vat_pct": 21 }
  ]
}
GET /quotations/{id} Auth Quotation detail

Returns quotation with full line items and customer info.

PUT /quotations/{id} Auth Update quotation

Update quotation fields. Only draft quotations may be fully edited; sent quotations allow notes-only updates.

DELETE /quotations/{id} Auth Delete quotation

Deletes the quotation. Only draft quotations can be deleted. Returns 204.

POST /quotations/{id}/send Auth Send quotation to customer

Sends the quotation PDF via email to the customer and sets status to sent.

{ "message": "Quotation sent", "status": "sent" }
POST /quotations/{id}/convert-to-invoice Auth Convert quotation to invoice

Creates an invoice from the accepted quotation lines and marks the quotation as accepted.

{ "invoice_id": 78, "message": "Invoice created from quotation" }
POST /quotations/{id}/convert-to-work-order Auth Convert quotation to work order
{ "work_order_id": 203, "message": "Work order created from quotation" }

Tasks

Lightweight task management with Kanban support — prefix: /api/v1/tasks
GET /tasks Auth List tasks
Query ParamTypeDescription
statusstringopen | in_progress | done | cancelled
assigned_tointegerFilter by assignee employee ID
pageintegerPage number
POST /tasks Auth Create task
FieldTypeDescription
title *stringTask title
descriptionstringTask details
assigned_tointegerEmployee ID
due_datedateDue date (YYYY-MM-DD)
statusstringInitial status (default: open)
work_order_idintegerLink to work order (optional)
PUT /tasks/{id} Auth Update task

Update any task field. Partial updates supported.

DELETE /tasks/{id} Auth Delete task

Permanently deletes the task. Returns 204.

GET /tasks/kanban Auth Kanban board view

Returns tasks grouped by status for Kanban board rendering.

[
  { "status": "open",        "tasks": [{ "id": 10, "title": "Order spare parts" }] },
  { "status": "in_progress", "tasks": [{ "id": 11, "title": "Call customer" }] },
  { "status": "done",        "tasks": [] }
]
POST /tasks/reorder Auth Reorder / move task
FieldTypeDescription
task_id *integerTask to move
position *integerNew sort position within column
status *stringTarget column status
{ "message": "Task reordered" }

Automation Rules

Trigger-based automation for work orders, invoices and notifications — prefix: /api/v1/automation-rules
GET /automation-rules Auth List automation rules
[
  {
    "id": 1, "name": "Auto-close after 30 days",
    "trigger": "wo_idle", "active": true,
    "last_run_at": "2026-03-26T23:00:00Z"
  }
]
POST /automation-rules Auth Create automation rule
FieldTypeDescription
name *stringDescriptive rule name
trigger *stringEvent that triggers the rule
conditionsarrayArray of condition objects [{field, operator, value}]
actions *arrayArray of action objects [{type, params}]
activebooleanEnable rule immediately (default: true)
PUT /automation-rules/{id} Auth Update automation rule

Update rule definition. Partial updates supported.

DELETE /automation-rules/{id} Auth Delete automation rule

Permanently deletes the rule. Returns 204.

POST /automation-rules/{id}/toggle Auth Toggle rule active state
{ "active": false, "message": "Rule deactivated" }
GET /automation-rules/{id}/logs Auth Rule execution log

Paginated execution history for the rule, including outcome and any errors.

{
  "data": [
    { "ran_at": "2026-03-26T23:00:00Z", "outcome": "success", "affected_records": 3 }
  ]
}

Business Intelligence

Analytics and KPI dashboards — prefix: /api/v1/bi
GET /bi/revenue Auth Revenue overview
Query ParamTypeDescription
periodstring7d | 30d | 90d | jaar (default: 30d)
{
  "period": "30d",
  "data": [
    { "date": "2026-02-25", "amount": 1240.50 },
    { "date": "2026-02-26", "amount": 890.00 }
  ],
  "total": 38450.75,
  "growth_pct": 12.4
}
GET /bi/work-orders Auth Work order statistics
{
  "status_distribution": { "new": 12, "in_repair": 34, "completed": 198 },
  "avg_duration_hours": 18.4,
  "completion_rate_pct": 94.2
}
GET /bi/engineers Auth Per-engineer performance stats
[
  { "engineer_id": 3, "name": "Jan Bakker", "completed": 45, "avg_hours": 16.2, "revenue": 9800.00 }
]
GET /bi/customers Auth Customer analytics
{
  "new_customers": 14, "returning_customers": 63,
  "avg_ltv": 420.50
}
GET /bi/parts Auth Parts analytics
{
  "top_parts": [
    { "product_id": 7, "name": "iPhone 14 Screen", "used": 28 }
  ],
  "inventory_value": 14250.00
}
GET /bi/invoices Auth Invoice analytics
{
  "outstanding_amount": 8420.00,
  "overdue_amount": 1240.00,
  "avg_payment_days": 11.3
}

Warranty

Warranty claims and warranty configuration — prefix: /api/v1/warranty
GET /warranty/claims Auth List warranty claims

Returns paginated warranty claims with status and linked work order information.

POST /warranty/claims Auth File warranty claim
FieldTypeDescription
work_order_id *integerOriginal work order
description *stringDefect description
claimed_atdateDate of claim (default: today)
{
  "id": 15, "status": "pending",
  "work_order_id": 101, "created_at": "2026-03-27T10:00:00Z"
}
PUT /warranty/claims/{id} Auth Update warranty claim

Update the description or metadata of a pending claim.

POST /warranty/claims/{id}/approve Auth Approve warranty claim

Approves the claim and automatically creates a warranty work order.

{ "message": "Claim approved", "warranty_work_order_id": 250 }
POST /warranty/claims/{id}/reject Auth Reject warranty claim
FieldTypeDescription
reason *stringRejection reason communicated to customer
{ "message": "Claim rejected", "status": "rejected" }
GET /warranty/configs Auth List warranty configurations

Returns defined warranty policies (e.g. duration per repair type).

POST /warranty/configs Auth Create warranty config
FieldTypeDescription
name *stringPolicy name
duration_days *integerWarranty period in days
applies_tostringWork order type / category
PUT /warranty/configs/{id} Auth Update warranty config

Update warranty policy fields. Partial updates supported.

SLA

Service Level Agreement tracking and configuration — prefix: /api/v1/sla
GET /sla/breaches Auth List SLA breaches

Returns paginated list of work orders that have breached their SLA.

{
  "data": [
    { "work_order_id": 88, "sla_config": "Standard 24h", "breached_at": "2026-03-26T14:00:00Z", "overdue_hours": 6.5 }
  ]
}
GET /sla/configs Auth List SLA configurations

Returns all defined SLA policies.

POST /sla/configs Auth Create SLA config
FieldTypeDescription
name *stringPolicy name
response_hours *integerTime to first response (hours)
resolution_hours *integerTime to resolution (hours)
applies_tostringWork order priority / category
PUT /sla/configs/{id} Auth Update SLA config

Update SLA policy fields. Partial updates supported.

POST /sla/run-check Auth Manually trigger SLA check

Runs the SLA breach check immediately (normally runs via scheduler). Useful for debugging.

{ "message": "SLA check completed", "new_breaches": 2 }

Credit Notes

Issue and apply credit notes against invoices — prefix: /api/v1/credit-notes
GET /credit-notes Auth List credit notes

Returns paginated credit notes.

POST /credit-notes Auth Create credit note
FieldTypeDescription
invoice_id *integerInvoice being credited
reason *stringReason for credit
lines *arrayLine items to credit [{description, qty, unit_price, vat_pct}]
{
  "id": 9, "credit_note_number": "CN-2026-0009",
  "total": -48.40, "status": "open"
}
GET /credit-notes/{id} Auth Credit note detail

Returns credit note with full line items and linked invoice details.

PUT /credit-notes/{id} Auth Update credit note

Update reason or lines on an unapplied credit note.

DELETE /credit-notes/{id} Auth Delete credit note

Deletes an unapplied credit note. Returns 204.

POST /credit-notes/{id}/apply Auth Apply credit note to invoice

Applies the credit note balance against the linked invoice, reducing its outstanding amount.

{ "message": "Credit applied", "invoice_remaining": 71.60 }

Recurring Invoices

Schedule and manage automatically recurring invoices — prefix: /api/v1/recurring-invoices
GET /recurring-invoices Auth List recurring invoices

Returns all recurring invoice schedules with next run date.

POST /recurring-invoices Auth Create recurring invoice
FieldTypeDescription
customer_id *integerCustomer ID
frequency *stringweekly | monthly | quarterly | yearly
next_run_date *dateFirst invoice generation date
lines *arrayInvoice line items
auto_sendbooleanAutomatically email invoice on generation (default: false)
PUT /recurring-invoices/{id} Auth Update recurring invoice

Update schedule frequency, lines, or auto-send setting.

DELETE /recurring-invoices/{id} Auth Delete recurring invoice

Cancels and deletes the recurring schedule. Existing generated invoices are not affected. Returns 204.

POST /recurring-invoices/{id}/generate Auth Generate invoice now

Immediately generates an invoice from the recurring template without waiting for the next scheduled run.

{ "invoice_id": 134, "message": "Invoice generated" }

Branches

Multi-branch / multi-location management — prefix: /api/v1/branches
GET /branches Auth List branches
[
  { "id": 1, "name": "Amsterdam HQ", "address": "Damrak 1, Amsterdam", "active": true },
  { "id": 2, "name": "Rotterdam",   "address": "Coolsingel 10, Rotterdam", "active": true }
]
POST /branches Auth Create branch
FieldTypeDescription
name *stringBranch name
addressstringPhysical address
phonestringBranch phone number
emailstringBranch email address
PUT /branches/{id} Auth Update branch

Update branch name, address, contact details, or active state.

DELETE /branches/{id} Auth Delete branch

Soft-deletes the branch. Returns 204.

3CX Integration

VoIP call management via 3CX PBX — prefix: /api/v1/3cx
GET /3cx/status Auth 3CX connection status
{
  "connected": true,
  "calls_today": 38,
  "missed_today": 4,
  "avg_duration_sec": 187,
  "active_calls": 2
}
GET /3cx/config Auth Get 3CX configuration
{
  "api_url": "https://pbx.example.com",
  "api_key": "••••••••",
  "extension_map": { "3": "100", "5": "101" }
}
PUT /3cx/config Auth Update 3CX configuration
FieldTypeDescription
api_urlstring3CX API base URL
api_keystring3CX API key
GET /3cx/call-logs Auth Call history

Returns paginated call log with caller, callee, duration, and outcome.

{
  "data": [
    { "id": 4501, "caller": "+31612345678", "extension": "100", "duration_sec": 243, "outcome": "answered", "started_at": "2026-03-27T09:05:00Z" }
  ]
}
GET /3cx/queue Auth Active call queue
[
  { "caller": "+31687654321", "waiting_sec": 45, "position": 1 }
]
GET /3cx/extensions Auth User extension mappings
[
  { "user_id": 3, "name": "Jan Bakker", "extension": "100" }
]
POST /3cx/extensions Auth Create / update extension mapping
FieldTypeDescription
user_id *integerEmployee user ID
extension *string3CX extension number

SMS System

Send, receive and manage SMS communications — prefix: /api/v1/sms
GET /sms/stats Auth SMS statistics
{
  "today": 24,
  "this_month": 412,
  "delivery_rate": 98.3,
  "cost_this_month": 8.24
}
GET /sms/log Auth SMS history log

Returns paginated SMS message history including delivery status.

POST /sms/send Auth Send SMS
FieldTypeDescription
phone *stringRecipient phone number (E.164 format)
body *stringMessage text (max 160 chars for 1 credit)
template_idintegerUse a saved template (overrides body)
// Request body
{ "phone": "+31612345678", "body": "Your repair is ready for pickup!" }

// Response 200
{ "message_id": "sms_9x2k", "status": "queued" }
GET /sms/templates Auth List SMS templates
[
  { "id": 1, "name": "Ready for pickup", "body": "Your repair is ready. Please collect at your convenience." }
]
POST /sms/templates Auth Create SMS template
FieldTypeDescription
name *stringTemplate label
body *stringTemplate text (supports {{customer_name}} placeholders)
PUT /sms/templates/{id} Auth Update SMS template

Update template name or body.

DELETE /sms/templates/{id} Auth Delete SMS template

Permanently deletes the template. Returns 204.

POST /sms/bulk-send Auth Bulk SMS send
FieldTypeDescription
body *stringMessage text or template body
tag_idintegerSend to all customers with this tag
is_businessbooleanSend only to business customers
{ "queued": 84, "message": "84 SMS messages queued for delivery" }

Email System

Inbound/outbound email threads, SMTP config and templates — prefix: /api/v1/email
GET /email/inbox Auth Inbox — inbound threads

Returns paginated inbound email threads, newest first.

GET /email/sent Auth Sent — outbound threads

Returns paginated outbound email threads.

GET /email/config Auth Get SMTP configuration
{
  "host": "smtp.mailgun.org",
  "port": 587,
  "encryption": "tls",
  "username": "postmaster@mg.example.com",
  "from_name": "Repair Angel",
  "from_address": "noreply@example.com"
}
PUT /email/config Auth Update SMTP configuration
FieldTypeDescription
host *stringSMTP server hostname
port *integerSMTP port (e.g. 587)
encryptionstringtls | ssl | none
username *stringSMTP username
passwordstringSMTP password (omit to keep existing)
from_namestringSender display name
from_addressstringSender email address
GET /email/threads Auth All email threads

Returns all email threads (inbound and outbound) with optional filtering by customer, date range, or subject.

GET /email/threads/{id} Auth Email thread detail
{
  "id": 77, "subject": "Re: Your repair status",
  "customer": { "id": 42, "name": "Jane Smith" },
  "messages": [
    { "id": 1, "direction": "outbound", "body_html": "<p>Your iPhone is ready...</p>", "sent_at": "2026-03-27T08:00:00Z" },
    { "id": 2, "direction": "inbound",  "body_html": "<p>Thanks, I'll pick it up at 5pm.</p>", "sent_at": "2026-03-27T10:30:00Z" }
  ]
}
POST /email/send Auth Send email
FieldTypeDescription
to *stringRecipient email address
subject *stringEmail subject line
body_html *stringHTML email body
template_idintegerUse a saved template (overrides body_html)
// Request body
{
  "to": "customer@example.com",
  "subject": "Your repair is ready",
  "body_html": "<p>Good news! Your device is ready for pickup.</p>"
}

// Response 200
{ "thread_id": 78, "message_id": "msg_abc123", "status": "sent" }
GET /email/templates Auth List email templates
[
  { "id": 1, "name": "Repair Ready", "subject": "Your repair is ready", "updated_at": "2026-01-15" }
]
POST /email/templates Auth Create email template
FieldTypeDescription
name *stringTemplate label
subject *stringDefault subject line
body_html *stringHTML template body (supports {{customer_name}} placeholders)
PUT /email/templates/{id} Auth Update email template

Update template name, subject, or body. Partial updates supported.

DELETE /email/templates/{id} Auth Delete email template

Permanently deletes the email template. Returns 204.

AI Repair Assistant

OpenAI-powered repair suggestions and configuration — prefix: /api/ai
POST /api/ai/suggest-repair Auth Get AI repair suggestions

Submits device symptoms to the AI engine and returns ranked repair suggestions with estimated prices. Requires the OpenAI integration to be enabled.

FieldTypeDescription
device_brand *stringDevice manufacturer (e.g. Apple)
device_model *stringDevice model name (e.g. iPhone 14 Pro)
symptoms *stringFree-text description of reported symptoms
customer_type *stringindividual or business — affects pricing context
// Request body
{
  "device_brand": "Apple",
  "device_model": "iPhone 14 Pro",
  "symptoms": "Screen cracked, touch unresponsive in bottom half",
  "customer_type": "individual"
}

// Response 200
{
  "suggestions": [
    {
      "name": "Screen replacement",
      "confidence": "high",
      "estimated_price": 149.00,
      "description": "Full OLED panel replacement including digitizer",
      "requires_diagnosis": false
    },
    {
      "name": "Digitizer calibration",
      "confidence": "medium",
      "estimated_price": 45.00,
      "description": "Software-level touch recalibration before hardware swap",
      "requires_diagnosis": true
    }
  ],
  "advice": "Recommend screen replacement as primary fix. Run diagnostics first to rule out logic board damage.",
  "not_repairable": false
}
GET /api/ai/config Auth Get AI configuration

Returns the current AI engine configuration including the OpenAI model in use, diagnosis pricing, and suggestion limits.

{
  "enabled": true,
  "openai_model": "gpt-4o",
  "diagnosis_cost": 25.00,
  "diagnosis_tax_rate_id": 3,
  "max_suggestions": 5
}
PUT /api/ai/config Auth Update AI configuration

Updates the AI configuration. Body fields are the same as the GET response. All fields are optional — only supplied fields are updated.

FieldTypeDescription
enabledbooleanEnable or disable the AI assistant globally
openai_modelstringOpenAI model identifier (e.g. gpt-4o)
diagnosis_costnumberCost charged for a physical diagnosis session
diagnosis_tax_rate_idintegerTax rate ID applied to the diagnosis cost
max_suggestionsintegerMaximum number of suggestions to return (1–10)

Tax Rates

Manage VAT / tax rate definitions used across products and invoices — prefix: /api/tax-rates
GET /api/tax-rates Auth List all tax rates

Returns all configured tax rates ordered by rate ascending.

[
  { "id": 1, "name": "Standard 21%", "rate": 21.00, "description": "Standard Dutch BTW", "is_default": true },
  { "id": 2, "name": "Low 9%",      "rate": 9.00,  "description": "Reduced rate",         "is_default": false },
  { "id": 3, "name": "Zero 0%",     "rate": 0.00,  "description": "Exempt",                "is_default": false }
]
POST /api/tax-rates Auth Create tax rate
FieldTypeDescription
name *stringDisplay label (e.g. Standard 21%)
rate *numberPercentage value (e.g. 21.00)
descriptionstringOptional notes about when to apply this rate
is_defaultbooleanIf true, unsets the current default first
// Response 201
{ "id": 4, "name": "Margin scheme", "rate": 9.00, "description": "Used-goods margin", "is_default": false }
GET /api/tax-rates/{id} Auth Get tax rate

Returns a single tax rate record by ID. Returns 404 if not found.

PUT /api/tax-rates/{id} Auth Update tax rate

Updates the name, rate, description, or default flag. Partial updates supported — only provided fields are changed.

DELETE /api/tax-rates/{id} Auth Delete tax rate

Permanently deletes the tax rate. Returns 204 on success. Returns 422 if the tax rate is currently assigned to one or more products or invoice lines.

// 422 — tax rate in use
{
  "message": "Cannot delete: tax rate is assigned to 12 product(s)."
}
POST /api/tax-rates/{id}/set-default Auth Set as default tax rate

Marks this tax rate as the system default. All other rates have their is_default flag unset atomically in the same transaction.

// Response 200
{ "id": 1, "name": "Standard 21%", "is_default": true }

Pricing & Price Agreements

Customer-specific pricing, business discounts, and signed price agreements — prefix: /api/pricing
GET /api/pricing/check?customer_id=&service_id= Auth Check effective price for customer + service

Returns the resolved price for a given customer and service combination. Considers active price agreements first, then business discount, then base price.

Query paramTypeDescription
customer_id *integerCustomer ID to price-check for
service_id *integerService / product ID to look up
// Response 200
{
  "has_agreement": true,
  "agreement": { "id": 12, "agreed_price": 89.00, "valid_until": "2026-12-31" },
  "business_discount_pct": 10,
  "recommended_price": 89.00,
  "base_price": 110.00,
  "discount_amount": 21.00
}
GET /api/pricing/agreements Auth List price agreements

Returns a paginated list of price agreements. Supports filtering by customer, status, and free-text search.

Query paramTypeDescription
customer_idintegerFilter by customer
statusstringdraft, active, expired, or signed
searchstringFull-text search on customer name or service name
POST /api/pricing/agreements Auth Create price agreement
FieldTypeDescription
customer_id *integerCustomer the agreement applies to
service_id *integerService or product being priced
agreed_price *numberFixed agreed price excluding VAT
valid_from *dateStart date (YYYY-MM-DD)
valid_untildateExpiry date — omit for open-ended agreements
notesstringInternal notes for this agreement
// Response 201
{
  "id": 13, "status": "draft", "customer_id": 42, "service_id": 7,
  "agreed_price": 89.00, "valid_from": "2026-04-01", "valid_until": null
}
GET /api/pricing/agreements/{id} Auth Get price agreement

Returns a single price agreement with its full details including any attached signature and contract PDF URL.

PUT /api/pricing/agreements/{id} Auth Update price agreement

Updates a draft agreement. Signed or expired agreements cannot be modified — returns 422.

DELETE /api/pricing/agreements/{id} Auth Delete price agreement

Permanently deletes a draft agreement. Returns 204. Active or signed agreements cannot be deleted — returns 422.

POST /api/pricing/agreements/{id}/activate Auth Activate agreement

Transitions the agreement from draft to active. The agreement will immediately be considered when resolving prices for the customer.

{ "id": 13, "status": "active", "activated_at": "2026-03-27T09:00:00Z" }
POST /api/pricing/agreements/{id}/sign Auth Attach customer signature

Attaches a base64-encoded PNG signature image to the agreement and transitions the status to signed. Typically called from the customer-facing signing screen.

FieldTypeDescription
signature_data *stringBase64-encoded PNG of the customer signature
{ "id": 13, "status": "signed", "signed_at": "2026-03-27T10:15:00Z" }
GET /api/pricing/agreements/{id}/contract Auth Download contract PDF

Returns the generated contract PDF as a binary stream (Content-Type: application/pdf). Returns 404 if no contract has been generated yet.

POST /api/pricing/agreements/{id}/generate-contract Auth Generate contract PDF

Renders and stores the contract PDF from the current agreement data and signature. Idempotent — regenerates the PDF if called again after changes.

{ "id": 13, "contract_url": "/api/pricing/agreements/13/contract", "generated_at": "2026-03-27T10:20:00Z" }

Integration Configuration Hub

Read, update, and test all third-party integration configs — prefix: /api/integrations. Secret values are always masked as ***masked*** in GET responses.
Supported slugs:
mollie · twilio · snelstart · moneybird · 3cx · ups · openai · outlook · google_workspace · mailpit · stripe · postmark · dhl
GET /api/integrations Auth List all integrations

Returns all 13 integrations with their enabled status, slug, and a masked config object.

[
  {
    "slug": "mollie",
    "name": "Mollie Payments",
    "is_enabled": true,
    "config": { "api_key": "***masked***" }
  },
  {
    "slug": "openai",
    "name": "OpenAI",
    "is_enabled": true,
    "config": { "api_key": "***masked***", "organisation_id": "***masked***" }
  }
  // ... 11 more
]
GET /api/integrations/{slug} Auth Get single integration

Returns the configuration for a single integration by slug. Secret fields (API keys, passwords, tokens) are always returned as ***masked***. Returns 404 for unknown slugs.

// GET /api/integrations/stripe
{
  "slug": "stripe",
  "name": "Stripe",
  "is_enabled": false,
  "config": {
    "publishable_key": "pk_test_***masked***",
    "secret_key": "***masked***",
    "webhook_secret": "***masked***"
  }
}
PUT /api/integrations/{slug} Auth Update integration config

Updates the config keys and/or enabled state for an integration. Only keys provided in config are updated — omitted keys are left unchanged. Sending a masked value (***masked***) for an existing secret leaves it unchanged.

FieldTypeDescription
configobjectKey/value pairs specific to the integration (see slug docs)
is_enabledbooleanEnable or disable the integration
// Request — enable Mollie and set API key
{
  "is_enabled": true,
  "config": { "api_key": "live_xxxxxxxxxxxxxxxx" }
}

// Response 200
{ "slug": "mollie", "is_enabled": true, "updated_at": "2026-03-27T11:00:00Z" }
POST /api/integrations/{slug}/test Auth Test integration connection

Performs a live connectivity check against the third-party service using the saved credentials. Returns a success flag and human-readable message.

// Response 200 — success
{
  "success": true,
  "message": "Connected to Mollie — account: RepairAngel BV",
  "tested_at": "2026-03-27T11:05:22Z"
}

// Response 200 — failure (still HTTP 200, check "success")
{
  "success": false,
  "message": "Authentication failed: invalid API key",
  "tested_at": "2026-03-27T11:05:22Z"
}

Employee Phone & Email Config

Per-employee phone (SIP/3CX) and email (SMTP/Outlook/Google Workspace) configuration — prefix: /api/employees/{id}
GET /api/employees/{id}/phone-config Auth Get employee phone config

Returns the telephony configuration for the given employee. SIP password is always returned masked.

{
  "employee_id": 5,
  "provider": "3cx",
  "extension": "101",
  "mobile_number": "+31612345678",
  "forwarding_number": "+31201234567",
  "sip_username": "john.doe@pbx.example.com",
  "sip_password": "***masked***",
  "is_active": true
}
PUT /api/employees/{id}/phone-config Auth Update employee phone config

Creates or replaces the employee's telephony configuration. All fields are optional — only supplied fields are updated.

FieldTypeDescription
providerstringTelephony provider (e.g. 3cx, twilio)
extensionstringInternal PBX extension number
mobile_numberstringEmployee's mobile number in E.164 format
forwarding_numberstringExternal call-forwarding number in E.164 format
sip_usernamestringSIP account username / URI
sip_passwordstringSIP account password (stored encrypted)
is_activebooleanWhether this phone config is active
GET /api/employees/{id}/email-config Auth Get employee email config

Returns the outgoing email configuration for the given employee. Passwords and OAuth tokens are always returned masked.

{
  "employee_id": 5,
  "provider": "smtp",
  "email_address": "john.doe@example.com",
  "display_name": "John Doe — Repair Angel",
  "smtp_host": "mail.example.com",
  "smtp_port": 587,
  "smtp_encryption": "tls",
  "smtp_username": "john.doe@example.com",
  "smtp_password": "***masked***",
  "is_default": true
}
PUT /api/employees/{id}/email-config Auth Update employee email config

Creates or replaces the employee's outgoing email configuration. All fields are optional — only supplied fields are updated.

FieldTypeDescription
providerstringsmtp, outlook, or google_workspace
email_addressstringSender email address
display_namestringSender display name shown to recipients
smtp_hoststringSMTP server hostname (SMTP provider only)
smtp_portintegerSMTP port, typically 587 (TLS) or 465 (SSL)
smtp_encryptionstringtls or ssl
smtp_usernamestringSMTP authentication username
smtp_passwordstringSMTP authentication password (stored encrypted)
is_defaultbooleanIf true, this becomes the employee's default sending account
POST /api/employees/{id}/email-config/test Auth Test employee email config

Sends a test email to the employee's configured address using the saved outgoing email settings. Returns a success flag and message.

// Response 200 — success
{ "success": true,  "message": "Test email delivered to john.doe@example.com" }

// Response 200 — failure
{ "success": false, "message": "SMTP connection refused on port 587" }
Repair Angel API v2 · Generated 2026 · Back to top ↑