Cookie Preferences

Manage your cookie preferences below. Essential cookies cannot be disabled as they are required for the website to function properly.

Essential Cookies

Required for the website to function. These cannot be disabled.

Always On

Analytics Cookies

Help us understand how visitors interact with our website to improve the experience.

Marketing Cookies

Used to deliver personalized advertisements and track ad campaign effectiveness.

For more information, see our Cookie Policy

Skip to content

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 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: 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-Limit
  • X-RateLimit-Remaining
  • X-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 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
}

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_transactions where transactionType = 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 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):

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 updatedAfter for 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 contractorId is 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, not api.fenceline.ai. The api host serves only the read-only /api/v1 endpoints and returns 404 for this path. The bare fenceline.ai host 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: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://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 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://www.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.

Get in touch

Tell us about your business.

Ask about access, your workflow, or connecting your catalog. Fields marked required help the team follow up.

This sends a request to the Fenceline team. It doesn’t create an account. See our privacy policy for how we handle your information.