Add a job item
- Permissions
job_items:create,job_items:read,jobs:read,products:read- Safe retries
Idempotency-Key- Also available as
fieldtask_create_job_item
Adds one catalog product or service to an existing job as a line item, priced from the catalog.
You choose the product, one of its price options and a quantity. FieldTask copies the product's name, description and type and the option's price onto the job. The item keeps those values even if the catalog changes later. You can't set a custom name or price.
This endpoint only creates. It never matches or updates an existing item, so adding the same product twice gives the job two items. If you send the same body twice without the same Idempotency-Key, you get two items. See Idempotency and safe retries.
To add items while you create a job, use the items field of Create a job instead. It follows the same rules.
This page is generated from contract 2026-11-03-review.2, a review export that hasn't been released. Job items work only where FieldTask has enabled them.
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. - 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 job. Find it with
GET /public/v1/jobs, or usedata.idfromPOST /public/v1/jobs. - A product ID and, usually, a price ID from
GET /public/v1/products. The product must be available, and you may need to choose a price. See Choosing a price.
Permissions
Every request needs all of these permissions:
| Permission | Dependency in the permission map |
|---|---|
job_items:create | Depends on job_items:read, jobs:read, products:read |
job_items:read | Depends on jobs:read |
jobs:read | No dependencies |
products:read | No dependencies |
No inputs need additional permissions.
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 job is identified by job_id in the URL. It must be a job in your organization that hasn't been deleted.
The request body is a single JSON object. Only product_id is required.
| Field | Required | What to send |
|---|---|---|
product_id | Yes | ID of a product or service from GET /public/v1/products. It must be approved in your catalog and have is_available: true. |
price_id | Sometimes | ID of one of the product's named price options. Required when the product has more than one eligible option. See Choosing a price. |
price_source | No | price or legacy. Send legacy to charge the product's legacy unit price. |
quantity | No | Decimal string, such as "1", "2.5" or "2.50". Defaults to "1.00". See Quantity. |
Anything else in the body is rejected with 422 validation_failed, including fields you might expect to set such as name, unit_price, status or allow_evidences.
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.
Choosing a price
FieldTask never picks a price for you. The request must identify exactly one of the product's eligible price options.
A product's eligible options are the entries in its prices array whose currency is your organization's currency. That includes the legacy unit price (source: "legacy"), which is listed only when it's greater than zero, and every named option (source: "price"), including recurring ones and zero-price ones.
FieldTask narrows the eligible options by what you send:
price_idkeeps only the named option with that ID. Send it exactly as returned by the products endpoint.price_sourcekeeps only options with thatsource.
If exactly one option is left, it's used. If none or more than one is left, the request is rejected with 422 validation_failed.
| The product's eligible options | What to send |
|---|---|
| One option of any kind | Nothing. You can still send its price_id or price_source. |
| Several named options | The chosen option's price_id. |
| The legacy price and one named option | price_source: "legacy" for the legacy price; price_id or price_source: "price" for the named option. |
| The legacy price and several named options | price_source: "legacy" for the legacy price; price_id for a named option. |
None (empty prices, or only other currencies) | The product can't be added through the API. |
When several options have the same amount, they're still different options. Choose by name and send the matching price_id; don't pick one because the amounts agree. If you're acting for a person, ask them which option they want.
A recurring option is charged once as the item's unit price. It doesn't create a subscription or a recurring job.
Quantity
quantityis a string, not a JSON number.- Up to 8 digits before the decimal point and up to 2 after:
"1","2.5"and"99999999.99"are valid. - No sign, no exponent and no leading zeros:
"-1","1e2"and"01"are rejected. "0"is accepted.- Responses always show two decimal places:
"1"comes back as"1.00".
What's saved on the job
The 201 response returns the new item as Get a job item would:
| Item field | Value |
|---|---|
name, description, item_type | Copied from the product. |
price_name | The chosen option's name; null for the legacy price. |
unit_price | The chosen option's amount. |
quantity | Your quantity, with two decimal places. |
amount | quantity × unit_price exactly, with four decimal places, such as "185.0000". |
currency | Your organization's currency. |
status | to do. |
allow_evidences | false. |
The item doesn't keep a link to the product or price. Later changes to the catalog, including price changes or deleting the product, don't change existing items.
Effect on the job
- The job's
job_valueincreases by the item'samount. Read it with Get a job. - The API doesn't check the job's status. You can add items to a job in any status, including
completedandcancelled. Checkstatusfirst if your workflow shouldn't change finished jobs. - Quotes and invoices keep their own line items. Adding a job item doesn't change any quote or invoice.
- Changes to
job_valuearen't covered by the job'srevision, and job items have no events. The event feed and webhooks don't report added items.
Idempotency and safe retries
Send an Idempotency-Key header with every create, and reuse the same key and body when you retry after a timeout, a connection error, a 5xx, 409 operation_in_progress or 429. A replay within seven days returns the original item with Idempotency-Replayed: true and adds nothing.
The full rules, including key format, scoping and the seven-day window, are in Idempotency and recovery. Two points are specific to job items:
- The job is part of the request. Sending the same key and body to a different job's URL returns
409 idempotency_key_reused. - Removed items aren't replayed. If the item or its job has since been deleted, replaying the key returns
410 resource_gone.
Job items have no external reference, so the key is your only protection against duplicates. Save it with your own record before you send.
Local creation and external synchronization
A 201 response means the item is saved on the job. Adding a job item doesn't queue any follow-up work: the operation at Operation-Location always reports integration.status: "not_applicable". Reading it needs job_items:read and jobs:read. See Get an operation.
Operation results are kept for seven days. After that, GET /public/v1/operations/{id} returns 410 operation_result_expired. That doesn't mean the item wasn't added.
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 or 429 | Wait for Retry-After seconds and retry with the same key. |
201 received | Store data.id and the Location and Operation-Location headers. Stop retrying. |
422 validation_failed | Read the product again with GET /public/v1/products/{id}. Check that it's available and choose a price as described above. |
| Key older than 7 days, or the result expired | Don't add the item again yet. List the job's items with GET /public/v1/jobs/{job_id}/items and compare. |
Never switch to a new key just because a response was lost. A new key means a new item.
Errors
Errors use application/problem+json. The top-level code is the stable machine-readable value. 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. See Errors.
| 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/jobs/{job_id}/items 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 job_items:create, job_items:read, jobs:read or products:read. 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, product details aren't readable in this environment, or a replay would return fields the connection can no longer read. | Check the connection's permissions, then retry with the same key and body. |
| 404 | resource_not_found | No job with this ID exists in your organization, the job has been deleted, job item creation isn't available in this environment, or the URL is wrong. | Check the job ID with GET /public/v1/jobs, and check the API origin and path (no trailing slash). |
| 405 | method_not_allowed | Job item creation isn't enabled in this environment. The Allow header lists the methods that are. | Contact FieldTask if you expected job item creation to be available. |
| 409 | idempotency_key_reused | This key was already used with a different body or for a different job. | Resend the original body to the original job with this key, or use a new key for a different item. |
| 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. | List the job's items with GET /public/v1/jobs/{job_id}/items before adding anything new. |
| 410 | resource_gone | This key added an item that has since been removed, or whose job has been deleted. | Don't add it again automatically. Check in FieldTask whether it was removed on purpose. |
| 413 | payload_too_large | The body is larger than 256 KiB. | Send only the documented fields. |
| 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 product doesn't exist, isn't approved or isn't available; or price_id and price_source don't identify exactly one eligible price option. | Read the product with GET /public/v1/products/{id} and choose a price as described in Choosing a price. 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, then retry. |
| 429 | daily_quota_exceeded | The organization or connection has used its daily allowance for this kind of request. 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 item may or may not have been added. | 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
Adding a job item counts against the same per-minute, concurrency and daily create allowances as other creates. A replay with the original key doesn't use another create. See Rate limits and quotas.
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 |
Location is the item's path, /public/v1/jobs/{job_id}/items/{id}. The server also returns the Idempotency-Key that was used, including one it generated.