FieldCamp
Resources

Invoices | FieldCamp API

Use the FieldCamp Invoices API to create, list, update, void, and collect on v1 invoices with line items, discounts, markup, and Stripe payment links.

The FieldCamp Invoices API is how your integration turns completed work into money in the bank — create draft invoices, attach line items with document-level or item-level discounts and markup, push status changes, and hand the customer a hosted Stripe payment link. With release_v2, the resource is available under the versioned path /api/v1/invoices, which adds server-side clientId, status, and since filters and a dedicated payment-link endpoint. If you want the in-app workflow instead, the Creating invoices guide walks through the UI step by step.

What changed in release_v2

The /api/v1/invoices namespace is now the recommended path for new integrations. The unversioned /api/invoices endpoints still exist for backward compatibility, but they do not expose the new filters or the Stripe payment-link helper.

  • Versioned base path: /api/v1/invoices (instead of /api/invoices).
  • Server-side filters on the list endpoint: clientId, status, and since.
  • Status vocabulary aligned with the in-app invoice views and analytics: unpaid, paid, partial, and overdue.
  • Document-level and item-level discount and markup fields on the same payload.
  • New GET /api/v1/invoices/{id}/payment-link endpoint that returns a hosted Stripe checkout URL.

This endpoint requires an fc_live_ API key. See Authentication for header conventions, Errors for the error envelope, and Rate limits for throttling rules.

When to use the Invoices API

Most integrations hit /api/v1/invoices the moment a job is wrapped — the technician marks the job complete, and your backend rolls up the products and services used on the visit into one invoice for the client. You can also use it to:

How an invoice is structured

Every FieldCamp invoice has three layers — header, line items, and totals — and the API mirrors that shape one-for-one.

  • HeaderclientId, billing/property address, invoice number, issue date, due date, currency, notes, optional reference to the originating job, and optional document-level discount and markup.
  • Line items — an array of items pulled from your price book and bundles. Each line carries itemId, description, quantity, unitPrice, an item-level markup (percent or amount), an item-level discount (percent or amount), and taxIds resolved from Taxes.
  • TotalssubTotal and total are stored exactly as you send them. This endpoint performs no arithmetic over the items, and there are no discountTotal, markupTotal or taxTotal fields on the record. Send the figures your own system computed; omit them and each is stored as 0.

Reuse the same itemId values you use on jobs. That keeps the client detail page consistent and avoids creating duplicate items in your price book.

GET /api/v1/invoices accepts the following query parameters:

  • clientId — return invoices for a single client. Useful for portals and statement views.
  • status — exact match against unpaid, paid, partial, or overdue. partial covers any invoice with deposits applied but a balance remaining.
  • since — ISO 8601 timestamp. Returns invoices whose updatedAt is greater than or equal to the supplied value. Ideal for nightly delta syncs.
  • limit and cursor — standard pagination parameters used across the API.

Example: invoices for one client

curl "https://api.fieldcamp.ai/api/v1/invoices?clientId=$CLIENT_ID" \
  -H "Authorization: Bearer $FIELDCAMP_API_KEY"

Example: pull overdue invoices

curl "https://api.fieldcamp.ai/api/v1/invoices?status=overdue&limit=100" \
  -H "Authorization: Bearer $FIELDCAMP_API_KEY"

Example: incremental sync

curl "https://api.fieldcamp.ai/api/v1/invoices?since=2026-05-01T00:00:00Z" \
  -H "Authorization: Bearer $FIELDCAMP_API_KEY"

Create an invoice with line items, discounts, and markup

Send a POST /api/v1/invoices request with the header and a lineItems array. Discounts and markup can be applied at the line level on each item, or as a single document-level value that applies to the whole invoice. Markup is computed on unitPrice before tax; discounts are computed after markup.

Look up or create the client

Call GET /api/v1/clients?search=<email> to find an existing record, or POST /api/v1/clients to create one. Reuse the returned id as the clientId on the invoice.

Resolve item and tax IDs

Pull itemId values from Items and taxId values from Taxes. Cache them locally — they almost never change between invoices.

Build the line items

For each line, set quantity, unitPrice, optional markup ({ type: "percent" | "amount", value }), optional discount ({ type: "percent" | "amount", value }), and taxIds. The server multiplies, marks up, discounts, then taxes — in that order.

POST the invoice

Send the payload to POST /api/v1/invoices. The response includes the server-calculated subTotal, discountTotal, markupTotal, taxTotal, and total so you can reconcile before persisting locally.

Store the returned ID

Keep the returned id alongside your internal record. You will need it for status updates, edits, and the Stripe payment-link call below.

Example request

curl -X POST https://api.fieldcamp.ai/api/v1/invoices \
  -H "Authorization: Bearer $FIELDCAMP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "clientId": "cli_123",
    "jobId": "job_456",
    "issueDate": "2026-05-29",
    "dueDate": "2026-06-12",
    "status": "unpaid",
    "currency": "USD",
    "discount": { "type": "percent", "value": 5 },
    "lineItems": [
      {
        "itemId": "itm_789",
        "description": "Annual HVAC tune-up",
        "quantity": 1,
        "unitPrice": 199.00,
        "markup": { "type": "percent", "value": 10 },
        "taxIds": ["tax_001"]
      }
    ],
    "notes": "Thanks for your business!"
  }'

Send monetary values as numbers in major units of the invoice's currency (e.g. 199.50). Mixing strings and numbers across line items is the most common cause of 400 validation errors. See Errors for the full catalog.

Update invoices and move statuses

  • Fetch oneGET /api/v1/invoices/{id} returns the full invoice including computed totals and any signature blocks captured in the field.
  • UpdatePUT /api/v1/invoices/{id} is a partial update, but its contract differs from create in both directions: it adds paidAmount, paymentStatus and actionStatus, and it removes document-level taxes — sending taxes on an update returns 400.
  • Status transitions — set status to unpaid, paid, partial, or overdue. Status drives where the invoice shows up in the invoice views and analytics and which clients see it in their portal.
  • DeleteDELETE /api/v1/invoices/{id} soft-deletes any invoice regardless of state and returns { id, deleted: true }. There is no draft-only check and no guard on a sent or paid invoice, so treat it as a destructive call and prefer changing the payment status instead if you want your reporting to stay intact. It emits invoice.deleted.

overdue is set automatically by FieldCamp when dueDate passes and the invoice is not fully paid. You can also set it explicitly via PUT if your billing rules require it earlier.

If your tenant has connected Stripe, call GET /api/v1/invoices/{id}/payment-link for a payment URL to send to the customer.

It returns four fields:

{
  "success": true,
  "data": {
    "paymentUrl": "https://app.fieldcamp.ai/documentView?token=…&docId=…",
    "invoiceId": "6a84…",
    "amount": 272.5,
    "currency": "usd"
  }
}

paymentUrl is a FieldCamp-hosted invoice page where the customer pays through your connected Stripe account — it is not a Stripe Checkout URL, so there is no stripePaymentIntentId to reconcile against and no expiresAt.

The link is stable and non-expiring: the view token is minted once per invoice and reused, so calling this endpoint again returns the same URL. That is deliberate — a link already sitting in a customer's inbox keeps working. It is safe to call repeatedly, and safe to store.

curl https://api.fieldcamp.ai/api/v1/invoices/$INVOICE_ID/payment-link \
  -H "Authorization: Bearer $FIELDCAMP_API_KEY"

You can email the same link to the client via two-way text messaging, or embed it in your own customer portal. Stripe handles the card capture; FieldCamp posts the payment back against the invoice automatically and moves the status from unpaid (or partial) to paid when the balance hits zero. For deposit-style up-front payments, see Deposits and partial payments.

Endpoints

  • GET /api/v1/invoices — list invoices filtered by clientId, status, and/or since.
  • POST /api/v1/invoices — create an invoice with line items, discounts, markup, and tax.
  • GET /api/v1/invoices/{id} — fetch a single invoice including computed totals.
  • PUT /api/v1/invoices/{id} — update header fields, line items, or status.
  • DELETE /api/v1/invoices/{id} — delete a draft (prefer a status change for sent invoices).
  • GET /api/v1/invoices/{id}/payment-link — return a Stripe hosted payment URL for the invoice.

Best practices

FAQs

Which statuses can I send to POST and PUT? unpaid, paid, partial, and overdue. New invoices default to unpaid if no status is sent.

How do document-level and item-level discounts combine? Item-level markup and discount are applied first while computing each line. The document-level discount is then applied to the resulting subTotal before tax. The final total is subTotal - documentDiscount + taxTotal.

Can I filter the list endpoint by both clientId and status? Yes. All three filters (clientId, status, since) are independent and can be combined in any order on GET /api/v1/invoices.

Does the payment link expire? No. The response carries no expiresAt, and calling the endpoint again returns the same URL rather than a new one — the token is minted once per invoice and reused, so a link you already sent keeps working. The customer always sees the invoice's current balance, because the page reads it live.

What happens if Stripe is not connected? GET /api/v1/invoices/{id}/payment-link returns 400 with code INTEGRATION_NOT_CONNECTED. Connect Stripe via Connect Stripe for online payments and retry.

Troubleshooting

  • 400 lineItems is requiredPOST /api/v1/invoices requires at least one line item. Add a placeholder labour line if you have nothing else to bill.
  • Totals don't match my upstream system — remember the calculation order: per-line markup, per-line discount, per-line tax, then document-level discount. Compute totals in the same order before comparing.
  • status ignored on update — only unpaid, paid, partial, and overdue are accepted. Other values are silently dropped; check the response body for the canonical status.
  • since not returning new rowssince matches updatedAt, not issueDate. Use the most recent updatedAt you have seen as the next cursor.

Endpoints

GET/api/v1/invoices

Authorization

BearerAuth
AuthorizationBearer <token>

JWT from POST /api/auth/login (response.data.token). Tokens do not expire.

In: header

Query Parameters

page?integer

Page number, 1-based.

limit?integer

Rows per page. Values above 100 are clamped to 100.

since?string

Return only rows whose updatedAt is at or after this ISO-8601 timestamp — the incremental-sync cursor. A value that is not a date returns 400 rather than being ignored.

sort?string

Order the result as field:direction. Direction is asc or desc (default desc). Sortable fields: createdAt, updatedAt, total, paymentStatus, dueDate, documentNumber. An unsupported field returns 400.

clientId?string

Return only invoices for this client id.

status?string

Filter by payment status, matched case-insensitively — paid or unpaid. Matching is on the whole value, so paid never also returns unpaid.

Response Body

application/json

application/json

application/json

curl -X GET "https://example.com/api/v1/invoices"
{}
POST/api/v1/invoices

Authorization

BearerAuth
AuthorizationBearer <token>

JWT from POST /api/auth/login (response.data.token). Tokens do not expire.

In: header

Response Body

application/json

application/json

curl -X POST "https://example.com/api/v1/invoices"
{}
GET/api/v1/invoices/{id}

Authorization

BearerAuth
AuthorizationBearer <token>

JWT from POST /api/auth/login (response.data.token). Tokens do not expire.

In: header

Path Parameters

id*string

Response Body

application/json

application/json

curl -X GET "https://example.com/api/v1/invoices/665f1c2ab3e4d5f6a7b8c9d0"
{}
PUT/api/v1/invoices/{id}

Authorization

BearerAuth
AuthorizationBearer <token>

JWT from POST /api/auth/login (response.data.token). Tokens do not expire.

In: header

Path Parameters

id*string

Response Body

application/json

application/json

curl -X PUT "https://example.com/api/v1/invoices/665f1c2ab3e4d5f6a7b8c9d0"
{}
DELETE/api/v1/invoices/{id}

Authorization

BearerAuth
AuthorizationBearer <token>

JWT from POST /api/auth/login (response.data.token). Tokens do not expire.

In: header

Path Parameters

id*string

Response Body

application/json

application/json

curl -X DELETE "https://example.com/api/v1/invoices/665f1c2ab3e4d5f6a7b8c9d0"
{}
GET/api/v1/invoices/{id}/payment-link

Authorization

BearerAuth
AuthorizationBearer <token>

JWT from POST /api/auth/login (response.data.token). Tokens do not expire.

In: header

Path Parameters

id*string

Response Body

application/json

application/json

curl -X GET "https://example.com/api/v1/invoices/665f1c2ab3e4d5f6a7b8c9d0/payment-link"
{}

On this page