Real-Time SSE Streaming Guide

Table of contents

  1. What is SSE Streaming?
  2. SSE Event Format
  3. SSE Events Reference
  4. Event Data Model
    1. Initialized Event
    2. Job Lifecycle Events
  5. Implementation Examples
    1. JavaScript
    2. Python
    3. Curl
  6. Error Handling
  7. Best Practices
  8. Alternative: Webhook Callbacks
  9. See Also

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"}

SSE Events Reference

The following events are sent 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

Event Data Model

Initialized Event

{
  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

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

Implementation Examples

JavaScript

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');

Python

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')

Curl

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.

Error Handling

Always implement error handling for SSE streams:

async function streamWithErrorHandling(prompt) {
  try {
    const response = await fetch(url, options);

    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(`API Error: ${errorData.error || response.statusText}`);
    }

    // Process SSE stream...
  } catch (error) {
    console.error('Stream error:', error);
    // Implement retry logic or fallback
  }
}

Best Practices

  1. Event Handling
    • Always check job.status field to determine event type
    • Handle all possible statuses: created, started, progress, completed, failed, moderated
  2. Progress Updates
    • Extract response.progress_percent for visual feedback (if provided - not always present)
    • Update UI in real-time as events arrive
  3. Media Extraction
    • Parse response.attachments for generated images/videos
    • Use response.imageUx and response.videoUx for upscaled media from https://cdn.midjourney.com
    • Access response.buttons for available actions
    • See GET /proxy/cdn-midjourney to retrieve imageUx/videoUx assets via useapi.net proxy
  4. Error Recovery
    • Catch JSON parse errors gracefully
    • 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:

{
  "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. See individual endpoint documentation for details.

See Also