Real-Time SSE Streaming Guide
Table of contents
- What is SSE Streaming?
- SSE Event Format
- SSE Events Reference
- Event Data Model
- Implementation Examples
- Error Handling
- Best Practices
- Alternative: Webhook Callbacks
- 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
- 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
response.progress_percentfor visual feedback (if provided - not always present) - Update UI in real-time as events arrive
- Extract
- Media Extraction
- Parse
response.attachmentsfor generated images/videos - Use
response.imageUxandresponse.videoUxfor upscaled media fromhttps://cdn.midjourney.com - Access
response.buttonsfor available actions - See GET /proxy/cdn-midjourney to retrieve imageUx/videoUx assets via useapi.net proxy
- Parse
- 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
- POST /jobs/imagine - Generate images with SSE
- POST /jobs/button - Execute buttons with SSE
- GET /jobs/
jobid- Classic polling (fallback)