Integration API v1
API home · Admin

Business AI OS — Integration API v1

Push customers, jobs, and invoices from your external system into a Business AI OS company (tenant). Data is upserted idempotently and becomes available to the mobile app and AI assistant.

Version: v1
Base path: /api/integrations/v1
Content type: application/json
Direction: Inbound only (your system → Business AI OS)


Overview

Concept Description
Tenant One company in Business AI OS. Each API key belongs to exactly one tenant.
Upsert Create if new, update if the same record is sent again.
External ID Your stable identifier for a record in your system. Used with external_source for matching.
Payload Entity-specific fields (name, title, amount, etc.).

Supported entities:


Authentication

All endpoints require a Bearer token (integration API key).

Authorization: Bearer bai_xxxxxxxxxxxxxxxx
Accept: application/json

Obtaining an API key

  1. Sign in to the platform admin portal: /admin
  2. Open the target company (tenant)
  3. Scroll to Integration API keysCreate API key
  4. Copy the key immediately — it is shown once and cannot be retrieved later

Key format: bai_ followed by 48 random characters.

Scopes

Scope Access
write POST /sync, POST /sync/batch (default for new keys)
read Reserved for future read endpoints

Keys without write scope receive 403 on sync endpoints.

Security notes


Base URL

Environment Example
Local http://127.0.0.1:8001/api/integrations/v1
Production https://your-host.example.com/api/integrations/v1

All paths below are relative to this base URL.


Rate limiting

When exceeded, the API returns 429 Too Many Requests.


Idempotency

Send the same record again to update it instead of creating a duplicate.

Matching order:

  1. external_source + external_id (recommended)
  2. Fallback heuristics if external_id is omitted:
    • customers: email, else name
    • jobs: reference, else title
    • invoices: number, else external_id as number

Recommended external ID pattern

Use stable IDs from your system:

your-system-customer-{id}
your-system-job-{id}
your-system-invoice-{id}

Set external_source to a short system name (e.g. fieldops, xero, erp). If omitted, the API key’s name is used as the source.


Endpoints

GET /health

Verify authentication and which tenant the key belongs to.

Authentication: Required
Scope: Any valid key

Response 200 OK

{
  "status": "ok",
  "tenant": {
    "id": "demo-cooltech",
    "name": "CoolTech HVAC"
  },
  "key": {
    "name": "fieldops production",
    "prefix": "bai_abc123456789",
    "scopes": ["write"]
  }
}

Example

curl -s "https://your-host.example.com/api/integrations/v1/health" \
  -H "Authorization: Bearer bai_YOUR_KEY" \
  -H "Accept: application/json"

POST /sync

Upsert one record.

Authentication: Required
Scope: write

Request body

Field Type Required Description
entity string Yes One of: customers, jobs, invoices
payload object Yes Entity fields (see Payload reference)
external_id string No Your record ID (max 255 chars). Strongly recommended.
external_source string No System identifier (max 64 chars). Defaults to API key name.

Response 200 OK

{
  "success": true,
  "created": 1,
  "updated": 0,
  "skipped": 0
}
Field Description
created New rows inserted in this request
updated Existing rows updated
skipped Records ignored (e.g. missing required payload fields)

Example — customer

curl -s -X POST "https://your-host.example.com/api/integrations/v1/sync" \
  -H "Authorization: Bearer bai_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "entity": "customers",
    "external_id": "erp-customer-42",
    "external_source": "my_erp",
    "payload": {
      "name": "Siti Rahman",
      "phone": "0170000000",
      "email": "siti@example.com",
      "address": "Petaling Jaya, Selangor"
    }
  }'

POST /sync/batch

Upsert up to 500 records in one request.

Authentication: Required
Scope: write

Request body

Field Type Required Description
records array Yes 1–500 sync records

Each element in records has the same shape as the /sync body:

Field Type Required
entity string Yes
payload object Yes
external_id string No
external_source string No

Response 200 OK

{
  "success": true,
  "processed": 2,
  "created": 2,
  "updated": 0,
  "skipped": 0
}

Example

curl -s -X POST "https://your-host.example.com/api/integrations/v1/sync/batch" \
  -H "Authorization: Bearer bai_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "records": [
      {
        "entity": "customers",
        "external_id": "erp-customer-1",
        "external_source": "my_erp",
        "payload": { "name": "ACME Corp", "phone": "0312345678" }
      },
      {
        "entity": "jobs",
        "external_id": "erp-job-99",
        "external_source": "my_erp",
        "payload": {
          "title": "Compressor replacement",
          "status": "completed",
          "amount": 900,
          "customer_external_id": "erp-customer-1"
        }
      }
    ]
  }'

Tip: Sync customers before jobs/invoices that reference customer_external_id.


Payload reference

Fields inside the payload object. Types reflect what the importer accepts.

Customers (entity: "customers")

Field Type Required Description
name string Yes Customer or company name. Records without name are skipped.
phone string No Phone number
email string No Email address
address string No Free-text address

Jobs (entity: "jobs")

Field Type Required Description
title string Yes Job title. Records without title are skipped.
status string No e.g. pending, assigned, in_progress, completed, cancelled. Default: pending
amount number No Job value in your currency. Default: 0
technician string No Assigned technician name
reference string No Your job reference code. Falls back to top-level external_id
scheduled_at string No Date (YYYY-MM-DD) or parseable datetime
completed_at string No Date (YYYY-MM-DD) or parseable datetime
notes string No Description or internal notes
customer_external_id string No Links to a customer synced with the same external_source

Invoices (entity: "invoices")

Field Type Required Description
number string Yes* Invoice number. *If omitted, top-level external_id is used as the number.
total number No Invoice total. Default: 0
tax number No Tax amount (stored separately; does not auto-calculate total)
status string No e.g. unpaid, paid, authorised (authorised maps to unpaid). Default: unpaid
issued_at string No Issue date. Default: today
due_at string No Due date
notes string No Invoice notes
customer_name string No Creates/finds customer by name if customer_external_id not set
customer_external_id string No Links to customer by external ID (same external_source)

Linking records

Customer (external_id: "erp-customer-42")
    ↑
    │ customer_external_id
    │
Job (external_id: "erp-job-100")
    │
    │ (optional business link — not enforced by FK from invoice to job)
    ↓
Invoice (external_id: "erp-invoice-55", customer_external_id: "erp-customer-42")

Use the same external_source on related records (or omit it consistently so all default to the same key name).


Error responses

401 Unauthorized

Missing, invalid, or revoked API key.

{
  "message": "Invalid or missing integration API key."
}

403 Forbidden

Tenant suspended/inactive, or key lacks write scope.

{
  "message": "This API key does not have write scope."
}

422 Unprocessable Entity

Validation failed (invalid entity, missing payload, batch too large, etc.).

{
  "message": "The entity field is required.",
  "errors": {
    "entity": ["The entity field is required."]
  }
}

429 Too Many Requests

Rate limit exceeded. Retry after a short delay.


Integration patterns

Event-driven (recommended)

Call /sync or /sync/batch from your app when data changes:

Initial backfill

Loop existing records and POST to /sync/batch in chunks of 500.

FieldOps (fieldops.kiets.dev)

FieldOps has a built-in Business AI OS settings page per company (paid add-on).

Provisioning flow (multi-tenant):

  1. Business AI OS — Create a tenant for this customer in /admin and generate an Integration API key.
  2. FieldOps superadmin — Enable Business AI OS (paid add-on) on that FieldOps company (admin_feature_flags).
  3. FieldOps company admin — Open Business AI OS in the sidebar, paste:
    • Integration API base URL (this server’s /api/integrations/v1)
    • API key (bai_…)
    • Optional tenant ID (reference only)
    • Enable automatic sync
  4. Click Test connection, then Sync all data now, or create or complete a job in FieldOps.

Each FieldOps company on a Professional account can link to a different Business AI OS tenant and API key.

Global FieldOps .env keys are optional dev fallback only:

BUSINESS_AI_INTEGRATION_ENABLED=true
BUSINESS_AI_INTEGRATION_URL=https://your-host.example.com/api/integrations/v1
BUSINESS_AI_INTEGRATION_API_KEY=bai_xxxxxxxx
BUSINESS_AI_INTEGRATION_SOURCE=fieldops
BUSINESS_AI_INTEGRATION_DOCS_URL=https://your-host.example.com/docs/integration-api

External IDs used by FieldOps:

Entity external_id pattern
Customer fieldops-customer-{company_id}-{phone_digits}
Job fieldops-job-{id}
Invoice fieldops-invoice-{id}

Code examples

PHP (Laravel HTTP client)

use Illuminate\Support\Facades\Http;

$baseUrl = config('services.business_ai.base_url');
$key = config('services.business_ai.api_key');

Http::withToken($key)
    ->acceptJson()
    ->timeout(10)
    ->post("{$baseUrl}/sync", [
        'entity' => 'jobs',
        'external_id' => 'erp-job-'.$job->id,
        'external_source' => 'my_erp',
        'payload' => [
            'title' => $job->title,
            'status' => $job->status,
            'amount' => $job->total,
            'customer_external_id' => 'erp-customer-'.$job->customer_id,
        ],
    ])
    ->throw();

JavaScript (fetch)

await fetch('https://your-host.example.com/api/integrations/v1/sync', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer bai_YOUR_KEY',
    'Content-Type': 'application/json',
    Accept: 'application/json',
  },
  body: JSON.stringify({
    entity: 'customers',
    external_id: 'crm-123',
    external_source: 'my_crm',
    payload: {
      name: 'ACME Corp',
      phone: '0312345678',
    },
  }),
});

Python (requests)

import requests

requests.post(
    "https://your-host.example.com/api/integrations/v1/sync",
    headers={
        "Authorization": "Bearer bai_YOUR_KEY",
        "Accept": "application/json",
    },
    json={
        "entity": "invoices",
        "external_id": "inv-5001",
        "external_source": "accounting",
        "payload": {
            "number": "INV-5001",
            "status": "paid",
            "total": 1200.00,
            "issued_at": "2026-07-26",
            "customer_external_id": "crm-123",
        },
    },
    timeout=10,
)

HTTP examples file

Import docs/integration-api.http into VS Code (REST Client) or JetBrains HTTP Client for ready-to-run requests.


Troubleshooting

table customers has no column named external_source

The tenant database was created before integration sync was added, or tenant migrations were never run. On the Business AI OS server:

php artisan tenants:run migrate \
  --option=force=true \
  --option=path=database/migrations/tenant/2026_07_26_150000_align_tenant_schema_for_integration.php

Then retry sync from FieldOps (Sync all data now).

New tenants provisioned after integration support get these columns automatically via tenants:migrate.


Changelog

Version Notes
v1 GET /health, POST /sync, POST /sync/batch. Entities: customers, jobs, invoices. Bearer API key auth.

Support