← Back to guidesGUIDE · WEBHOOKS

Migrate from polling to ticket webhooks

Webhooks10 minIntermediate

If your integration polls GET /api/v1/tickets/{id} on a schedule to detect changes, switching to webhooks gives you faster reactions, fewer requests, and less chance of being throttled. This guide walks through the cutover.

Why move off polling

Polling has three operational costs: latency between the change and your reaction, request volume that pressures your API rate limit, and brittleness when the AlgaPSA side returns 503s during peak load. A webhook subscription replaces all three with a single signed POST per event.

The before pattern

A typical polling loop looks like this:

poller.ts (before)
// runs every minute via cron
for (const ticketId of WATCHED_TICKET_IDS) {
  const ticket = await fetch(
    `https://algapsa.com/api/v1/tickets/${ticketId}`,
    { headers: { "X-API-Key": process.env.ALGA_API_KEY! } },
  ).then((r) => r.json());

  if (ticket.data.updated_at > lastSeen[ticketId]) {
    react(ticket.data);
    lastSeen[ticketId] = ticket.data.updated_at;
  }
}

Six tickets at one-minute granularity is 360 requests per hour per integration — and each one is an independent risk for a 5xx response or a rate-limit hit.

The after pattern

Replace the loop with a single webhook subscription and a stateless receiver. The receiver verifies the signature, deduplicates on X-Alga-Event-Id, and reacts.

receiver.ts (after)
app.post(
  "/webhooks/alga",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sig = req.header("X-Alga-Signature") ?? "";
    if (!verify(req.body.toString("utf8"), sig, secret)) {
      return res.status(401).send("bad signature");
    }
    const eventId = req.header("X-Alga-Event-Id")!;
    if (await alreadyProcessed(eventId)) return res.status(200).send("ok");

    const envelope = JSON.parse(req.body.toString("utf8"));
    if (WATCHED_TICKET_IDS.has(envelope.data.ticket_id)) {
      await react(envelope.data);
      await markProcessed(eventId);
    }
    res.status(200).send("ok");
  },
);

Field mapping

The webhook envelope's data object is a curated subset of the ticket record. Most fields you would have read from GET /tickets/{id} are already there, with the same names. The fields you should expect:

data.ticket_iduuid
Same as the polling response.
data.ticket_numberstring
Display number such as TKT-1042.
data.title / data.status_name / data.priority_namestring
Surface fields the polling response also had.
data.assigned_to / data.assigned_to_namestring
Updated assignee on assignment events.
data.previous_status_id / previous_status_namestring
Only on ticket.status_changed.
data.tagsstring[]
Replaces the separate tags lookup you may have been doing.
data.urlstring
Deep link to the ticket in the AlgaPSA UI.

Need a field that is not in the curated set? Hit GET /api/v1/tickets/{id} on receipt — you will still make far fewer requests than continuous polling, and only when something actually changed.

Cutover steps

  1. Stand up the webhook receiver alongside the existing polling job. The receiver should be idempotent on X-Alga-Event-Id so duplicate events are safe.
  2. Subscribe to the ticket events you actually need. For most polling integrations, ticket.created, ticket.updated, ticket.status_changed, and ticket.assigned cover the use case.
  3. Run both pipelines in parallel for a day. Compare the events your receiver processed against what the poller would have caught. Account for events on tickets your poller did not watch — webhooks deliver every event for every ticket your tenant has access to, and you filter on receive.
  4. Once you trust the webhook path, decommission the cron job. Keep the receiver as the single source of truth.

What to watch

  • Receiver downtime. Webhooks retry on a 1 min → 5 min → 30 min → 2 h → 12 h schedule for up to five attempts. After that, deliveries are abandoned. A receiver outage longer than ~14 hours will lose events — budget for one-off backfill from the API if downtime stretches.
  • Auto-disable. If your receiver returns only failures for 24 hours, AlgaPSA disables the webhook and emails the user who created it. Re-enable with PUT /api/v1/webhooks/{id}.
  • Out-of-order arrivals. Two events for the same ticket can arrive in any order, especially when retries are involved. Use occurred_at in the envelope to sequence on your side if order matters.