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

Webhooks

Webhooks send an HTTPS POST to your server when something happens in PROradius. Use them instead of polling.

  1. Give your API key webhooks:manage, plus the read ability of every topic you want (see the event catalog).

  2. Create the endpoint:

    Terminal window
    curl -X POST https://api.stg.invoicing.proradius.co/v1/webhooks \
    -H "Authorization: Bearer $PRORADIUS_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
    "url": "https://hooks.example.com/proradius",
    "description": "CRM sync",
    "topics": [
    "ticket.created",
    "ticket.scheduled",
    "ticket.completed",
    "ticket.cancelled"
    ]
    }'
  3. Store the secret from the answer. It is shown only here and when you roll it with POST /v1/webhooks/{uuid}/roll-secret.

  4. Send a test event with POST /v1/webhooks/{uuid}/test. It delivers a webhook.ping through the same signed path as real events.

Endpoint rules:

  • The URL must use https on port 443 and resolve to a public address.
  • An organization can have up to 10 endpoints.
  • Optional branch_id limits the endpoint to events of one branch. Without it the endpoint receives every event of the organization.
  • PUT /v1/webhooks/{uuid} changes url, description, topics, active or branch_id.
  • A key sees and manages only the endpoints it created. Other endpoints answer 404.
  • A key can subscribe only to topics it can read. Any other topic answers 422.
  • GET /v1/webhooks/topics called with a key lists only the topics that key may use.
  • If the key is deleted, switched off, or loses the read ability for a topic, its endpoints stop receiving those events. They resume when the key is active and has the ability again.
TopicSent whenKey ability
ticket.createdA service job is created, from any channel.tickets:read
ticket.status_changedA job moves to another column. Reordering inside a column sends nothing.tickets:read
ticket.assignedThe assignees change.tickets:read
ticket.scheduledA visit is booked, moved, auto-assigned or cleared.tickets:read
ticket.completedA completion with outcome done or partial succeeds. Not sent for revisit or replays.tickets:read
ticket.cancelledA job is cancelled.tickets:read
ticket.archivedA job is archived by a person, a key, or the daily auto-archive.tickets:read
client.createdA client is created.clients:read
client.updatedA client is updated.clients:read
invoice.createdAn invoice is created.invoices:read
invoice.paidAn invoice is fully paid.invoices:read
invoice.voidedAn invoice is voided.invoices:read
payment.createdA payment is recorded.client_payments:read
payment.deletedA payment is deleted.client_payments:read
webhook.pingYou call the test endpoint. Not subscribable.-

Events are sent after the change is saved. A change that is rolled back sends nothing.

Every delivery has the same envelope:

{
"id": "evt_1f0b6a3c-9d4e-4a1b-8c22-5f7e0d9a1234",
"topic": "ticket.completed",
"created_at": "2026-10-20T08:40:02Z",
"api_version": "2026-09-01",
"organization_id": "7c2d1e8a-3b45-4f9c-a0d1-6e8b2f4a9c11",
"data": {
"ticket": {
"id": "9a7c3e1f-2b4d-4e6f-8a1c-3d5e7f9b1c2d",
"ticket_number": "TKT-0123",
12 collapsed lines
"external_ref": "crm-job-48213",
"status": "Done",
"status_category": "done",
"board_id": "4c6e8a0b-1d3f-4a5c-9e7b-2f4a6c8e0b1d",
"client_id": "b8f2a1c4-7d39-4e52-9a06-1f4c8b3d5e27",
"created_via": "api",
"job_type": "installation",
"technician_id": "5d1e2f3a-4b5c-4d6e-8f70-1a2b3c4d5e6f",
"scheduled_at": "2026-10-20T06:00:00Z",
"sla_due_at": "2026-10-21T08:00:00Z",
"archived_at": null,
"updated_at": "2026-10-20T08:40:00Z"
},
"outcome": "done",
"invoice_id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d",
"resolved_at": "2026-10-20T08:40:00Z"
}
}
FieldMeaning
idThe event id. The same across retries, resends and every endpoint that receives the event. Use it to skip duplicates.
topicThe topic.
created_atWhen the payload was built, UTC.
api_versionThe payload version stamped on the endpoint at creation.
organization_idThe organization’s id (uuid).
dataThe topic’s data.

Extra fields in data per ticket topic:

TopicFields next to ticket
ticket.status_changedfrom and to, each { "column", "category" }
ticket.assignedassigned_user_ids, added_user_ids, removed_user_ids
ticket.scheduledtechnician_id, scheduled_at, scheduled_until (null when cleared), auto, override
ticket.completedoutcome, invoice_id (null when not billed), resolved_at
ticket.cancelledreason, closed_at
ticket.archivedauto (true for the daily auto-archive)

Ticket payloads carry ids and the main fields only. Fetch GET /v1/tickets/{id} for the rest. Examples for every topic are in the API reference and in GET /v1/webhooks/topics.

HeaderValue
Content-Typeapplication/json
User-AgentPROradius-Webhooks/1.0
X-PROradius-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>
X-PROradius-TopicThe topic, for example ticket.completed.
X-PROradius-DeliveryThe delivery id. A resend gets a new one.
X-PROradius-EventThe event id without the evt_ prefix.

Each delivery is signed with your endpoint’s secret:

  1. Read t and v1 from the X-PROradius-Signature header.
  2. Build the string {t}.{raw request body}. Use the body bytes exactly as received; do not parse and re-encode the JSON.
  3. Compute HMAC-SHA256 of that string with the endpoint secret as the key, as lowercase hex.
  4. Compare it with v1 in constant time.
  5. Reject the request if t is more than 300 seconds away from your clock.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.post('/proradius', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-PROradius-Signature') || '';
const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=', 2)));
const t = Number(parts.t);
const expected = crypto
.createHmac('sha256', process.env.PRORADIUS_WEBHOOK_SECRET)
.update(`${parts.t}.${req.body.toString('utf8')}`)
.digest('hex');
const valid =
Number.isInteger(t) &&
Math.abs(Date.now() / 1000 - t) <= 300 &&
typeof parts.v1 === 'string' &&
parts.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
if (!valid) return res.status(400).send('invalid signature');
const event = JSON.parse(req.body.toString('utf8'));
// Queue the work, then answer quickly.
res.sendStatus(200);
});

Paste the raw body and the X-PROradius-Signature header exactly as your server received them, or sign a test body to call your own handler with.

Runs in your browser only. Your secret never leaves this page.

Answer with any 2xx status within 10 seconds to acknowledge a delivery. The connection itself must open within 5 seconds. Redirects are not followed.

Your answerWhat happens
2xxDelivered.
Timeout, connection or TLS error, 5xx, 429Retried.
Any other status (3xx, 4xx)Failed, not retried.
sequenceDiagram
    participant P as PROradius
    participant Y as Your endpoint
    P->>Y: POST event (attempt 1)
    Y-->>P: 503
    Note over P: wait 10 s
    P->>Y: POST same event (attempt 2)
    Y-->>P: timeout
    Note over P: wait 1 min
    P->>Y: POST same event (attempt 3)
    Y-->>P: 200 OK
    Note over P,Y: Delivered. The event id is the same on every attempt.

A delivery is tried up to 6 times. The waits between attempts are 10 seconds, 1 minute, 5 minutes, 30 minutes and 2 hours.

After 15 deliveries in a row fail for good, the endpoint is switched off and the organization is notified by email. A successful delivery resets the count. Switch the endpoint back on with PUT /v1/webhooks/{uuid} and {"active": true}.

Deliveries can arrive more than once and in any order. Skip events whose id you have already processed, and use updated_at or a fresh GET when order matters.

  • GET /v1/webhooks/{uuid}/deliveries lists deliveries with their status: pending, delivered, failed or dropped. Filter by status, topic, date_from and date_to.
  • GET /v1/webhooks/deliveries/{uuid} returns one delivery with its payload and your server’s response.
  • POST /v1/webhooks/deliveries/{uuid}/resend sends the stored payload again, with the same event id. The endpoint must be active.

Delivery history is kept for 30 days.