Fenceline Logo

Fenceline Developer Guide

Comprehensive overview of the External API, authentication, endpoints, examples, and best practices.

πŸ“‹ Overview

Public Beta v1.0 Updated: 2025-08-09

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.

Key Information

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

πŸ” Discovery

Base URLs

Environment URL
Production https://api.fenceline.ai
Local Development http://localhost:5173/api/v1

Available Endpoints

Endpoints on Production are root-relative (no duplicate "/api"):

  • Metadata: GET https://api.fenceline.ai/metadata
  • OpenAPI: GET https://api.fenceline.ai/openapi (or same-origin: GET /api/v1/openapi)
  • Customers: GET https://api.fenceline.ai/customers
  • Projects: GET https://api.fenceline.ai/projects
  • Invoices: GET https://api.fenceline.ai/invoices
  • Payments: GET https://api.fenceline.ai/payments

Note: For local dev, use /api/v1/* paths as shown above.

Interactive Documentation

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.

πŸ” 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. Keys can be rotated without downtime.

Important Notes

⚠️ Production Requirement: In production, requests to /api/v1/* must be made via the api.fenceline.ai host.

Exception: The OpenAPI spec route (/api/v1/openapi) is intentionally available on all hosts (e.g., www.fenceline.ai and api.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 a Retry-After header.

⏱️ Rate Limits

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.

Common Query Parameters

  • contractorId (string) β€” Recommended to scope results to a single contractor/tenant
  • limit (number) β€” 1 to 100, default 50
  • updatedAfter (ISO 8601) β€” Return items updated after this timestamp (where supported)

Response Envelope

All list endpoints return:

{
  "apiVersion": "v1",
  "items": [ /* array of resources */ ],
  "nextPageToken": null
}

Note: Pagination tokens are reserved for future use; nextPageToken is currently null.


πŸ“¦ 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/metadata"

Response:

{
  "apiVersion": "v1",
  "resources": [
    {
      "name": "customers",
      "path": "/api/v1/customers",
      "query": ["contractorId", "limit"],
      "fields": [ { "name": "id", "type": "string" }, ... ]
    }
  ],
  "query": { "common": ["contractorId", "limit", "updatedAfter"] }
}

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/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

  • List: GET /projects
  • Supports: 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"
}

Invoices

  • List: GET /invoices
  • Source: analytics_invoices
  • Supports: 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


Payments

  • List: GET /payments
  • Source: analytics_revenue_transactions where transactionType = PAYMENT_RECEIVED
  • Supports: 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"
}

πŸ’‘ Examples

JavaScript (Node)

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)

Python

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'])

πŸ”„ Pagination & Delta Sync

  • Use limit (1–100, default 50)
  • Use 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.
  • Persist lastSuccessfulUpdatedAt (ISO string) in your system after a completed sync.
  • On each run, request with updatedAfter = (lastSuccessfulUpdatedAt - overlap), where overlap is 1–2 minutes to tolerate clock skew/eventual consistency.
  • Always follow nextPageToken until it is absent; never assume a single page completes a window.
  • De-duplicate by id on 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):

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()
  • Use updatedAfter for delta syncs. Combine with pagination for large result sets.

⚠️ Errors

  • 401 Unauthorized β€” Missing/invalid API key
  • 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

  • Always pass contractorId to scope data
  • Cache responses when synchronizing
  • Respect rate limits; implement retries with jitter
  • Plan for additive fields in responses (ignore unknown fields)

Changelog

  • v1 (Beta): Initial endpoints for customers, projects, invoices, payments

Support

If you need an API key or have questions, contact your Fenceline administrator or support.