Real-Time SSE Streaming Guide

Table of contents

  1. What is SSE Streaming?
  2. SSE Event Format
    1. Initialized Event • event : initialized
    2. Job Lifecycle Events • event : midjourney_*
  3. Examples
  4. Best Practices
  5. 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

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.