Datacove API Developer guides
Realtime · Socket.IO

Realtime guide

Report workflows can run for minutes. Instead of polling, a client can open a Socket.IO connection and be told the moment a run finishes. This guide covers authenticating that connection, joining a run, the events you'll receive, and how to stay correct across reconnects.

Socket URLhttps://<api-host> · path /socket.io
LibrarySocket.IO v4 client
AuthSingle-use ticket
Ticket lifetime30 seconds

00How it works

The socket never sees your access token or API key. You trade them over HTTPS for a short-lived, single-use ticket, and present that ticket when you connect. Each run of a report workflow has its own room, named by the eventId that execute returned; you join it and wait for one event.

Step 1Mint a ticketPOST /api/realtime/v1/ticket
Step 2Connectio(origin, { auth: { ticket } })
Step 3Join with ackemit join_workflow
Step 4Wait for resulton workflow_status_update
Why a ticket

Web apps keep the real session token in a server-side, httpOnly cookie that browser JavaScript can't read. Minting a ticket on the server and handing only that to the browser means the long-lived token never reaches the page. Native apps, extensions and servers use the same flow, so there's one way to authenticate for every client.

01Mint a ticket

POST/realtime/v1/ticket Access tokenor API keyNo body

Any authenticated caller can mint one: signed-in users, MSP seats, and API-key integrations. The ticket carries the caller's identity into the socket. It is valid for expiresIn seconds and can be used for exactly one connection, so mint a new one for every connection attempt, including every reconnect.

Response · 201
{ "ticket": "eyJhbGciOiJIUzI1NiIs...", "expiresIn": 30 }

In a web app, mint the ticket in server-side code (a Server Action or API route) that already holds the user's session, and return only the ticket to the browser.

02Connect

Connect to the API host itself, not to /api. The Socket.IO endpoint is at the default path, /socket.io; if your deployment serves the API under a path prefix, prefix this path the same way (for example { path: "/prefix/socket.io" }).

JavaScript
import { io } from "socket.io-client";

const socket = io("https://api.example.com", {
  transports: ["websocket"],
  // A function, not a fixed object: it runs before every connection attempt,
  // so each reconnect presents a brand-new ticket.
  auth: (cb) => mintTicket().then((ticket) => cb({ ticket })),
});

Who can connect

The handshake is checked twice. First the request's origin is admitted, then the ticket is verified. A failure surfaces as connect_error, never as a connection that opens and then drops.

ClientOriginAdmitted
Native apps, servers, scriptsNone sentYes
Browser extensionschrome-extension://, moz-extension://, safari-web-extension://Yes
Outlook add-insMicrosoft-hosted Outlook originsYes
Web pagesThe page's own originOnly if that origin is registered on a tenant (its allowed origins or web app URL). Ask your administrator to add it.
connect_error messageMeaning
Missing realtime ticketNo auth.ticket was sent.
Invalid or expired realtime ticketThe ticket is malformed or older than 30 seconds. Mint a new one.
Realtime ticket already usedTickets are single-use. Mint a new one for each attempt.

03Join a run

EMITjoin_workflow (eventId) → ack Signed-in users and MSP seats

Send the eventId from execute and pass a callback to receive the acknowledgement. You can only join runs from your own tenant; any other id is answered with not_found.

The acknowledgement also reports the run's current status. If the run already finished before you joined, you'll learn that here instead of waiting for an event that has already been sent.

Ack · joined
{
  "status": "ok",
  "execution": {
    "status": "GENERATING",
    "error": false
  }
}
Ack · refused
{ "status": "error", "error": "not_found" }
FieldValues
statusok or error
execution.statusGENERATING: still running, wait for the event. SUCCESSFUL: already done, fetch the report now. FAILED: already failed.
execution.failureReasonPresent when the run failed.
errornot_found: no such run, or not in your tenant. wrong_channel: an API-key ticket used this event; use join_third_party_workflow. unauthorized: the connection has no identity.

04Receive the result

ONworkflow_status_update Sent once, when the run finishes
Finished
{ "error": false, "data": { "...": "..." } }
Failed
{ "error": true }

Treat the event as a signal, and load the report from GET /api/workflows/reports/v1/:eventId. That endpoint is the source of truth: it applies report passwords and returns the same shape your client already handles from polling. Disconnect once you have the result.

05Reconnects and fallback

  • Join again on every connect, not just the first. Room membership doesn't survive a reconnect, and mobile networks drop connections often. The ack's status tells you whether you missed the result while offline.
  • Mint a new ticket for every attempt. Passing auth as a function, as above, does this automatically.
  • Keep a polling fallback. If the socket can't connect, or no event arrives, poll the report every 5 seconds. Runs can take up to 20 minutes, so keep going at least that long before giving up.
  • One socket can watch several runs. Join each eventId on the same connection.

06Complete examples

Each example mints a ticket per connection, joins on every connect, handles the "already finished" case from the ack, and hands off to a loadReport function that calls the report endpoint.

JavaScript · socket.io-client v4
import { io } from "socket.io-client";

export function watchRun(eventId, { mintTicket, loadReport }) {
  const socket = io(SOCKET_ORIGIN, {
    transports: ["websocket"],
    auth: (cb) => mintTicket().then((ticket) => cb({ ticket })),
  });

  const finish = (outcome) => {
    socket.disconnect();
    return outcome === "failed" ? onFailed(eventId) : loadReport(eventId);
  };

  socket.on("connect", () => {
    socket.emit("join_workflow", eventId, (ack) => {
      if (ack.status === "error") return onJoinError(ack.error);   // not_found, wrong_channel
      if (ack.execution.status === "SUCCESSFUL") finish("done");    // finished before we joined
      if (ack.execution.status === "FAILED") finish("failed");
    });
  });

  socket.on("workflow_status_update", (payload) => finish(payload.error ? "failed" : "done"));
  socket.on("connect_error", (err) => console.warn("socket refused:", err.message));
  return socket;
}

07API-key integrations

Server-to-server integrations that authenticate with an API key use a separate channel. Execute with ?callAsync=true to queue a run and get an eventId, then mint a ticket with the same API key and connect as above.

Signed-in users and seatsAPI-key integrations
Ticket minted withAccess tokenx-api-key
Join eventjoin_workflowjoin_third_party_workflow
Result eventworkflow_status_updatethird_party_workflow_result
Result payload{ error, data }. Load the report via REST.{ error, data }. data is the workflow result.
Who can joinRuns from your tenantRuns started by your app

Using the other channel's join event is refused with wrong_channel. Acknowledgements and reconnect rules are identical on both channels.

08Adding a realtime feature

For backend developers. Workflow results are the first use of the realtime channel, not the only one. A new feature (notifications, live dashboards) should follow the same rules so clients authenticate one way for everything.

  1. Reuse the ticket. Don't add a new auth endpoint. POST /realtime/v1/ticket is feature-agnostic, and the gateway's handshake middleware verifies it and stores the caller's identity on the socket.
  2. Authorize every join. Rooms are keyed by a resource id, and a client may only join a room for a resource it owns. Check ownership with the same query the matching REST endpoint uses, so the two can never disagree.
  3. Acknowledge with current state. Every join returns an ack carrying the resource's current state, so a client that joins late or reconnects is immediately correct.
  4. Keep events as signals. Emit small payloads and let clients load the full resource through REST.
  5. Document it here. Add the new events, ack shape and payloads to this guide in the same change, and link the guide from the related Swagger operations.