Developer guide
Authentication, resource examples, pagination, and inbound lead submission.
Overview
Reviewed against the implementation on October 25, 2026. API version: v1.
The Fenceline External API provides read-only access to account data for custom integrations. The separate inbound lead webhook accepts lead submissions. Both require API keys with the appropriate scopes.
Connection details
| Property | Value |
|---|---|
| Base URL | https://api.fenceline.ai/api/v1 |
| Versioning | v1 endpoints. Non-breaking changes may add fields. |
| Auth | API key in header (see below) |
| Format | JSON over HTTPS |
Discovery
Base URLs
| Environment | URL |
|---|---|
| Production | https://api.fenceline.ai/api/v1 |
| Local Development | http://localhost:5173/api/v1 |
Available Endpoints
Append each resource path to the production base URL, including its /api/v1 prefix:
- Metadata:
GET https://api.fenceline.ai/api/v1/metadata - OpenAPI:
GET https://api.fenceline.ai/api/v1/openapi(or same-origin:GET /api/v1/openapi) - Customers:
GET https://api.fenceline.ai/api/v1/customers - Projects:
GET https://api.fenceline.ai/api/v1/projects - Invoices:
GET https://api.fenceline.ai/api/v1/invoices - Payments:
GET https://api.fenceline.ai/api/v1/payments
For local dev, use
/api/v1/*paths as shown above.
Interactive Documentation
Swagger UI: Visit /developers/api for an interactive API explorer backed by the published OpenAPI specification. This page loads the spec from the same origin at /api/v1/openapi to avoid CORS issues.
Authentication
API Key Headers
Include your key using either header:
x-api-key: <YOUR_API_KEY>
or
Authorization: Bearer <YOUR_API_KEY>
Getting an API Key
If you need a key, contact your Fenceline admin. When rotating a key, update and verify your integration before revoking the previous key.
Host requirements
Production host: 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: If the rate-limiter backend is temporarily unavailable, endpoints stay available — a limiter outage is never surfaced as
503 Service Unavailable. See the Rate Limits section below for the degraded behavior.
Rate Limits
Default: 10 requests per minute per API key per endpoint, with optional per-key overrides. Failed authentication has a separate IP-based limit. Rate-limit responses include headers:
X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-Reset(epoch seconds)
Exceeding limits returns 429 Too Many Requests with Retry-After.
During a limiter outage: configuration determines whether requests pass through or use a per-instance limit. The limiter does not return 503 for an availability failure. Per-instance enforcement can permit a higher effective ceiling across instances; requests that exceed that fallback limit return 429 with Retry-After.
Common Query Parameters
contractorId(string) — Must match the contractor assigned to a scoped API key. Omitting it uses the key’s contractor. A query parameter cannot expand the key’s access.limit(number) — 1 to 100, default 50updatedAfter(ISO 8601) — Return items updated after this timestamp (where supported)
Response Envelope
All list endpoints return:
{
"apiVersion": "v1",
"items": [ /* array of resources */ ],
"nextPageToken": null
}
Projects, invoices, and payments return nextPageToken when another page is available. Pass it as pageToken with the same filters until the token is null. The customers endpoint supports limit but does not provide cursor pagination or delta sync.
Resources
Metadata
GET /metadata- Returns available resources and their fields for programmatic discovery.
Example:
curl -s -H "x-api-key: $API_KEY" "https://api.fenceline.ai/api/v1/metadata"
Response:
{
"apiVersion": "v1",
"resources": [
{
"name": "customers",
"path": "/api/v1/customers",
"query": ["contractorId", "limit"],
"fields": [ { "name": "id", "type": "string" }, ... ]
}
],
"query": { "common": ["contractorId", "limit", "updatedAfter", "pageToken"] }
}
Customers
- List:
GET /customers - Description: High-level customers derived from project ownership and user data.
- Supports:
contractorId,limit
Example request:
curl -s \
-H "x-api-key: $API_KEY" \
"https://api.fenceline.ai/api/v1/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)
Projects
totalAmount is optional and passes through the stored project value. Do not use it as a substitute for the invoice and payment amounts when reconciling accounting data.
- List:
GET /projects - Supports:
contractorId,limit,updatedAfter,pageToken
Example request:
curl -s \
-H "x-api-key: $API_KEY" \
"https://api.fenceline.ai/api/v1/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"
}
Invoices
- List:
GET /invoices - Source:
analytics_invoices - Supports:
contractorId,limit,updatedAfter,pageToken
Example request:
curl -s \
-H "x-api-key: $API_KEY" \
"https://api.fenceline.ai/api/v1/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"
}
amount is in minor currency units: 5000 means $50.00 USD. It is the invoice’s face value, not the outstanding balance.
status: paid | overdue | current | future | partial | void. Exclude void invoices when calculating receivables.
Payments
This feed contains receipt records only. Amounts are in minor currency units: 2500 means $25.00 USD. Refunds, voids, and holds are not a separate ledger in this feed, so a sum gives gross receipts rather than net cash collected. Reconcile adjustments through your payment processor or the Fenceline dashboard.
The response can also include status, type, held, settlementState, and processorTransactionId when the source record contains them. See the OpenAPI schema for their allowed values.
- List:
GET /payments - Source:
analytics_revenue_transactionswheretransactionType = PAYMENT_RECEIVED - Supports:
contractorId,limit,updatedAfter,pageToken
Example request:
curl -s \
-H "x-api-key: $API_KEY" \
"https://api.fenceline.ai/api/v1/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"
}
Examples
JavaScript (Node)
import fetch from 'node-fetch'
const API_KEY = process.env.EXTERNAL_API_KEY
const BASE = 'https://api.fenceline.ai/api/v1'
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)
Python
import os, requests
API_KEY = os.environ['EXTERNAL_API_KEY']
BASE = 'https://api.fenceline.ai/api/v1'
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'])
Pagination and delta sync
- Use
limit(1–100, default 50) - Use
pageTokento fetch next pages. Treat tokens as opaque. WhenEXTERNAL_API_TOKEN_SECRETis configured, pagination tokens are HMAC-signed and bound to the requesting contractor/key; tampering will cause rejection.
Delta Sync Guide (Recommended)
- Persist
lastSuccessfulUpdatedAt(ISO string) in your system after a completed sync. - On each run, request with
updatedAfter = (lastSuccessfulUpdatedAt - overlap), whereoverlapis 1–2 minutes to tolerate clock skew/eventual consistency. - Always follow
nextPageTokenuntil it is absent; never assume a single page completes a window. - De-duplicate by
idon your side to account for the overlap. - Suggested polling: every 5–15 minutes (or per integration needs). Back off on 429 using
Retry-After.
Example (pseudocode):
syncStartedAt = now()
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 = syncStartedAt
- Use
updatedAfterfor delta syncs. Combine with pagination for large result sets.
Errors
- 401 Unauthorized — Missing/invalid API key
- 403 Forbidden — Wrong host, missing scope, contractor mismatch, or IP restriction
- 429 Too Many Requests — Rate limit exceeded
- 400 Bad Request — Invalid query parameter
- 5xx Server Error — Unexpected error (safe to retry with backoff)
Error example:
{ "error": "Unauthorized: invalid API key" }
Mapping Guidance
- QuickBooks
- Customer ↔ Customer
- Invoice ↔ Invoice
- Payment ↔ Payment/ReceivePayment
- Salesforce
- Customer ↔ Account/Contact
- Project ↔ Opportunity (or custom object)
- Invoice/Payment ↔ Custom objects
- ServiceM8
- Customer ↔ Client
- Project ↔ Job
- Invoice/Payment ↔ Invoice/Payment
Best Practices
- Use the contractor assigned to the API key. A mismatched
contractorIdis rejected. - Cache responses when synchronizing
- Respect rate limits; implement retries with jitter
- Plan for additive fields in responses (ignore unknown fields)
Inbound Webhooks
In addition to the read-only data endpoints above, Fenceline provides webhook endpoints for receiving data from external systems.
Lead Submission Webhook
Receive leads from third-party sources like Facebook Lead Ads, Google Ads, Zapier, or custom integrations.
Endpoint: POST https://www.fenceline.ai/api/webhooks/leads/inbound
Host: Send webhook requests to
www.fenceline.ai, notapi.fenceline.ai. Theapihost serves only the read-only/api/v1endpoints and returns 404 for this path. The barefenceline.aihost answers with a 308 redirect, which some form tools and HTTP clients don't follow on a POST.
Required Scope: write:leads
This endpoint requires an API key with the
write:leadsscope. Contact your Fenceline administrator to request a key with this scope enabled.
Request Headers
x-api-key: <YOUR_API_KEY>
Content-Type: application/json
Request Body
| 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 |
Example Request
curl -X POST "https://www.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 Response (201 Created)
{
"success": true,
"projectId": "abc123xyz",
"jobNumber": 1042,
"message": "Lead created successfully"
}
Duplicate Response (200 OK)
When the same externalId is submitted again:
{
"success": true,
"projectId": "abc123xyz",
"duplicate": true,
"message": "Lead already exists"
}
Error Responses
| 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 |
Lead Source Mapping
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 |
Rate Limiting
- Limit: 60 requests per minute per API key
- Headers included in response:
X-RateLimit-Limit: Maximum requests allowedX-RateLimit-Remaining: Requests remaining in windowX-RateLimit-Reset: Unix timestamp when limit resets
Idempotency
Use 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.
Integration Examples
Zapier Webhook:
- Create a Zap with your lead source (Facebook Lead Ads, Google Sheets, etc.)
- Add a "Webhooks by Zapier" action with "POST" method
- Set URL to
https://www.fenceline.ai/api/webhooks/leads/inbound - Add header
x-api-keywith your API key - Map fields from your lead source to the request body
Facebook 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}}"
}
Changelog
- v1 (Beta): Initial endpoints for customers, projects, invoices, payments
- v1.1: Added inbound leads webhook (
POST /api/webhooks/leads/inbound)
Support
If you need an API key or have questions, contact your Fenceline administrator or support.