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)

πŸ“₯ 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 /api/webhooks/leads/inbound

Required Scope: write:leads

Note: This endpoint requires an API key with the write:leads scope. 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://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 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 allowed
    • X-RateLimit-Remaining: Requests remaining in window
    • X-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:

  1. Create a Zap with your lead source (Facebook Lead Ads, Google Sheets, etc.)
  2. Add a "Webhooks by Zapier" action with "POST" method
  3. Set URL to https://api.fenceline.ai/api/webhooks/leads/inbound
  4. Add header x-api-key with your API key
  5. 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.