Create a draft invoice
- Permissions
invoices:create,invoices:read,customers:read,products:read- Safe retries
Idempotency-Key- Also available as
fieldtask_create_invoice
Creates one draft invoice for an existing customer, priced from your product and service catalog, with optional taxes and a discount.
A draft invoice is only saved in FieldTask. Creating it doesn't send it to the customer, doesn't collect or record a payment, and doesn't link it to a job or copy a job's items. The invoice stays a draft until someone in your organization sends it from FieldTask.
The invoice, its line items, taxes and discount are saved together or not at all. This endpoint only creates: it never matches or updates an existing invoice. If you send the same body twice without the same Idempotency-Key, you get two invoices. See Idempotency and safe retries.
This page is generated from contract 2026-11-03-review.2, a review export that hasn't been released.
Before you start
You need:
- API access for your organization. An owner or admin requests it in Settings → API connections. Until FieldTask enables it, every request returns
403 api_access_not_enabled. - Invoicing enabled for your organization. If your organization can't use invoices in FieldTask, every invoice request returns
404 resource_not_found. - A REST credential with the permissions below: an API token created in Settings → API connections, or an access token from an OAuth client-credentials connection. Tokens from an MCP sign-in don't work with the REST API.
- The ID of an existing customer. Find it with
GET /public/v1/customers, or create the customer first withPOST /public/v1/customers. Archived customers can be used. - The product and price IDs for every line item. Find them with
GET /public/v1/products. Each line item is priced from the catalog; you can't send your own prices.
Permissions
Every request needs all of these permissions:
| Permission | Dependency in the permission map |
|---|---|
invoices:create | Depends on customers:read, invoices:read, products:read |
invoices:read | Depends on customers:read |
customers:read | No dependencies |
products:read | No dependencies |
No input needs further permissions. REST and MCP connections can both apply taxes and a discount.
Adding a permission to a connection doesn't happen automatically. An owner or admin edits it in Settings → API connections. API tokens keep the same token string when their permissions change. OAuth integrations must also request the new scope when they get an access token.
Request
Responses
- 201
- 400
- 401
- 403
- 404
- 405
- 409
- 410
- 413
- 415
- 422
- 429
- 503
Created
Response Headers
Responses must not be cached.
The operation key used for this create, including a server-generated key when omitted.
Server-generated request correlation ID.
Bad Request
Response Headers
Responses must not be cached.
Server-generated request correlation ID.
Unauthorized
Response Headers
Responses must not be cached.
Server-generated request correlation ID.
Forbidden
Response Headers
Responses must not be cached.
Server-generated request correlation ID.
Not Found
Response Headers
Responses must not be cached.
Server-generated request correlation ID.
Method Not Allowed
Response Headers
Responses must not be cached.
Server-generated request correlation ID.
Conflict
Response Headers
Responses must not be cached.
Seconds before retry; present on 429 and an in-progress operation.
Server-generated request correlation ID.
Gone
Response Headers
Responses must not be cached.
Server-generated request correlation ID.
Request Entity Too Large
Response Headers
Responses must not be cached.
Server-generated request correlation ID.
Unsupported Media Type
Response Headers
Responses must not be cached.
Server-generated request correlation ID.
Unprocessable Entity
Response Headers
Responses must not be cached.
Server-generated request correlation ID.
Too Many Requests
Response Headers
Responses must not be cached.
Seconds before retry; present on 429 and an in-progress operation.
Server-generated request correlation ID.
Service Unavailable
Response Headers
Responses must not be cached.
Server-generated request correlation ID.
Field guide
The request body is a single JSON object. customer_id, issued_date and items are required.
| Field | Required | What to send |
|---|---|---|
customer_id | Yes | ID of a customer in this organization. Archived customers are accepted. |
issued_date | Yes | The invoice date, such as 2026-11-03. See Dates. |
items | Yes | 1–100 catalog line items. See Line items. |
due_date | No | The payment due date, such as 2026-12-03. Defaults to null. |
name | No | A label for the invoice, up to 256 characters. Returned as name. Defaults to null. |
subject | No | Up to 256 characters. Saved with the invoice but not returned by the API. |
po_number | No | Your customer's purchase order number, up to 256 characters. Saved with the invoice but not returned by the API. |
notes | No | Up to 1,024 characters. Saved with the invoice but not returned by the API. |
tax1, tax2 | No | Up to two named taxes, each {"name": "…", "type": "percent" or "fixed", "amount": "…"}. See Taxes. |
discount | No | {"type": "percent" or "fixed", "amount": "…"}. A percentage can't be more than 100. See Totals and discounts. |
Anything else in the body is rejected with 422 validation_failed, including fields you might expect to set such as status, invoice_number, currency, job_id or a custom unit price. The organization and the currency always come from your credential's organization, and FieldTask assigns the next invoice number automatically.
Decimal values are JSON strings with up to 2 decimal places, never numbers. Send "8.25", not 8.25.
Body and header rules
- Send
Content-Type: application/jsonwith an uncompressed body. AContent-Encodingheader is rejected with415. - The body can be at most 256 KiB and nest at most 10 levels deep.
- Duplicate property names,
NaNandInfinityare rejected with400 invalid_json. - The URL must have no query string and no trailing slash.
Line items
Each entry in items adds one product or service from your catalog to the invoice.
| Item field | Required | What to send |
|---|---|---|
product_id | Yes | ID of a product or service from GET /public/v1/products. |
price_id | Sometimes | ID of one of the product's named prices. Required when more than one price option applies. |
price_source | No | price or legacy. Send legacy to use the product's legacy unit price explicitly. |
quantity | No | A positive decimal string with up to 8 whole digits and 2 decimal places, such as "1" or "2.50". Zero isn't allowed. Defaults to "1.00". |
tax1 | No | true to apply the invoice's tax1 to this item. Defaults to false. |
tax2 | No | true to apply the invoice's tax2 to this item. Defaults to false. |
- The product must be active and available in your catalog. A product marked unavailable is rejected.
- Only prices in your organization's currency are considered. If no price option matches your selection, or more than one does, the request is rejected with
422 validation_failedrather than FieldTask choosing one for you. Sendprice_idwhenever a product has more than one price. - The item's name, description, type and unit price are copied from the catalog when the invoice is created. Later catalog changes don't change the invoice. A product whose description is longer than 1,200 characters can't be added to an invoice.
- If any item is invalid, no invoice is created.
Taxes
An invoice can have up to two taxes, tax1 and tax2. Each has a name (1–64 characters, such as "Sales tax"), a type and an amount:
"type": "percent":amountis a percentage of each flagged item's amount."8.25"means 8.25%."type": "fixed":amountis added once for each flagged item, not once per invoice.
A tax applies only to the items where you set "tax1": true or "tax2": true. Flagging an item with a tax the invoice doesn't define is rejected with 422 validation_failed. Taxes aren't compounded: both are calculated on the item's amount before tax.
Totals, taxes and discounts
FieldTask calculates the invoice's amounts; you can't send them. For each item:
amountis quantity × unit price, rounded to cents.tax1_amountandtax2_amountare the item's taxes, each rounded to cents, or"0.00"when the item isn't flagged.taxed_amountis the amount plus both taxes.
For the invoice:
| Response field | Meaning |
|---|---|
total_tax1, total_tax2 | The sum of each tax over the items. |
total_amount | The sum of the items' taxed amounts: the total including tax, before the discount. |
discount | The discount you sent, or null. A percentage discount is taken from total_amount, including tax. |
discounted_amount | The final invoice total: total_amount minus the discount, rounded to cents. |
paid_amount | The sum of recorded payments. Always "0.00" for a new draft. |
balance | discounted_amount minus paid_amount. |
Rounding uses round-half-to-even at each step, so a value exactly halfway rounds to the nearest even cent. For example, 8.25% of 370.00 is 30.525, which becomes "30.52". Quotes round halves up instead, so the same figures give "30.53" on a quote. Display the returned values rather than recalculating them, and parse them as decimals, not floating-point numbers.
tax1, tax2 and discount in the response show what's stored: their type and both percent_amount and fixed_amount, where the one that doesn't apply is "0.00".
Dates
issued_date and due_date are calendar dates in YYYY-MM-DD format, with no time or time zone. FieldTask stores them exactly as sent. List filters use issued_date. See Dates and times.
The draft you get back
A successful create returns the invoice with every field, including its items, taxes and discount:
statusisdraft.invoice_numberis assigned by FieldTask. You can't choose it.currencyis your organization's currency.customersummarizes the customer.
Read the invoice again later with GET /public/v1/invoices/{id}, using the ID from data.id or the Location header.
Idempotency and safe retries
Networks fail after a request is sent. The Idempotency-Key header lets you retry without creating a duplicate invoice.
- Before sending, generate a new key for the invoice, such as a UUID, and save it with your own record.
- Send the request with that key.
- If you get no response, a timeout or a
5xx, send the same body with the same key again, with backoff.
| Situation | Result |
|---|---|
| New key | The invoice is created. Response 201, Idempotency-Replayed: false. |
| Same key, same body, within 7 days | The original result is returned again. No second invoice is created and no further allowance is used. Response 201, Idempotency-Replayed: true. |
| Same key, different body | 409 idempotency_key_reused. Nothing is created. |
| Same key while the first request is still running | 409 operation_in_progress with Retry-After. Retry with the same key and body. |
| Same key more than 7 days later | 409 idempotency_window_expired. A key can never be reused for a different invoice. |
A replay returns the original response, not the invoice's current state. If the customer in that response is no longer visible to the connection, the replay is rejected with 403 operation_forbidden; if the invoice has been deleted, with 410 resource_gone.
Invoices have no external_reference, so keep the returned data.id with your own record. See Idempotency and recovery for key scope, what counts as the same body, and how replays are authorized.
Local creation and external synchronization
A 201 response means the draft invoice is saved in FieldTask. Nothing further is needed for it to exist, and follow-up work never removes it.
If your organization creates invoices in a connected CRM, FieldTask also sends the new draft there after it's saved. That follow-up only creates the record in the connected CRM. It doesn't send the invoice to the customer, record a payment or attach files. Its progress is reported on the operation, not on the invoice.
To check it, read the Operation-Location URL from the 201 response with the same credential. It needs invoices:read and customers:read, which the create already requires:
curl "$FIELDTASK_API_ORIGIN/public/v1/operations/OPERATION_ID" \
--header "Authorization: Bearer $FIELDTASK_API_TOKEN"
data.local_status is always committed. data.integration.status is not_applicable when your organization doesn't create invoices in a connected CRM. For the other values, see Local creation and external synchronization.
FieldTask makes one attempt to create the invoice in the connected CRM:
- It's reported as
blockedwithconfiguration_requiredwhen an item uses a fixed tax. The connected CRM can't represent a fixed per-item tax, and FieldTask won't publish a lower total. Use percentage taxes if the invoice must be synchronized. - It's reported as
blockedwithreconciliation_requiredwhen the invoice was changed in FieldTask or is no longer a draft before synchronization ran, the customer was removed, or the customer is linked to more than one contact in the connected CRM. - It's reported as
failedwithprovider_unavailablewhen a request to the connected CRM failed before the invoice was sent. - It's reported as
outcome_unknownwhen the request reached the connected CRM but FieldTask couldn't confirm the result. Check there before doing anything that could create a duplicate.
In the review contract, invoices created through the API aren't sent to a connected CRM. If your organization creates invoices in a connected CRM, the operation reports a blocked target with configuration_required. The invoice itself is still created.
Never create another invoice under a new key to force synchronization. That creates a second invoice in FieldTask.
Operation results are kept for seven days. After that, GET /public/v1/operations/{id} returns 410 operation_result_expired. That doesn't mean the invoice wasn't created.
Recovering from failures
| What happened | What to do |
|---|---|
Timeout, connection error or 5xx | Retry the same body with the same key, with exponential backoff. |
409 operation_in_progress | Wait for Retry-After seconds and retry with the same key. |
429 | Wait for Retry-After seconds and retry with the same key. |
201 received | Store data.id, data.invoice_number and the Location and Operation-Location headers. Stop retrying. |
| Key older than 7 days, or the result expired | Don't create again yet. Look for the invoice with GET /public/v1/invoices?customer_id=…&status=draft or your saved invoice ID. |
Never switch to a new key just because a response was lost. A new key means a new invoice.
Errors
Errors use application/problem+json. The top-level code is the stable machine-readable value; error adds a readable message, a suggested fix and a documentation link:
{
"type": "about:blank",
"title": "Unprocessable Entity",
"status": 422,
"code": "validation_failed",
"detail": "The request contains invalid fields.",
"instance": "urn:uuid:99999999-9999-4999-8999-999999999993",
"request_id": "99999999-9999-4999-8999-999999999993",
"error": {
"code": "validation_failed",
"message": "The request contains invalid fields.",
"fix": "Check the documented request fields.",
"docs_url": "https://api.fieldtask.example/public/docs/"
}
}
error.code usually equals code. For a missing permission, code is insufficient_scope and error.code is missing_permission. Validation errors don't say which field failed, and error responses never echo submitted values.
| Status | code | Meaning | What to do |
|---|---|---|---|
| 400 | invalid_query | The URL has a query string. Create requests accept none. | Send the request to /public/v1/invoices with no query parameters. |
| 400 | invalid_json | The body isn't one valid JSON value, repeats a property name, uses NaN/Infinity or nests deeper than 10 levels. | Send a single JSON object encoded as UTF-8. |
| 400 | invalid_operation_key | Idempotency-Key isn't 16–128 printable ASCII characters. | Use a UUID or another 16–128 character key without spaces at either end. |
| 401 | invalid_token | The bearer token is missing, malformed, expired, revoked or issued for a different resource (for example an MCP sign-in). | Send a current REST API token or REST OAuth access token as Authorization: Bearer …. |
| 403 | api_access_not_enabled | FieldTask hasn't enabled API access for this organization, or has disabled it. | An owner or admin requests access in Settings → API connections. |
| 403 | insufficient_scope | The connection lacks one of the permissions this request needs. error.code is missing_permission. | Add the permission named in error.message to the connection, then retry with the same key. |
| 403 | operation_forbidden | The connection's access changed while the request was processed, or a replay would return a customer the connection can no longer read. | Check the connection's permissions, then retry with the same key and body. |
| 404 | resource_not_found | Invoicing isn't enabled for your organization, invoices aren't available in this environment, or the URL is wrong. | Check that your organization can use invoices in FieldTask, and that the path has no trailing slash. |
| 405 | method_not_allowed | Invoice creation isn't enabled in this environment. The Allow header lists the methods that are. | Contact FieldTask if you expected invoice creation to be available. |
| 409 | idempotency_key_reused | This key was already used with a different body. | Resend the original body with this key, or use a new key for a different record. |
| 409 | operation_in_progress | A request with the same key is still being processed. | Wait for Retry-After seconds, then retry with the same key and body. |
| 409 | idempotency_window_expired | This key was used more than seven days ago. It can't be replayed or reused. This doesn't mean the original request failed. | Reconcile using your saved invoice ID, or list the customer's draft invoices, before creating anything new. |
| 410 | resource_gone | A replay of this key found that the invoice it created has since been deleted. | Don't create it again automatically. Check in FieldTask whether it was removed on purpose. |
| 413 | payload_too_large | The body is larger than 256 KiB. | Reduce the body, for example by sending fewer line items. |
| 415 | unsupported_media_type | Content-Type isn't application/json, or the body is compressed. | Send an uncompressed body with Content-Type: application/json. |
| 422 | validation_failed | A field is missing, unknown or invalid; the customer or a product isn't eligible; a price selection matches no option or more than one; an item is flagged with a tax the invoice doesn't define; a percentage discount is over 100; or your plan's invoice allowance for today is used up. | Check the request against the field guide, and send price_id for products with several prices. The response doesn't name the field. |
| 429 | rate_limit_exceeded | Too many requests in the current minute, or too many concurrent requests, for the organization or connection. | Wait for Retry-After seconds. Retry creates with the same key. |
| 429 | daily_quota_exceeded | The organization or connection has used its daily API create allowance. Allowances reset at 00:00 UTC. | Wait for Retry-After seconds, or check remaining allowance in Settings → API connections. |
| 503 | temporarily_unavailable | The service couldn't complete the request. The invoice may or may not have been created. | Retry with the same key and body, with exponential backoff. |
Every response, including errors, has an X-Request-ID header. Include it, with the time and the status code, when you contact support. Never send tokens or secrets.
Limits
Invoice creates count against the same limits as other creates: per-minute and concurrency limits, and a daily create allowance that resets at 00:00 UTC, for both the organization and each connection. When you hit a limit you get 429 with a Retry-After header in seconds. Replaying an already-created invoice with its original key doesn't count as another create. See Rate limits.
Your FieldTask plan also applies. On the free plan, or without a plan, an organization can create one invoice per day in its own time zone, counting invoices created in FieldTask as well as through the API. A further create that day is rejected with 422 validation_failed.
Response headers
A successful create returns 201 Created with these headers:
| Header | Always present | Value |
|---|---|---|
Cache-Control | Yes | Always no-store |
Idempotency-Key | Yes | string |
Idempotency-Replayed | Yes | string |
Location | Yes | string |
Operation-Location | Yes | string |
X-Request-ID | Yes | uuid |
The server also returns the Idempotency-Key that was used, including one it generated.