You created a provisioning job and got back a pjb_ job ID and an eng_ engine ID in milliseconds. The engine is usable already, but it is still filling in: an AI agent is crawling your sources and writing brand voices, glossary items, and instructions onto it. While that runs you want to show the work – a "Crawling your style guide… configuring the engine… done" line, the way an install wizard does, instead of a spinner that says nothing.
The WebSocket gives you exactly that feed. Connect to the job and the server pushes a snapshot of the current state, then a provisioning.progress event each time the workflow moves to a new step. And because the server sends the current state on connect and closes a finished job right after, you can connect any time, even after it finishes – there is no window you have to catch.
GET /jobs/provisioning/:jobId/wsThe jobId is the pjb_ value from the create call. New to async provisioning? Start with the Overview for the mental model.
On this page
- Message types
- Snapshot on connect
- Progress events
- Connecting after the job finishes
- Wiring it into your UI
- Keep your API key server-side
Message types#
Two message types travel over the socket. The first arrives once, on connect; the second arrives repeatedly, as the job advances.
| Type | When | Key fields |
|---|---|---|
provisioning.snapshot | On initial connection | jobId, status, errorMessage |
provisioning.progress | As each workflow step starts or completes | jobId, step, detail |
This is a liveness feed, not a results feed: it tells you where the job is and whether it failed, not which records the AI created. The summary of everything provisioned – the brand-voice, glossary, and instruction IDs – arrives separately, in the completion webhook or by reading the job once it is done. Keep the socket for the progress bar; reach for the webhook for the payload.
Snapshot on connect#
The instant you connect, the server reads the job's current state from the database and sends it. No progress event is required first – the snapshot stands on its own.
{
"type": "provisioning.snapshot",
"jobId": "pjb_A1b2C3d4E5f6G7h8",
"status": "in_progress",
"errorMessage": null
}| Field | Description |
|---|---|
status | in_progress, completed, or failed. |
errorMessage | The failure description when status is failed, otherwise null. |
The snapshot is the one message you are guaranteed to receive. If the job is still running you will get progress events after it; if the job has already finished you will get the snapshot and nothing more (see below).
Progress events#
As the workflow runs, the server broadcasts a provisioning.progress event each time it enters a new step. Each event names the step and carries a human-readable detail.
{
"type": "provisioning.progress",
"jobId": "pjb_A1b2C3d4E5f6G7h8",
"step": "crawling",
"detail": "Crawling source URLs..."
}step | When | Example detail |
|---|---|---|
crawling | Source URLs are being fetched | "Crawling source URLs..." or "Retrying crawl (attempt 2)..." |
configuring | The AI agent is analyzing content and writing engine config | "AI agent analyzing content and configuring engine..." or "Retrying configuration (attempt 2)..." |
completed | The job finished successfully | "Provisioning complete" |
failed | The job failed | An error message describing the failure |
A retry is not a failure
The crawling and configuring steps can fire more than once – a transient fetch or analysis error retries, and the retry surfaces as a progress event with a detail like "Retrying crawl (attempt 2)...". That is the job recovering, not the job failing. Treat only the failed step as terminal; its detail carries the actual reason.
Handle steps you do not recognize
New step values may be added over time. Switch on the steps you know, treat completed and failed as the two that close the socket, and ignore anything else as informational – a forward-compatible client keeps working without an update.
Connecting after the job finishes#
The hard question with any progress socket is what happens if you connect late – after the crawl is done, after a deploy reconnected the tab, after the job has already failed. Here the answer is built into how the snapshot works.
If the job has already reached completed or failed, the server sends the snapshot with that final status (and errorMessage, if it failed) and closes the connection immediately. There are no progress events to replay, because the final state is the snapshot. A job still in flight keeps the connection open and streams progress; a finished job hands you the outcome and hangs up.
Either way, the first message tells you where things stand. Connect any time, even after it finishes – you cannot connect too early and you cannot connect too late.
Wiring it into your UI#
Open the socket against the pjb_ job ID, read the snapshot to set your initial state, then update on each progress event and close when the job reaches completed or failed:
import WebSocket from "ws";
const jobId = "pjb_A1b2C3d4E5f6G7h8";
const ws = new WebSocket(
`wss://api.lingo.dev/jobs/provisioning/${jobId}/ws`,
{ headers: { "X-API-Key": process.env.LINGO_API_KEY } }
);
ws.on("message", (raw) => {
const event = JSON.parse(raw);
switch (event.type) {
case "provisioning.snapshot":
console.log(`status: ${event.status}`);
break;
case "provisioning.progress":
console.log(`${event.step}: ${event.detail}`);
if (event.step === "completed" || event.step === "failed") {
ws.close();
}
break;
}
});Run against a job that crawls cleanly and that prints the configuration happening, step by step:
status: in_progress
crawling: Crawling source URLs...
configuring: AI agent analyzing content and configuring engine...
completed: Provisioning completeThat is the whole arc on screen: the job opens in_progress, you watch it crawl and then configure, and completed tells you the engine is fully provisioned. The same loop is correct on a late connect – a finished job sends one snapshot with its final status and the socket closes, so the code that handles the live run handles the replay without a special case.
Keep your API key server-side#
The socket authenticates with your API key – the same organization-scoped key the REST endpoints use. That key reaches every engine in your organization, so the browser is the wrong place to open the connection: anyone who views source would see it.
Connect from your backend, not the browser
Open the WebSocket from your server, where the key already lives, then forward the progress to the browser over your own channel – a WebSocket or server-sent events stream you control. Your frontend shows the engine configuring; your key never leaves your infrastructure.
This mirrors the webhook model: the connection that touches Lingo.dev runs server-side, and what reaches the user is whatever your own app chooses to forward.
Where this fits#
The WebSocket is the live view – it is bound to one job and closes when that job is done. For a durable, server-to-server record of the result that survives a closed tab or a deploy, pair it with the completion webhook: the socket drives the progress bar while the job is on screen, the webhook delivers the summary of everything the AI created the moment it lands. Wire both from the same create call.
