Real-Time SSE Streaming Guide
Table of contents
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
-Nflag 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.statusfield to determine event type - Handle all possible statuses: created, started, progress, completed, failed, moderated
- Always check
- Progress Updates
- Extract
job.response.progress_percentfor visual feedback (if provided - not always present) - Update UI in real-time as events arrive
- Extract
- Media Extraction
- Access
job.response.buttonsfor available actions - Parse
job.response.attachmentsfor generated images/videos - Use
job.response.imageUxandjob.response.videoUxfor upscaled media fromhttps://cdn.midjourney.com. See GET /proxy/cdn-midjourney to retrieveimageUx/videoUxassets via useapi.net proxy
- Access
- 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.