# Job status

> Subscribe to live progress events for a denoising job over Server-Sent Events. The connection emits processing events as work advances and terminates with either a done event carrying the final audio URL, or an error event.

## The request

Stream denoise progressGET/denoise/{denoiseId}/progress

Authenticate with your API key in the `x-api-key` header, and ask for an event stream via the `Accept` header.

| Path parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `denoiseId` | string (UUID) | **Yes** | The `denoiseId` returned by [`POST /denoise`](/audio/voice-isolation). |

| Header | Value | Description |
| --- | --- | --- |
| `x-api-key` | your API key | Required for authentication. |
| `Accept` | `text/event-stream` | Required to negotiate the SSE response. |

## The event stream

On success the server answers **200 OK** with `Content-Type: text/event-stream`, `Cache-Control: no-cache` and `Connection: keep-alive`. Each message is delivered as a `data:` line whose payload is a JSON object. The connection automatically closes after a terminal event (`done` or `error`), or after **15 minutes** of inactivity. The `stage` field discriminates the event:

| Stage | Fields | Description |
| --- | --- | --- |
| `processing` | `pct` (number, 0–100), `message` (string, optional) | Incremental progress update. |
| `done` | `url` (string), `denoiseId` (string) | Terminal success — `url` is the denoised audio URL. |
| `error` | `message` (string) | Terminal failure — human-readable error message. |

Example streamwhat the wire looks like

```
data: {"stage":"processing","pct":15,"message":"Extracting audio"}

data: {"stage":"processing","pct":62,"message":"Running noise reduction"}

data: {"stage":"done","url":"https://cdn.munsit.com/denoising/user_123/5b1d2f7e_denoised.wav","denoiseId":"5b1d2f7e-3f81-4c2a-9c5d-7a2e91b4c7a1"}
```

> Reconnecting. When you connect to an already-completed job, the server immediately emits the cached final event and closes — clients never miss completion and there is no need to poll separately.

## Example usage

Use any HTTP client that can read a streamed body — no special SSE library required.

```bash
curl -N "https://api.munsit.com/api/v1/denoise/5b1d2f7e-3f81-4c2a-9c5d-7a2e91b4c7a1/progress" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: text/event-stream"
```

```
import httpx, json

denoise_id = "5b1d2f7e-3f81-4c2a-9c5d-7a2e91b4c7a1"
url = f"https://api.munsit.com/api/v1/denoise/{denoise_id}/progress"
headers = {
    "x-api-key": "YOUR_API_KEY",
    "Accept": "text/event-stream",
}

with httpx.stream("GET", url, headers=headers, timeout=None) as r:
    for line in r.iter_lines():
        if not line or not line.startswith("data: "):
            continue
        event = json.loads(line[6:])
        if event["stage"] == "processing":
            print(f"Progress: {event['pct']}% — {event.get('message','')}")
        elif event["stage"] == "done":
            print("Denoised audio URL:", event["url"])
            break
        elif event["stage"] == "error":
            print("Failed:", event["message"])
            break
```

```javascript
const denoiseId = "5b1d2f7e-3f81-4c2a-9c5d-7a2e91b4c7a1";

const res = await fetch(`https://api.munsit.com/api/v1/denoise/${denoiseId}/progress`, {
  headers: {
    'x-api-key': 'YOUR_API_KEY',
    'Accept': 'text/event-stream',
  },
});

const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";

while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
  const lines = buffer.split("\n");
  buffer = lines.pop() ?? "";

  for (const line of lines) {
    if (!line.startsWith("data: ")) continue;
    const event = JSON.parse(line.slice(6));
    if (event.stage === "processing") {
      console.log(`Progress: ${event.pct}%`, event.message);
    } else if (event.stage === "done") {
      console.log("Denoised audio ready:", event.url);
      break;
    } else if (event.stage === "error") {
      console.error("Denoising failed:", event.message);
      break;
    }
  }
}
```

```go
package main

import (
    "bufio"
    "encoding/json"
    "fmt"
    "net/http"
    "os"
    "strings"
)

func main() {
    denoiseID := "5b1d2f7e-3f81-4c2a-9c5d-7a2e91b4c7a1"
    url := fmt.Sprintf("https://api.munsit.com/api/v1/denoise/%s/progress", denoiseID)

    req, _ := http.NewRequest("GET", url, nil)
    req.Header.Set("x-api-key", os.Getenv("MUNSIT_API_KEY"))
    req.Header.Set("Accept", "text/event-stream")

    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()

    scanner := bufio.NewScanner(resp.Body)
    for scanner.Scan() {
        line := scanner.Text()
        if !strings.HasPrefix(line, "data: ") {
            continue
        }
        var event map[string]any
        json.Unmarshal([]byte(line[6:]), &event)
        switch event["stage"] {
        case "processing":
            fmt.Printf("Progress: %v%% — %v\n", event["pct"], event["message"])
        case "done":
            fmt.Println("Denoised audio URL:", event["url"])
            return
        case "error":
            fmt.Println("Failed:", event["message"])
            return
        }
    }
}
```

Output

```
Progress: 15% Extracting audio
Progress: 62% Running noise reduction
Denoised audio ready: https://cdn.munsit.com/denoising/user_123/5b1d2f7e_denoised.wav
```

## Errors

Before the stream is established, failures come back as plain JSON — not as SSE.

| Status | Body |
| --- | --- |
| **404** Not Found | `{"message": "Denoising job not found"}` |
| **401** Unauthorized | `{"errorCode": "40101", "errorMessage": "Invalid or missing API key"}` |

## Next steps

Once the `done` event delivers the URL, the clean audio is yours.

- [Voice isolation](/audio/voice-isolation) — Submit jobs and list past results with their final URLs. — `POST /denoise · GET /denoise`

- [Transcribe](/speech-to-text/transcribe) — Feed the denoised file straight into speech-to-text. — `POST /speech-to-text`
