Comprehensive overview of the External API, authentication, endpoints, examples, and best practices.
The Fenceline External API provides read-only access to operational and accounting data for integrations with QuickBooks, Salesforce, ServiceM8, and similar platforms. The API is versioned, simple, and secured via API keys.
Property | Value |
---|---|
Base URL | https://api.fenceline.ai |
Versioning | Path-based (/v1 ). Non-breaking changes may add fields. |
Auth | API key in header (see below) |
Format | JSON over HTTPS |
Environment | URL |
---|---|
Production | https://api.fenceline.ai |
Local Development | http://localhost:5173/api/v1 |
Endpoints on Production are root-relative (no duplicate "/api"):
GET https://api.fenceline.ai/metadata
GET https://api.fenceline.ai/openapi
(or same-origin: GET /api/v1/openapi
)GET https://api.fenceline.ai/customers
GET https://api.fenceline.ai/projects
GET https://api.fenceline.ai/invoices
GET https://api.fenceline.ai/payments
Note: For local dev, use
/api/v1/*
paths as shown above.
Swagger UI: Visit /developers/api
for an interactive, always-up-to-date API explorer. This page loads the spec from the same origin at /api/v1/openapi
to avoid CORS issues.
Include your key using either header:
x-api-key: <YOUR_API_KEY>
or
Authorization: Bearer <YOUR_API_KEY>
If you need a key, contact your Fenceline admin. Keys can be rotated without downtime.
β οΈ Production Requirement: In production, requests to
/api/v1/*
must be made via theapi.fenceline.ai
host.Exception: The OpenAPI spec route (
/api/v1/openapi
) is intentionally available on all hosts (e.g.,www.fenceline.ai
andapi.fenceline.ai
) and returns permissive CORS headers so documentation and tools can fetch it.
βΉοΈ Rate Limiting: Some deployments may enforce fail-closed rate limiting; if the limiter backend is unavailable, endpoints return
503 Service Unavailable
with aRetry-After
header.
Default: 10 requests per minute per IP per endpoint. Responses include headers:
X-RateLimit-Limit
X-RateLimit-Remaining
X-RateLimit-Reset
(epoch seconds)Exceeding limits returns 429 Too Many Requests
with Retry-After
.
If EXTERNAL_API_RATE_LIMIT_FAIL_CLOSED=true
, temporary limiter outages result in 503 Service Unavailable
with Retry-After
.
contractorId
(string) β Recommended to scope results to a single contractor/tenantlimit
(number) β 1 to 100, default 50updatedAfter
(ISO 8601) β Return items updated after this timestamp (where supported)All list endpoints return:
{
"apiVersion": "v1",
"items": [ /* array of resources */ ],
"nextPageToken": null
}
Note: Pagination tokens are reserved for future use; nextPageToken
is currently null
.
GET /metadata
Example:
curl -s -H "x-api-key: $API_KEY" "https://api.fenceline.ai/metadata"
Response:
{
"apiVersion": "v1",
"resources": [
{
"name": "customers",
"path": "/api/v1/customers",
"query": ["contractorId", "limit"],
"fields": [ { "name": "id", "type": "string" }, ... ]
}
],
"query": { "common": ["contractorId", "limit", "updatedAfter"] }
}
GET /customers
contractorId
, limit
Example request:
curl -s \
-H "x-api-key: $API_KEY" \
"https://api.fenceline.ai/customers?contractorId=abc123&limit=25"
Customer object:
{
"id": "cust_123",
"name": "Acme Corp",
"email": "ops@acme.com",
"phone": "+1-555-0100",
"createdAt": "2025-01-05T12:34:56.000Z",
"updatedAt": "2025-02-10T08:12:03.000Z"
}
Fields:
id
(string)name
(string)email
(string, optional)phone
(string, optional)createdAt
(ISO string, optional)updatedAt
(ISO string, optional)GET /projects
contractorId
, limit
, updatedAfter
Example request:
curl -s \
-H "x-api-key: $API_KEY" \
"https://api.fenceline.ai/projects?contractorId=abc123&limit=50"
Project object:
{
"id": "proj_456",
"contractorId": "abc123",
"customerId": "cust_123",
"name": "West Yard Fence",
"status": "in_progress",
"totalAmount": 12875.5,
"createdAt": "2025-02-01T10:00:00.000Z",
"updatedAt": "2025-02-08T14:20:00.000Z"
}
GET /invoices
analytics_invoices
contractorId
, limit
, updatedAfter
Example request:
curl -s \
-H "x-api-key: $API_KEY" \
"https://api.fenceline.ai/invoices?contractorId=abc123&limit=50"
Invoice object:
{
"id": "inv_proj_456_1736448000000",
"projectId": "proj_456",
"contractorId": "abc123",
"customerId": "cust_123",
"number": "INV-2025-0012",
"amount": 5000,
"currency": "USD",
"date": "2025-02-01T10:00:00.000Z",
"dueDate": "2025-02-15T10:00:00.000Z",
"status": "current",
"createdAt": "2025-02-01T10:00:00.000Z",
"updatedAt": "2025-02-01T10:00:00.000Z"
}
status
: paid | overdue | current | future
GET /payments
analytics_revenue_transactions
where transactionType = PAYMENT_RECEIVED
contractorId
, limit
, updatedAfter
Example request:
curl -s \
-H "x-api-key: $API_KEY" \
"https://api.fenceline.ai/payments?contractorId=abc123&limit=50"
Payment object:
{
"id": "pay_proj_456_1736534400000",
"projectId": "proj_456",
"contractorId": "abc123",
"customerId": "cust_123",
"amount": 2500,
"currency": "USD",
"date": "2025-02-02T10:00:00.000Z",
"method": "card",
"reference": "txn_9xYz",
"createdAt": "2025-02-02T10:00:00.000Z",
"updatedAt": "2025-02-02T10:00:00.000Z"
}
import fetch from 'node-fetch'
const API_KEY = process.env.EXTERNAL_API_KEY
const BASE = 'https://api.fenceline.ai'
async function listInvoices() {
const res = await fetch(`${BASE}/invoices?contractorId=abc123&limit=25`, {
headers: { 'x-api-key': API_KEY }
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
console.log(data.items)
}
listInvoices().catch(console.error)
import os, requests
API_KEY = os.environ['EXTERNAL_API_KEY']
BASE = 'https://api.fenceline.ai'
resp = requests.get(
f"{BASE}/payments",
headers={'x-api-key': API_KEY},
params={'contractorId': 'abc123', 'limit': 50}
)
resp.raise_for_status()
print(resp.json()['items'])
limit
(1β100, default 50)pageToken
to fetch next pages. Treat tokens as opaque. When EXTERNAL_API_TOKEN_SECRET
is configured, pagination tokens are HMAC-signed and bound to the requesting contractor/key; tampering will cause rejection.lastSuccessfulUpdatedAt
(ISO string) in your system after a completed sync.updatedAfter = (lastSuccessfulUpdatedAt - overlap)
, where overlap
is 1β2 minutes to tolerate clock skew/eventual consistency.nextPageToken
until it is absent; never assume a single page completes a window.id
on your side to account for the overlap.Retry-After
.Example (pseudocode):
since = (lastSuccessfulUpdatedAt || initialISO) - 2 minutes
token = undefined
do {
res = GET /invoices?contractorId=...&updatedAfter=since&limit=50&pageToken=token
upsert(res.items)
token = res.nextPageToken
} while (token)
lastSuccessfulUpdatedAt = now()
updatedAfter
for delta syncs. Combine with pagination for large result sets.Error example:
{ "error": "Unauthorized: invalid API key" }
contractorId
to scope dataIf you need an API key or have questions, contact your Fenceline administrator or support.