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/metadataGET https://api.fenceline.ai/openapi (or same-origin: GET /api/v1/openapi)GET https://api.fenceline.ai/customersGET https://api.fenceline.ai/projectsGET https://api.fenceline.ai/invoicesGET https://api.fenceline.ai/paymentsNote: 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.aihost.Exception: The OpenAPI spec route (
/api/v1/openapi) is intentionally available on all hosts (e.g.,www.fenceline.aiandapi.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 Unavailablewith aRetry-Afterheader.
Default: 10 requests per minute per IP per endpoint. Responses include headers:
X-RateLimit-LimitX-RateLimit-RemainingX-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 /metadataExample:
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 /customerscontractorId, limitExample 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 /projectscontractorId, limit, updatedAfterExample 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 /invoicesanalytics_invoicescontractorId, limit, updatedAfterExample 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 /paymentsanalytics_revenue_transactions where transactionType = PAYMENT_RECEIVEDcontractorId, limit, updatedAfterExample 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 dataIn addition to the read-only data endpoints above, Fenceline provides webhook endpoints for receiving data from external systems.
Receive leads from third-party sources like Facebook Lead Ads, Google Ads, Zapier, or custom integrations.
Endpoint: POST /api/webhooks/leads/inbound
Required Scope: write:leads
Note: This endpoint requires an API key with the
write:leadsscope. Contact your Fenceline administrator to request a key with this scope enabled.
x-api-key: <YOUR_API_KEY>
Content-Type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
firstName |
string | Yes | Customer's first name |
lastName |
string | Yes | Customer's last name |
email |
string | Conditional | Customer's email (required if no phone) |
phone |
string | Conditional | Customer's phone (required if no email) |
address |
string | No | Street address |
city |
string | No | City |
state |
string | No | State (2-letter code) |
zipCode |
string | No | ZIP/postal code |
leadSource |
string | No | Source of the lead (e.g., "Facebook", "Google Ads") |
notes |
string | No | Additional notes or customer message |
externalId |
string | No | Your unique ID for idempotency |
curl -X POST "https://api.fenceline.ai/api/webhooks/leads/inbound" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"phone": "555-123-4567",
"address": "123 Main St",
"city": "Austin",
"state": "TX",
"zipCode": "78701",
"leadSource": "Facebook",
"notes": "Interested in wood fence for backyard",
"externalId": "fb-lead-12345"
}'
{
"success": true,
"projectId": "abc123xyz",
"jobNumber": 1042,
"message": "Lead created successfully"
}
When the same externalId is submitted again:
{
"success": true,
"projectId": "abc123xyz",
"duplicate": true,
"message": "Lead already exists"
}
| Status | Description |
|---|---|
| 400 | Missing required fields or invalid format |
| 401 | Invalid or missing API key |
| 403 | API key lacks write:leads scope |
| 429 | Rate limit exceeded (60 requests/minute) |
| 500 | Server error |
The leadSource field is automatically mapped to internal tracking categories:
| Input Contains | Maps To |
|---|---|
| "facebook", "fb" | Facebook Ads |
| "google", "adwords", "ppc" | Google Ads |
| "angie" | Angie's List |
| "homeadvisor" | HomeAdvisor |
| "thumbtack" | Thumbtack |
| "yelp" | Yelp |
| "nextdoor" | Nextdoor |
| "referral" | Referral |
| "phone", "call" | Phone Call |
| "website", "web" | Website Form |
| Other | Other |
X-RateLimit-Limit: Maximum requests allowedX-RateLimit-Remaining: Requests remaining in windowX-RateLimit-Reset: Unix timestamp when limit resetsUse the externalId field to prevent duplicate leads. If a lead with the same externalId already exists for your contractor account, the endpoint returns the existing project ID with duplicate: true instead of creating a new lead.
Zapier Webhook:
https://api.fenceline.ai/api/webhooks/leads/inboundx-api-key with your API keyFacebook Lead Ads (via Zapier or direct):
{
"firstName": "{{lead.first_name}}",
"lastName": "{{lead.last_name}}",
"email": "{{lead.email}}",
"phone": "{{lead.phone_number}}",
"leadSource": "Facebook Lead Ads",
"notes": "{{lead.custom_fields}}",
"externalId": "fb-{{lead.id}}"
}
POST /api/webhooks/leads/inbound)If you need an API key or have questions, contact your Fenceline administrator or support.