Service jobs
A service job (a ticket in the API) is a piece of field work for a client: an installation, a repair, a router swap. This guide follows one job from creation to completion.
POST /v1/ticketsnewPOST .../schedulescheduledPOST .../startin_progressPOST .../completedonePOST .../cancelcancelledcomplete, outcome revisitback to newBefore you begin
Section titled “Before you begin”- The organization needs the Ticketing System add-on.
- The key needs these abilities:
| Step | Ability |
|---|---|
| Read boards, job types, technicians and jobs | tickets:read |
| Create a job | tickets:create |
| Schedule or unschedule a visit | tickets:schedule |
| Start, update, move or cancel a job | tickets:update |
| Complete a job | tickets:complete |
| Complete a job that bills, or bill it later | invoices:create as well |
All times in responses are UTC with a Z suffix. A time you send without an offset is read in the organization’s
timezone, so prefer sending an offset or Z.
1. Pick a board and a job type
Section titled “1. Pick a board and a job type”Every job lives on a board. Each board has columns, and each column has a category: new, scheduled,
in_progress, waiting, done or cancelled. The API moves jobs between categories for you as they progress.
A job type is a template. It sets the default priority, the SLA, a checklist and the billing mode. Job types can belong to one board or to every board.
GET /v1/ticket-job-types?board_id={board_id}{ "data": [ { "id": "0c4e6a8b-2d1f-4e3a-9b5c-7d9e1f3a5b7c", "name": "Installation", "code": "installation", "board_id": null, "default_priority": "high", "sla_hours": 48, "duration_minutes": 120, "billing_mode": "on_completion", "requires_client": true, "auto_assign": false, "active": true } ]}2. Create the job
Section titled “2. Create the job”curl -X POST https://api.stg.invoicing.proradius.co/v1/tickets \ -H "Authorization: Bearer $PRORADIUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Fiber installation", "description": "Ground floor, entrance from the side street", "board_id": "4c6e8a0b-1d3f-4a5c-9e7b-2f4a6c8e0b1d", "job_type_id": "0c4e6a8b-2d1f-4e3a-9b5c-7d9e1f3a5b7c", "client_external_ref": "crm-1001", "sla_hours": 24, "external_ref": "crm-job-48213" }'| Field | Notes |
|---|---|
title, board_id | Required. |
job_type_id | Optional. With a job type, priority and type become optional; the job type fills the priority, SLA and checklist. A job type that requires a client answers 422 without one. |
priority, type | Required without a job type. priority: low, medium, high, critical. type: task, bug, support, feature. |
client_id or a lookup field | See Identify the client. |
sla_hours | See SLA hours. |
external_ref | Your id for this job, unique per organization. Makes retries safe. |
due_date | Optional date. |
technician_id, scheduled_at, scheduled_until | Optional. Books the visit at creation; needs tickets:schedule. |
A new job answers 201 with the full job. The server sets created_via to api and api_key_id to your key.
Safe retries with external_ref
Section titled “Safe retries with external_ref”Sending the same external_ref again returns the existing job with 200 and changes nothing. If the reference
belongs to a deleted job, or to a job on a board the key cannot open, the answer is 409. Find a job by your
reference later with GET /v1/tickets?external_ref=crm-job-48213.
Identify the client
Section titled “Identify the client”Send one of these fields:
| Field | Matches |
|---|---|
client_id | The client’s PROradius id (uuid). |
client_external_ref | The client’s own external_ref, set through the Clients API. |
client_phone | The client’s phone number. Spaces, dashes and the +961, 00961 or leading 0 prefixes are normalised on both sides. |
client_isp_username | The username of the client’s ISP account. |
The lookup is limited to your organization. If nothing matches, the create answers 422 with reason
client_not_found. If several clients match, it answers 422 with reason client_ambiguous and up to ten
candidate ids; retry with client_id. Sending two lookup fields answers client_lookup_conflict. See
client lookup errors.
The same fields filter GET /v1/tickets. There, no match returns an empty list instead of an error.
SLA hours
Section titled “SLA hours”The SLA is the number of hours, counted from creation, within which the job should be done. The job’s
sla_due_at is the deadline.
- Not sent: the job type’s
sla_hoursapplies (no SLA without a job type). - An integer from 1 to 8760: this job’s own SLA.
null: no SLA for this job.
Time spent in a waiting column pauses the SLA and moves sla_due_at later by the paused time. Change the SLA of an
open job with PUT /v1/tickets/{id} and {"sla_hours": 12}; send null to remove it. A done or cancelled job
answers 422 with “Reopen the job to change its SLA.”. Each job returns its current sla_hours, sla_due_at and
sla_breached_at.
3. Schedule a visit
Section titled “3. Schedule a visit”Find who can take the job, then book a window.
GET /v1/ticket-technicians?board_id={board_id}Optionally ask for ranked suggestions for a window:
GET /v1/tickets/{id}/technician-suggestions?scheduled_at=…&scheduled_until=…Then book:
POST /v1/tickets/{id}/schedule{ "technician_id": "5d1e2f3a-4b5c-4d6e-8f70-1a2b3c4d5e6f", "scheduled_at": "2026-10-20T09:00:00+03:00", "scheduled_until": "2026-10-20T11:00:00+03:00"}The API answers 422 and books nothing when:
- the window is longer than 24 hours, or starts more than 5 minutes in the past,
- the technician already has an open job overlapping the window (touching windows such as 10:00 to 12:00 and 12:00 to 13:00 are fine),
- the window falls outside the technician’s working hours, on their time off, or they lack a skill the job type requires,
- the job is done, cancelled or archived.
Booking over an overlap or an availability conflict needs "override": true, an override_reason, and the
tickets:dispatch ability. override never allows a visit in the past.
A job in a new column moves to the board’s first scheduled column, and the technician is added to the
assignees. POST /v1/tickets/{id}/unschedule clears the technician and window.
4. Start the job
Section titled “4. Start the job”Sets started_at the first time (repeating it changes nothing) and moves the job to the first in_progress column
when the board has one.
5. Complete the job
Section titled “5. Complete the job”Read the completion preview first if you want the job type’s default items with the client’s prices:
GET /v1/tickets/{id}/completion-previewThen complete:
POST /v1/tickets/{id}/completecurl -X POST https://api.stg.invoicing.proradius.co/v1/tickets/9a7c3e1f-2b4d-4e6f-8a1c-3d5e7f9b1c2d/complete \ -H "Authorization: Bearer $PRORADIUS_API_KEY" \ -H "Idempotency-Key: 6f1d2c3b-7a8e-4f90-a1b2-c3d4e5f60718" \ -H "Content-Type: application/json" \ -d '{ "outcome": "done", "notes": "Installed ONT, speed test 48/20 Mbps", "items": [ { "item_uuid": "3f1c2b4a-5d6e-4f70-8a9b-0c1d2e3f4a5b", "quantity": 1, "serial": "ZTEG12345678" } ], "collect_payment": { "amount": 50, "currency": "USD", "method": "cash" } }'| Field | Notes |
|---|---|
outcome | Required. done, partial or revisit. |
notes | Up to 5000 characters. |
reason | Required for revisit. Optional reason_code: no_show, needs_parts, needs_time, customer_request, other. |
items | Up to 100 lines: item_uuid, quantity, optional price, serial, mac. |
checklist_state | [{"id": "...", "done": true}] for the job type’s checklist. |
collect_payment | Optional payment on the new invoice: amount, optional currency, method (cash, whish, omt, bob, bank_transfer), exchange_rate. |
notify_client | Default true. Sends the “job completed” message when customer messages are on. |
invoice_now | For partial only. Default true; false keeps the items to bill later. |
create_follow_up | Creates a linked follow-up job. Needs tickets:create. |
Everything happens in one transaction. If the invoice or payment fails, the answer is 422 with the reason and
nothing changes.
Billing modes
Section titled “Billing modes”The job type’s billing_mode decides what completion does with the items:
billing_mode | On completion |
|---|---|
none | No invoice. Use it for free work such as repairs under contract. |
on_completion | When items are sent, an invoice is created for the client and linked to the job. An optional payment is recorded on it. Needs invoices:create. |
manual | The items are saved and no invoice is created. Bill later with POST /v1/tickets/{id}/create-invoice. |
revisit never bills. It sends the job back to the queue: the technician and window are cleared and the job returns
to the first new column.
The answer
Section titled “The answer”complete, create-invoice and cancel return the job plus a completion object:
{ "id": "9a7c3e1f-2b4d-4e6f-8a1c-3d5e7f9b1c2d", "ticket_number": "TKT-0123", "status": "Done", "status_category": "done", "resolved_at": "2026-10-20T08:40:00Z", "completion": { "replayed": false, "billing_mode": "on_completion", "invoice": { "id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d", "number": "INV-001204", "total": 50, "balance": 0, "currency": "USD", "status": "paid" }, "payment_id": "a41d6f92-8c07-4b3e-95a2-0d7e6c1b8f43", "follow_up": null, "client_notified": true }}Completing a job that is already finished answers 200 with completion.replayed: true and the same invoice. Send
an Idempotency-Key as well to make network retries exact. See Retries and idempotency.
Cancel instead
Section titled “Cancel instead”{ "reason": "Client moved to another address" }The job moves to the first cancelled column with closed_at and cancellation_reason. Nothing is invoiced.
Cancelling twice answers 200 with no change; a done job answers 422.
Follow along with webhooks
Section titled “Follow along with webhooks”Instead of polling, subscribe an endpoint to the ticket.* topics:
| Topic | Sent when |
|---|---|
ticket.created | A job is created, from any channel. |
ticket.scheduled | A visit is booked, moved or cleared. |
ticket.status_changed | The job moves to another column. |
ticket.assigned | Assignees change. |
ticket.completed | A completion with outcome done or partial succeeds. |
ticket.cancelled | The job is cancelled. |
ticket.archived | The job is archived. |
See Webhooks for signatures and retries.
Thank you for the feedback.

