Real-Time SSE Streaming Guide

October 27, 2025 (November 26, 2025)

Table of contents

  1. What is SSE Streaming?
    1. Benefits
    2. Connection Stability Requirements
  2. SSE Event Format
    1. Initialized Event • event : initialized
    2. Job Lifecycle Events • event : midjourney_*
    3. Examples
  3. Best Practices
  4. Alternative: Webhook Callbacks

This guide explains how to implement real-time Server-Sent Events (SSE) streaming for Midjourney v3 API.

What is SSE Streaming?

SSE streaming provides real-time job updates as events occur. When you set stream: true, the API returns a persistent connection that sends job progress events as they happen.

Benefits

  • Instant progress updates (no polling required)
  • Real-time progress percentages
  • Live status changes (created → started → progress → completed/failed/moderated)
  • Lower latency than polling

Connection Stability Requirements

You must keep the SSE connection open until the job completes. If you disconnect prematurely (before receiving completed, failed, or moderated status):

  • Job becomes stuck in progress status
  • Consumes one of your available job slots
  • Prevents new jobs from starting if you’re at your concurrent limit
  • Remains stuck for 14 minutes until automatic timeout

To fix stuck jobs call DELETE /jobs/jobId to free the job slot right away. Without manual cleanup, you must wait 14 minutes for automatic timeout.

Common causes: Closing browser/Postman, network interruptions, application crashes.

Alternatives if connection stability is a concern:

  • Use webhooks: stream: false + replyUrl for fire-and-forget workflows
  • Use polling: stream: false + GET /jobs/{jobId} for better connection control

SSE Event Format

SSE responses use the text/event-stream content type. Each line starts with data: followed by a JSON object:

data: {"event":"initialized","message":"Stream initialized","jobId":"j1024...","seq":0,"ts":"22:41:58.458"}

data: {"event":"midjourney_created","job":{"jobid":"j1024...","verb":"imagine","status":"created",...},"seq":1,"ts":"22:41:59.123"}

data: {"event":"midjourney_progress","job":{"jobid":"j1024...","status":"progress","response":{"progress_percent":15},...},"seq":5,"ts":"22:42:10.456"}

data: {"event":"midjourney_completed","job":{"jobid":"j1024...","status":"completed","response":{...},...},"seq":8,"ts":"22:42:25.789"}

The following events are sent in event field during job execution:

Event Description When Sent
initialized Stream initialized First event when connection opens
midjourney_created Job created and queued Immediately after job creation
midjourney_started Job processing started When Midjourney begins processing
midjourney_progress Progress update During job execution (includes progress_percent)
midjourney_completed Job completed successfully When job finishes with results
midjourney_failed Job failed On error or timeout
midjourney_moderated Content moderation When prompt is flagged by Midjourney
error General error On unexpected errors

Initialized Event • event : initialized

{
  event: "initialized"
  message: string            // "Stream initialized"
  jobId: string              // Job ID
  seq: number                // Sequence number (starts at 0)
  ts: string                 // Timestamp (HH:MM:SS.mmm)
}

Job Lifecycle Events • event : midjourney_*

All other events contain the full job object. See Job Response Model for complete response structure.

Examples

  • curl -N -H "Authorization: Bearer YOUR_API_TOKEN" \
         -H "Content-Type: application/json" \
         -X POST "https://api.useapi.net/v3/midjourney/jobs/imagine" \
         -d '{"prompt":"a cat in a hat","stream":true}'
    

    Note: The -N flag disables buffering for real-time streaming.

  • async function streamMidjourneyJob(prompt) {
      const response = await fetch('https://api.useapi.net/v3/midjourney/jobs/imagine', {
        method: 'POST',
        headers: {
          'Authorization': 'Bearer YOUR_API_TOKEN',
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          prompt: prompt,
          stream: true
        })
      });
    
      const reader = response.body.getReader();
      const decoder = new TextDecoder('utf-8');
      let buffer = '';
    
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
    
        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split('\n');
        buffer = lines.pop(); // Keep incomplete line in buffer
    
        for (const line of lines) {
          if (line.startsWith('data:')) {
            const data = line.slice(5).trim();
            try {
              const eventData = JSON.parse(data);
              console.log('Event:', eventData.event);
    
              // Handle initialized event
              if (eventData.event === 'initialized') {
                console.log('Stream initialized for job:', eventData.jobId);
                continue;
              }
    
              // Handle job lifecycle events
              const job = eventData.job;
              if (!job) continue;
    
              console.log('Job status:', job.status);
    
              if (job.status === 'progress') {
                console.log(`Progress: ${job.response?.progress_percent}%`);
              } else if (job.status === 'completed') {
                console.log('Job completed!', job.response);
                // Extract media URLs
                if (job.response?.attachments) {
                  job.response.attachments.forEach(att => {
                    console.log('Attachment:', att.url);
                  });
                }
                if (job.response?.imageUx) {
                  job.response.imageUx.forEach(img => {
                    console.log(`Image ${img.id}:`, img.url);
                  });
                }
                if (job.response?.videoUx) {
                  job.response.videoUx.forEach(vid => {
                    console.log(`Video ${vid.id}:`, vid.url);
                  });
                }
              } else if (job.status === 'failed') {
                console.error('Job failed:', job.error);
              } else if (job.status === 'moderated') {
                console.error('Job moderated:', job.error);
              }
            } catch (e) {
              console.error('Failed to parse event data:', e);
            }
          }
        }
      }
    }
    
    // Usage
    streamMidjourneyJob('a cat in a hat');
    
  • import requests
    import json
    
    def stream_midjourney_job(prompt):
        url = 'https://api.useapi.net/v3/midjourney/jobs/imagine'
        headers = {
            'Authorization': 'Bearer YOUR_API_TOKEN',
            'Content-Type': 'application/json'
        }
        payload = {
            'prompt': prompt,
            'stream': True
        }
    
        response = requests.post(url, headers=headers, json=payload, stream=True)
    
        for line in response.iter_lines():
            if not line:
                continue
    
            line_str = line.decode('utf-8')
    
            if line_str.startswith('data:'):
                data_str = line_str[5:].strip()
                try:
                    event_data = json.loads(data_str)
                    print(f"Event: {event_data.get('event')}")
    
                    # Handle initialized event
                    if event_data.get('event') == 'initialized':
                        print(f"Stream initialized for job: {event_data.get('jobId')}")
                        continue
    
                    # Handle job lifecycle events
                    job = event_data.get('job')
                    if not job:
                        continue
    
                    print(f"Job status: {job.get('status')}")
    
                    if job.get('status') == 'progress':
                        progress = job.get('response', {}).get('progress_percent', 0)
                        print(f'Progress: {progress}%')
                    elif job.get('status') == 'completed':
                        print('Job completed!', job.get('response'))
                        # Extract media URLs
                        attachments = job.get('response', {}).get('attachments', [])
                        for att in attachments:
                            print(f"Attachment: {att['url']}")
                        image_ux = job.get('response', {}).get('imageUx', [])
                        for img in image_ux:
                            print(f"Image {img['id']}: {img['url']}")
                        video_ux = job.get('response', {}).get('videoUx', [])
                        for vid in video_ux:
                            print(f"Video {vid['id']}: {vid['url']}")
                    elif job.get('status') == 'failed':
                        print(f"Job failed: {job.get('error')}")
                    elif job.get('status') == 'moderated':
                        print(f"Job moderated: {job.get('error')}")
                except json.JSONDecodeError as e:
                    print(f'Failed to parse event data: {e}')
    
    # Usage
    stream_midjourney_job('a cat in a hat')
    

Best Practices

  • Event Handling
    • Always check job.status field to determine event type
    • Handle all possible statuses: created, started, progress, completed, failed, moderated
  • Progress Updates
    • Extract job.response.progress_percent for visual feedback (if provided - not always present)
    • Update UI in real-time as events arrive
  • Media Extraction
    • Access job.response.buttons for available actions
    • Parse job.response.attachments for generated images/videos
    • Use job.response.imageUx and job.response.videoUx for upscaled media from https://cdn.midjourney.com. See GET /proxy/cdn-midjourney to retrieve imageUx/videoUx assets via useapi.net proxy
  • Error Handling
    • Always implement proper error handling for SSE streams
    • Check response status before processing stream
    • Catch JSON parse errors gracefully
    • Implement retry logic or fall back to polling (GET /jobs/jobid) if SSE fails

Alternative: Webhook Callbacks

If you prefer server-to-server notifications instead of client SSE streams, use the replyUrl parameter whit stream: false:

{
  "prompt": "a cat in a hat",
  "stream": false,
  "replyUrl": "https://your-server.com/webhook"
}

All job events will be POSTed to your webhook URL in real-time using content: application/json format, see Job Response Model for complete job events structure.