{"openapi":"3.0.3","info":{"title":"Fenceline API","version":"1.1.0","description":"Read-only API for accounting, BI, and CRM integrations with Fenceline contractor data.\n\n## Authentication\nEvery endpoint requires an API key, sent as an `X-API-Key` header or as a bearer token in the `Authorization` header. Keys are issued per organization and may be scoped to a contractor and to specific read scopes (`read:customers`, `read:projects`, `read:invoices`, `read:payments`).\n\n## Rate limiting\nRequests are limited per key per endpoint (default 10 requests per minute; per-key overrides are possible). Responses carry both the legacy `X-RateLimit-*` headers and the IETF draft `RateLimit` / `RateLimit-Policy` headers; a 429 also carries `Retry-After`. Repeated failed authentication attempts from one address are throttled and answered with 429.\n\n## Versioning and deprecation\nThe API is versioned in the URL path (`/api/v1`). Breaking changes ship as a new version, never in place. Deprecated versions or endpoints get at least 90 days' notice, signaled with `Deprecation` and `Sunset` headers (RFC 9745) and announced on the developer documentation page — except urgent security or legal changes, which may move faster. Backward-compatible additions (new fields, new endpoints) can appear without notice, so clients must tolerate unknown fields.","contact":{"email":"support@fenceline.ai","url":"https://www.fenceline.ai/developers"}},"servers":[{"url":"https://api.fenceline.ai/api/v1","description":"Production"},{"url":"http://localhost:5173/api/v1","description":"Local development"}],"tags":[{"name":"metadata","description":"API discovery"},{"name":"customers","description":"Customers"},{"name":"projects","description":"Projects"},{"name":"invoices","description":"Invoices"},{"name":"payments","description":"Payments"}],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"x-api-key"},"BearerAuth":{"type":"http","scheme":"bearer"}},"parameters":{"ContractorId":{"name":"contractorId","in":"query","description":"Filter to one contractor. Keys bound to a contractor are always scoped to that contractor; passing a different contractorId returns 403 with code CONTRACTOR_SCOPE_MISMATCH.","schema":{"type":"string","minLength":1}},"Limit":{"name":"limit","in":"query","description":"Maximum items to return.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},"UpdatedAfter":{"name":"updatedAfter","in":"query","description":"Return only records updated at or after this instant (RFC 3339 date-time).","schema":{"type":"string","format":"date-time"}},"PageToken":{"name":"pageToken","in":"query","description":"Opaque cursor from a previous response's nextPageToken. Tokens are bound to the same contractorId and API key; a foreign or malformed token returns 400 with code INVALID_PAGE_TOKEN.","schema":{"type":"string"}}},"headers":{"XRateLimitLimit":{"description":"Request allowance for the current window.","schema":{"type":"integer"}},"XRateLimitRemaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"XRateLimitReset":{"description":"When the current window resets, as Unix epoch seconds.","schema":{"type":"integer"}},"RateLimit":{"description":"IETF draft rate-limit header: limit, remaining, and seconds until reset for the current window (draft-ietf-httpapi-ratelimit-headers).","schema":{"type":"string","example":"\"default\";r=7;t=35"}},"RateLimitPolicy":{"description":"IETF draft rate-limit policy: allowance and window length in seconds (draft-ietf-httpapi-ratelimit-headers).","schema":{"type":"string","example":"\"default\";q=10;w=60"}},"RetryAfter":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable message."},"code":{"type":"string","description":"Machine-readable error code. Values: MISSING_API_KEY, INVALID_API_KEY, IP_NOT_ALLOWED, WRONG_HOST, MISSING_SCOPE, CONTRACTOR_SCOPE_MISMATCH, INVALID_QUERY, INVALID_PAGE_TOKEN, RATE_LIMIT_EXCEEDED, INTERNAL_ERROR."},"hint":{"type":"string","description":"How to resolve the error."},"details":{"type":"object","description":"Validation details for INVALID_QUERY (Zod error format).","additionalProperties":true},"correlationId":{"type":"string","description":"Correlation id for a server error — include it when contacting support."},"retryAfter":{"type":"integer","description":"Seconds to wait before retrying (RATE_LIMIT_EXCEEDED only)."}}},"Customer":{"type":"object","description":"Neutral customer record derived from projects and user data.","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"Project":{"type":"object","description":"High-level project record.","required":["id","contractorId","customerId"],"properties":{"id":{"type":"string"},"contractorId":{"type":"string"},"customerId":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"totalAmount":{"type":"number"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"Invoice":{"type":"object","description":"Invoice record.","required":["id","projectId","contractorId","customerId","amount","date","status"],"properties":{"id":{"type":"string"},"projectId":{"type":"string"},"contractorId":{"type":"string"},"customerId":{"type":"string"},"number":{"type":"string"},"amount":{"type":"integer","description":"Integer minor currency units — US cents (5000 = $50.00). This is the gross invoice face total, NOT the outstanding balance: payments and credits are not netted here."},"currency":{"type":"string","default":"USD"},"date":{"type":"string","format":"date-time"},"dueDate":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["paid","overdue","current","future","partial","void"],"description":"'partial' means partially paid; 'void' means the invoice was voided and should be excluded from receivables."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"Payment":{"type":"object","description":"Payment-received record from the revenue analytics feed. This v1 feed contains receipts only — refunds, voids, and holds are NOT included; reconcile refunds through your payment processor or the Fenceline dashboard. The lifecycle fields (status, type, held, settlementState, processorTransactionId) are emitted only when the underlying row carries them; most analytics-fed rows omit type, held, settlementState, and processorTransactionId.","required":["id","projectId","contractorId","customerId","amount","date"],"properties":{"id":{"type":"string"},"projectId":{"type":"string"},"contractorId":{"type":"string"},"customerId":{"type":"string"},"amount":{"type":"integer","description":"Integer minor currency units — US cents (5000 = $50.00)."},"currency":{"type":"string","default":"USD"},"date":{"type":"string","format":"date-time","description":"Receipt instant (the analytics writer's paymentDate when present)."},"method":{"type":"string"},"reference":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["received","completed","pending","refunded","returned","voided","failed"],"description":"Payment lifecycle status. 'received' means money is in but settlement may still be pending (rows from the revenue analytics writer arrive as 'paid' and are normalized to 'received'); 'completed' is reserved for charges promoted past settlement; 'pending' covers unsettled ACH. Absent on some older records. Refund-family values exist for forward compatibility — refund records themselves are not part of this v1 feed."},"type":{"type":"string","enum":["payment","refund"],"description":"Discriminates refund records from receipts. When absent, treat as a regular receipt. Present for forward compatibility — this v1 feed contains receipts only."},"held":{"type":"boolean","description":"True when the processor put a hold on the funds (fraud review, batch hold). Held payments should not count toward collected totals."},"settlementState":{"type":"string","enum":["pending","settled","returned","failed"],"description":"Settlement-state machine value: 'pending' for ACH before funding, 'settled' once money landed, 'returned' for a bounced ACH. Card payments typically start at 'settled'."},"processorTransactionId":{"type":"string","description":"Processor's canonical transaction id (e.g. Payabli referenceId), for cross-referencing against the processor's dashboard."}}},"CustomerListResponse":{"type":"object","required":["apiVersion","items","nextPageToken"],"properties":{"apiVersion":{"type":"string","enum":["v1"]},"items":{"type":"array","items":{"$ref":"#/components/schemas/Customer"}},"nextPageToken":{"type":"string","nullable":true,"description":"Always null: the customers list is not paginated."}}},"ProjectListResponse":{"type":"object","required":["apiVersion","items","nextPageToken"],"properties":{"apiVersion":{"type":"string","enum":["v1"]},"items":{"type":"array","items":{"$ref":"#/components/schemas/Project"}},"nextPageToken":{"type":"string","nullable":true,"description":"Cursor for the next page; null when no further pages are known."}}},"InvoiceListResponse":{"type":"object","required":["apiVersion","items","nextPageToken"],"properties":{"apiVersion":{"type":"string","enum":["v1"]},"items":{"type":"array","items":{"$ref":"#/components/schemas/Invoice"}},"nextPageToken":{"type":"string","nullable":true,"description":"Cursor for the next page; null when no further pages are known."}}},"PaymentListResponse":{"type":"object","required":["apiVersion","items","nextPageToken"],"properties":{"apiVersion":{"type":"string","enum":["v1"]},"items":{"type":"array","items":{"$ref":"#/components/schemas/Payment"}},"nextPageToken":{"type":"string","nullable":true,"description":"Cursor for the next page; null when no further pages are known."}}},"MetadataField":{"type":"object","required":["name","type"],"properties":{"name":{"type":"string"},"type":{"type":"string"},"required":{"type":"boolean"},"default":{"type":"string"}}},"MetadataResource":{"type":"object","required":["name","path","description","query","fields"],"properties":{"name":{"type":"string"},"path":{"type":"string"},"description":{"type":"string"},"query":{"type":"array","items":{"type":"string"}},"fields":{"type":"array","items":{"$ref":"#/components/schemas/MetadataField"}}}},"MetadataResponse":{"type":"object","required":["apiVersion","resources","query"],"properties":{"apiVersion":{"type":"string","enum":["v1"]},"resources":{"type":"array","items":{"$ref":"#/components/schemas/MetadataResource"}},"query":{"type":"object","properties":{"common":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"BadRequest":{"description":"Invalid query parameters (code INVALID_QUERY, with details) or an invalid, foreign, or malformed page token (code INVALID_PAGE_TOKEN).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"Missing or invalid API key (code MISSING_API_KEY or INVALID_API_KEY).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"The key cannot perform this request: wrong host (WRONG_HOST), missing scope (MISSING_SCOPE), contractor scope mismatch (CONTRACTOR_SCOPE_MISMATCH), or IP not in the key's allowlist (IP_NOT_ALLOWED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"TooManyRequests":{"description":"Rate limit exceeded (code RATE_LIMIT_EXCEEDED). Also returned for repeated failed authentication attempts from one address. Wait Retry-After seconds before retrying.","headers":{"Retry-After":{"$ref":"#/components/headers/RetryAfter"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"InternalServerError":{"description":"Unexpected server error (code INTERNAL_ERROR). The correlationId identifies the failure when contacting support.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/metadata":{"get":{"tags":["metadata"],"operationId":"getMetadata","summary":"API metadata","description":"Machine-readable discovery document: every resource with its path, query parameters, and field types. Requires an API key but no specific read scope.","responses":{"200":{"description":"Discovery document.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MetadataResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/customers":{"get":{"tags":["customers"],"operationId":"listCustomers","summary":"List customers","description":"Neutral customer list derived from projects and user data. Requires the read:customers scope. Not paginated: nextPageToken is always null.","parameters":[{"$ref":"#/components/parameters/ContractorId"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"description":"Customer list.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/projects":{"get":{"tags":["projects"],"operationId":"listProjects","summary":"List projects","description":"High-level project records, newest first (or oldest-updated first when updatedAfter is set). Requires the read:projects scope. Archived projects are excluded.","parameters":[{"$ref":"#/components/parameters/ContractorId"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/UpdatedAfter"},{"$ref":"#/components/parameters/PageToken"}],"responses":{"200":{"description":"Project list.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/invoices":{"get":{"tags":["invoices"],"operationId":"listInvoices","summary":"List invoices","description":"Invoice records, newest invoice date first (or oldest-updated first when updatedAfter is set). Requires the read:invoices scope.","parameters":[{"$ref":"#/components/parameters/ContractorId"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/UpdatedAfter"},{"$ref":"#/components/parameters/PageToken"}],"responses":{"200":{"description":"Invoice list.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/payments":{"get":{"tags":["payments"],"operationId":"listPayments","summary":"List payments","description":"Payment-received records (receipts), newest first (or oldest-updated first when updatedAfter is set). Requires the read:payments scope. This v1 feed contains receipts only — refunds, voids, and holds are NOT included, so totals computed from it are gross receipts; reconcile refunds through your payment processor or the Fenceline dashboard.","parameters":[{"$ref":"#/components/parameters/ContractorId"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/UpdatedAfter"},{"$ref":"#/components/parameters/PageToken"}],"responses":{"200":{"description":"Payment list.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"}}}}}}