---
title: "Creates a job (estimate or draft) for a customer, optionally with material, labor and custom-charge lines."
method: POST
path: "/api/v1/jobs"
tags: ["Jobs"]
---

# Creates a job (estimate or draft) for a customer, optionally with material, labor and custom-charge lines.

`POST /api/v1/jobs`

## Request

- Base URL: `https://app.flexonthejob.com`
- URL: `https://app.flexonthejob.com/api/v1/jobs`
- Auth: HTTP bearer

Same code path as the MVC "Create job" page. Requires an `Idempotency-Key`
header. Line totals are computed on the server from the lines you send. The job
number is generated when omitted. Initial status: `Estimated`, or `Draft`
when `isDraft` is true, unless `statusId` / `statusKey` names one of
the organization's statuses. `publishImmediately: true` is the "Create & Publish"
button: the job is created as a `Draft` and published in the same call under the
rules of `POST /api/v1/jobs/{id}/finalize`; when it cannot be published yet it is
still created as a `Draft` and the response's `warning` says why (never a 4xx
for an unpublishable-but-created job). The write is attributed to the user who created the key.

## Request body

- CreateJobApiRequest: Body of `POST /api/v1/jobs` (API_AND_MCP.md, Phase 1b). Mirrors what the MVC Create page posts, minus the browser-only fields; totals are computed server-side from the lines instead of being trusted from the client.
  - `customerId` integer, required: Customer the job is for (must belong to the key's organization).
  - `customerAddressId` integer, nullable: Optional site address id; must belong to `customerId`.
  - `jobNumber` string, nullable: Optional job number. Omit to have one generated; a duplicate is suffixed to stay unique.
  - `jobType` string: Free-text job type, e.g. `Installation` (default), `Repair`, `Service`.
  - `description` string, nullable: Short description of the work, shown in lists. ≤ 500 characters. Example: `Replace water heater`.
  - `notes` string, nullable: Free-form internal notes (not shown to the customer). No length limit.
  - `isDraft` boolean: Save as a draft (status `Draft`) instead of an estimate. Ignored when `statusId` / `statusKey` is given.
  - `publishImmediately` boolean: Create and publish in one call — the "Create & Publish" button. The job starts as a `Draft` and is then published exactly as `POST /api/v1/jobs/{id}/finalize` would (needs at least one charge and a unit price on every material line), landing on `Estimated`, or `Scheduled` when `scheduledDate` is given. When it cannot be published yet the job is still created as a `Draft` (201) and `warning` on the response says why. Takes precedence over `isDraft`; 400 when `statusId` / `statusKey` names a non-Draft status. Default `false`.
  - `statusId` integer, nullable: Initial status definition id (one of the org's statuses). Mutually exclusive with `statusKey`.
  - `statusKey` string, nullable: Initial status by name — a status definition name (case-insensitive) or a system status key such as `Draft`, `Estimated`, `Scheduled`. Mutually exclusive with `statusId`.
  - `scheduledDate` string, date-time, nullable: Scheduled start as the organization's local wall clock, `yyyy-MM-ddTHH:mm:ss` with no UTC offset (the `date-time` format here is a local time, not RFC 3339 with zone); a trailing `Z` is converted to local time. Example: `2026-09-08T08:30:00`.
  - `estimateChecklistTemplateId` integer, nullable: Optional id of one of the organization's estimate pre-inspection checklist templates; its sections and items are copied onto the job.
  - `items` CreateJobItemApiRequest[]: Material lines (item templates with quantity and pricing). May be empty.
    - `itemTemplateId` integer: Item template id from `GET /api/v1/items`; must belong to the key's organization.
    - `quantity` integer: Whole units, ≥ 1. Default 1.
    - `unitCost` number, double: Cost per unit to the organization, in its currency, ≥ 0. Example: `12.50`.
    - `unitPrice` number, double: Price per unit charged to the customer, in the organization's currency, ≥ 0. Example: `19.99`.
    - `discountPercent` number, double: Line discount as a percentage, 0–100 (`10` = 10 % off `quantity` × `unitPrice`). Default 0.
    - `notes` string, nullable: Line note, ≤ 500 characters.
  - `laborItems` CreateJobLaborApiRequest[]: Labor lines (hours × rates). May be empty.
    - `description` string, required: What the labor is, ≤ 200 characters. Required. Example: `Install and commission unit`.
    - `taskTemplateId` integer, nullable: Optional id of the organization's task template this line derives from.
    - `laborRateId` integer, nullable: Optional id of the organization's labor rate the hourly figures were taken from (the figures themselves are still sent explicitly).
    - `estimatedHours` number, double: Estimated hours, decimal ≥ 0. Example: `2.5`.
    - `customerRate` number, double: Hourly rate billed to the customer.
    - `employeeCost` number, double: Hourly cost of the technician.
    - `overheadFactor` number, double: Multiplier applied to `employeeCost` for overhead (default 1).
    - `notes` string, nullable: Line note, ≤ 500 characters.
  - `customCharges` CreateJobCustomChargeApiRequest[]: Custom charges (fees, disposal, permits …) with a price and optional cost. May be empty.
    - `name` string, required: Charge name shown on the estimate, ≤ 200 characters. Required. Example: `Disposal fee`.
    - `description` string, nullable: Optional longer description, ≤ 500 characters.
    - `price` number, double: Amount charged to the customer, in the organization's currency, ≥ 0. Example: `75.00`.
    - `cost` number, double: Cost to the organization (for margin), in its currency, ≥ 0. Default 0.

## Response `201`

The created job (same shape as `GET /api/v1/jobs/{id}`, plus `warning` when a requested publish did not happen); `Location` points at it.

- JobDetailDto: Full job as returned by `GET /api/v1/jobs/{id}`: everything in `JobSummaryDto` plus the job site, pricing breakdown, assignees, material and labor lines and the note/update timeline.
  - `id` integer: Numeric id used in `/api/v1/jobs/{id}`. Unique within this organization. Example: `1042`.
  - `syncId` string, uuid: Stable cross-device identifier (GUID) — also the pagination cursor value. Example: `3f2504e0-4f89-11d3-9a0c-0305e82c3301`.
  - `jobNumber` string: Human-readable job number shown to staff and customers. Example: `J-2026-0042`.
  - `jobType` string, nullable: Free-text job category configured by the organization. Example: `Installation`, `Repair`.
  - `status` string: Workflow status name. One of `Draft`, `InspectionInProgress`, `Estimated`, `AwaitingOptionSelection`, `Scheduled`, `InProgress`, `Completed`, `Invoiced`, `Paid`, `Cancelled`, `Outstanding`.
  - `statusName` string: Display label of the status as the organization has customised it (falls back to `status`). Example: `On Site`.
  - `paymentStatus` string, nullable: Payment state, independent of workflow status: `Unpaid`, `Partial`, `Paid`, `Overpaid` or `Refunded`.
  - `customerId` integer: Id of the customer the job is for (see `/api/v1/customers/{id}`).
  - `customerName` string: Customer display name at read time. Example: `Acme Property Management`.
  - `description` string, nullable: Short description of the work. May be null for drafts.
  - `createdAt` string, date-time: When the job was created, ISO-8601 UTC. Example: `2026-09-01T14:03:22Z`.
  - `updatedAt` string, date-time: Last modification time, ISO-8601 UTC (creation time when never modified). Use with the `updatedSince` filter to poll for changes.
  - `scheduledDate` string, date-time, nullable: Scheduled start as the organization's local wall-clock time with no UTC offset (the `date-time` format here is a local time, not RFC 3339 with zone), e.g. `2026-09-08T08:30:00`. Null when unscheduled.
  - `scheduledDurationMinutes` integer, nullable: Planned duration in minutes (default 120 when scheduled without an explicit duration). Null when unscheduled.
  - `startDate` string, date-time, nullable: When work actually started, ISO-8601 UTC. Null until the job is in progress.
  - `completedDate` string, date-time, nullable: When work was completed, ISO-8601 UTC. Null until completed.
  - `totalPrice` number, double: Customer price before tax, after the job-level discount, in the organization's currency (decimal, e.g. `1250.00`).
  - `notes` string, nullable: Internal notes typed on the job itself (not the timeline). Null when empty.
  - `siteAddress` JobSiteAddressDto: A customer address used as the job site.
    - `id` integer: Customer address id (see `/api/v1/customers/{id}/locations`).
    - `name` string: Label the customer gave the address. Example: `Main Office`.
    - `address` string, nullable: Street line 1.
    - `address2` string, nullable: Street line 2 (suite, unit).
    - `city` string, nullable: City.
    - `state` string, nullable: State / province.
    - `postalCode` string, nullable: Postal / ZIP code.
    - `country` string, nullable: Country, when recorded.
  - `materialPrice` number, double: Sum of material lines before job-level discount and tax (decimal currency, e.g. `800.00`).
  - `laborPrice` number, double: Sum of labor lines before job-level discount and tax (decimal currency).
  - `customChargesTotal` number, double: Sum of custom charges before job-level discount and tax (decimal currency).
  - `discountPercent` number, double, nullable: Job-level discount as a percentage of every line (0–100), or null when a fixed amount / no discount applies.
  - `discountAmount` number, double, nullable: Job-level fixed discount amount (decimal currency), or null when a percentage / no discount applies.
  - `taxRate` number, double: Tax rate locked in at creation, as a percentage (e.g. `8.25`).
  - `downPaymentAmount` number, double, nullable: Money collected before invoicing (decimal currency), or null when none.
  - `assignees` JobAssigneeDto[]: Staff assigned to the job (requires the Scheduling module to change). Empty when unassigned.
    - `userId` string: Identity user id of the assignee (matches `/api/v1/users`).
    - `name` string: Display name (first + last, or email when no name is set). Example: `Maria Lopez`.
    - `scheduleGroupId` integer, nullable: Schedule group (crew) the assignment was made under, or null.
    - `scheduleGroupName` string, nullable: Display name of `scheduleGroupId`. Example: `Crew A`.
    - `notes` string, nullable: Note attached to the assignment, if any.
  - `items` JobMaterialLineDto[]: Material lines (parts/products) on the job, excluding removed lines.
    - `id` integer: Line id.
    - `syncId` string, uuid: Stable cross-device identifier of the line.
    - `itemTemplateId` integer: Item template (catalog product) id — see `/api/v1/items/{id}`.
    - `itemTemplateName` string: Catalog product name at read time. Example: `Torsion Spring 2in x 32in`.
    - `quantity` integer: Quantity ordered for the job (whole units).
    - `deliveredQuantity` integer: Quantity already delivered to / used at the site.
    - `unitPrice` number, double: Customer unit price before line discount (decimal currency).
    - `discountPercent` number, double: Line discount as a percentage (0–100).
    - `discountAmount` number, double: Line discount as a fixed amount (decimal currency).
    - `totalPrice` number, double: Extended customer price after line discount, before job discount and tax (decimal currency).
    - `status` string: Fulfilment state: `Planned`, `Reserved`, `Used` (delivered) or `Returned`.
    - `notes` string, nullable: Line note, or null when none. Example: `Bring the 8-ft ladder`.
  - `laborItems` JobLaborLineDto[]: Labor lines on the job, excluding removed lines.
    - `id` integer: Line id.
    - `syncId` string, uuid: Stable cross-device identifier of the line.
    - `description` string: What the work is. Example: `Replace torsion springs`.
    - `estimatedHours` number, double: Hours quoted (decimal, e.g. `1.5`).
    - `actualHours` number, double: Hours actually worked (decimal). Zero until logged.
    - `customerRate` number, double: Hourly rate billed to the customer (decimal currency).
    - `customerTotal` number, double: Customer price for the line after line discount, before job discount and tax (decimal currency).
    - `technicianId` string, nullable: Identity user id of the technician the line is booked to, or null.
    - `status` string: Line state name from the LaborItemStatus enum (e.g. `Planned`, `Scheduled`, `Completed`).
    - `notes` string, nullable: Line note, or null when none. Example: `Overtime rate approved by owner`.
  - `customCharges` JobCustomChargeDto[]: Custom charges (fees, disposal, permits …) on the job, excluding removed charges. Edit or remove one with its `id` via `PATCH`/`DELETE /api/v1/jobs/{id}/charges/{chargeId}`.
    - `id` integer: Charge id — pass as `chargeId` to `PATCH`/`DELETE /api/v1/jobs/{id}/charges/{chargeId}`. Example: `4210`.
    - `syncId` string, uuid: Stable cross-device identifier of the charge.
    - `name` string: Charge name shown on the job and invoice. Example: `Disposal fee`.
    - `description` string, nullable: Longer description of the charge, or null when none. Example: `Haul-away of the old unit`.
    - `price` number, double: Price charged to the customer before discounts, in the organization's currency (e.g. `75.00`).
    - `discountPercent` number, double: Line discount as a percentage (0–100), applied before the flat discount.
    - `discountAmount` number, double: Flat discount taken off the charge after the percentage discount, in the organization's currency.
    - `effectivePrice` number, double: Price after both line discounts, in the organization's currency — the amount that rolls into `customChargesTotal`. Example: `67.50`.
  - `updates` JobNoteDto[]: Timeline of notes, progress updates and status changes, oldest first.
    - `id` integer: Entry id.
    - `syncId` string, uuid: Stable cross-device identifier of the entry.
    - `type` string: Kind of entry — JobUpdateType name such as `GeneralNote`, `ProgressUpdate`, `Issue`, `CustomerRequest`, `StatusChange`, `Photo` or `Signature`.
    - `description` string: The note text.
    - `isInternal` boolean: True when the note is staff-only and must not be shown to the customer.
    - `createdAt` string, date-time: When the entry was written, ISO-8601 UTC.
    - `createdByName` string, nullable: Display name of the author, when recorded.
    - `oldStatus` string, nullable: For status-change entries: the previous status name; otherwise null.
    - `newStatus` string, nullable: For status-change entries: the new status name; otherwise null.
    - `attachmentName` string, nullable: For `Photo` / `Signature` entries: the attached image's file name (e.g. `J-1042-20260929-143015.jpg`); null for entries without an attachment. The image itself is viewable on the job's page in the web app and is included in the organization data export.
  - `warning` string, nullable: Set only on the `POST /api/v1/jobs` response when `publishImmediately` was requested but the job could not be published yet (it was created as a `Draft`), e.g. `Not published yet: Cannot finalize estimate. All material items must have a unit price.`. Always null on `GET`.

## Other responses

- `400`: Malformed body, missing `Idempotency-Key`, both `statusId` and `statusKey` given, or `publishImmediately` with a non-Draft status.
- `401`: Unauthorized
- `403`: Forbidden
- `409`: The key's creator is unknown on this server, or the service refused the create.
- `422`: Unknown customer, site address, status, item template, labor rate or task template for this organization (field listed in `errors`).
- `429`: Too Many Requests

---

[API](https://skmtc.dev/flexonthejob/apis/flex-api.md) · [All operations](https://skmtc.dev/flexonthejob/apis/flex-api/llms.txt) · [OpenAPI document](https://skmtc.dev/flexonthejob/apis/flex-api/revisions/19c8f4d27acf?raw)
