Skip to content
Staging documentation. Requests go to https://api.stg.invoicing.proradius.co.

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.

  • The organization needs the Ticketing System add-on.
  • The key needs these abilities:
StepAbility
Read boards, job types, technicians and jobstickets:read
Create a jobtickets:create
Schedule or unschedule a visittickets:schedule
Start, update, move or cancel a jobtickets:update
Complete a jobtickets:complete
Complete a job that bills, or bill it laterinvoices: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.

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.

GET /v1/ticket-boards

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
}
]
}
POST /v1/tickets
Terminal window
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"
}'
FieldNotes
title, board_idRequired.
job_type_idOptional. 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, typeRequired without a job type. priority: low, medium, high, critical. type: task, bug, support, feature.
client_id or a lookup fieldSee Identify the client.
sla_hoursSee SLA hours.
external_refYour id for this job, unique per organization. Makes retries safe.
due_dateOptional date.
technician_id, scheduled_at, scheduled_untilOptional. 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.

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.

Send one of these fields:

FieldMatches
client_idThe client’s PROradius id (uuid).
client_external_refThe client’s own external_ref, set through the Clients API.
client_phoneThe client’s phone number. Spaces, dashes and the +961, 00961 or leading 0 prefixes are normalised on both sides.
client_isp_usernameThe 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.

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_hours applies (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.

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.

POST /v1/tickets/{id}/start

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.

Read the completion preview first if you want the job type’s default items with the client’s prices:

GET /v1/tickets/{id}/completion-preview

Then complete:

POST /v1/tickets/{id}/complete
Terminal window
curl -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" }
}'
FieldNotes
outcomeRequired. done, partial or revisit.
notesUp to 5000 characters.
reasonRequired for revisit. Optional reason_code: no_show, needs_parts, needs_time, customer_request, other.
itemsUp to 100 lines: item_uuid, quantity, optional price, serial, mac.
checklist_state[{"id": "...", "done": true}] for the job type’s checklist.
collect_paymentOptional payment on the new invoice: amount, optional currency, method (cash, whish, omt, bob, bank_transfer), exchange_rate.
notify_clientDefault true. Sends the “job completed” message when customer messages are on.
invoice_nowFor partial only. Default true; false keeps the items to bill later.
create_follow_upCreates 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.

The job type’s billing_mode decides what completion does with the items:

billing_modeOn completion
noneNo invoice. Use it for free work such as repairs under contract.
on_completionWhen 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.
manualThe 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.

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.

POST /v1/tickets/{id}/cancel
{ "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.

Instead of polling, subscribe an endpoint to the ticket.* topics:

TopicSent when
ticket.createdA job is created, from any channel.
ticket.scheduledA visit is booked, moved or cleared.
ticket.status_changedThe job moves to another column.
ticket.assignedAssignees change.
ticket.completedA completion with outcome done or partial succeeds.
ticket.cancelledThe job is cancelled.
ticket.archivedThe job is archived.

See Webhooks for signatures and retries.