Skip to main content

Event Structure

All webhook events follow a consistent structure with two main fields:
{
  "type": "event.name",
  "payload": {
    // Event-specific data matching API response format
  }
}

Event Naming Convention

Events use dot notation: {resource}.{action}
  • Resource: video, image, audio
  • Action: started, completed, errored

Payload Format

The payload field contains the same data structure as the corresponding API endpoint:
Consistent Data: Webhook payloads use the exact same format as API responses, making it easy to reuse your existing data handling code.

Video Events

Video events are triggered during the lifecycle of video processing jobs. All payloads match the GET /v1/video-projects/:id response format.

video.started

When triggered: Video processing begins (job moves from pending to rendering) Use cases:
  • Update UI to show “processing” status
  • Log job start time for analytics
  • Send user notification that processing began
Key payload fields:
{
  "type": "video.started",
  "payload": {
    "id": "clx7uu86w0a5qp55yxz315r6r",
    "status": "rendering",
    "type": "ANIMATION",
    "created_at": "2024-10-19T05:10:19.027Z",
    "width": 512,
    "height": 960,
    "total_frame_cost": 450,
    "credits_charged": 450,
    "downloads": [],
    "error": null
  }
}
Processing Time: Video processing can take 30 seconds to several minutes depending on length and complexity.

video.completed

When triggered: Video processing finishes successfully Use cases:
  • Download the completed video
  • Update database with completion status
  • Send user notification with download link
  • Trigger downstream workflows
Key payload fields:
{
  "type": "video.completed",
  "payload": {
    "id": "clx7uu86w0a5qp55yxz315r6r",
    "status": "complete",
    "type": "ANIMATION",
    "downloads": [
      {
        "url": "https://videos.magichour.ai/clx7uu86w0a5qp55yxz315r6r/output.mp4",
        "expires_at": "2024-10-19T05:16:19.027Z"
      }
    ],
    "error": null,
    "completed_at": "2024-10-19T05:15:45.123Z"
  }
}
Download URLs Expire: Video download URLs typically expire after 24 hours. Download and store videos promptly or use the API to get fresh URLs.

video.errored

When triggered: Video processing fails due to an error Use cases:
  • Log error details for debugging
  • Retry processing if appropriate
  • Notify user of failure
  • Update job status in database
Key payload fields:
{
  "type": "video.errored",
  "payload": {
    "id": "clx7uu86w0a5qp55yxz315r6r",
    "status": "error",
    "downloads": [],
    "error": {
      "code": "invalid_video_file",
      "message": "The video file contains invalid data. Please try a different file."
    },
    "failed_at": "2024-10-19T05:12:30.456Z"
  }
}
Common error codes:
  • invalid_video_file - Corrupted or unsupported video format
  • file_too_large - Video file exceeds size limits
  • insufficient_credits - Not enough credits to process
  • invalid_parameters - Invalid width, height, or other parameters
  • processing_timeout - Processing took too long and was terminated
No Charges: You’re never charged credits when processing fails with an error.

Image Events

Image events track the lifecycle of image processing jobs. All payloads match the GET /v1/image-projects/:id response format.

image.started

When triggered: Image processing begins (job moves from pending to rendering) Use cases:
  • Show processing indicator in UI
  • Track processing start time
  • Update job status in database
Key payload fields:
{
  "type": "image.started",
  "payload": {
    "id": "clx8abc123def456ghi789",
    "status": "rendering",
    "type": "AI_IMAGE_GENERATOR",
    "created_at": "2024-10-19T05:10:19.027Z",
    "width": 1024,
    "height": 1024,
    "credits_charged": 10,
    "downloads": [],
    "error": null
  }
}
Fast Processing: Some image modes (like AI Image Generator) process so quickly that image.started events may not be sent. Always handle cases where you only receive image.completed.

image.completed

When triggered: Image processing finishes successfully Use cases:
  • Download the generated image
  • Display result to user
  • Update completion status
  • Trigger post-processing workflows
Key payload fields:
{
  "type": "image.completed",
  "payload": {
    "id": "clx8abc123def456ghi789",
    "status": "complete",
    "type": "AI_IMAGE_GENERATOR",
    "downloads": [
      {
        "url": "https://images.magichour.ai/clx8abc123def456ghi789/output.png",
        "expires_at": "2024-10-19T05:16:19.027Z"
      }
    ],
    "error": null,
    "completed_at": "2024-10-19T05:10:25.789Z"
  }
}
Fast Processing: Most image operations complete within 5-30 seconds, much faster than video processing.

image.errored

When triggered: Image processing fails due to an error Use cases:
  • Display error message to user
  • Log error for debugging
  • Retry with different parameters
  • Update job status
Key payload fields:
{
  "type": "image.errored",
  "payload": {
    "id": "clx8abc123def456ghi789",
    "status": "error",
    "downloads": [],
    "error": {
      "code": "no_source_face",
      "message": "Please use an image with a detectable face"
    },
    "failed_at": "2024-10-19T05:10:22.123Z"
  }
}
Common error codes:
  • no_source_face - Face required but not detected in image
  • invalid_file_format - Unsupported image format
  • file_too_large - Image file exceeds size limits
  • insufficient_credits - Not enough credits for operation
  • invalid_parameters - Invalid dimensions or other parameters
  • content_policy_violation - Image violates content guidelines
No Charges: Failed image processing never consumes credits.

Audio Events

Audio events track the lifecycle of audio processing jobs. All payloads match the GET /v1/audio-projects/:id response format.

audio.started

When triggered: Audio processing begins Key payload fields:
{
  "type": "audio.started",
  "payload": {
    "id": "clx9audio123voice456",
    "status": "rendering",
    "type": "AI_VOICE_GENERATOR",
    "created_at": "2024-10-19T05:10:19.027Z",
    "credits_charged": 5,
    "downloads": [],
    "error": null
  }
}

audio.completed

When triggered: Audio processing finishes successfully Key payload fields:
{
  "type": "audio.completed",
  "payload": {
    "id": "clx9audio123voice456",
    "status": "complete",
    "downloads": [
      {
        "url": "https://audio.magichour.ai/clx9audio123voice456/output.mp3",
        "expires_at": "2024-10-19T05:16:19.027Z"
      }
    ],
    "error": null,
    "completed_at": "2024-10-19T05:10:35.456Z"
  }
}

audio.errored

When triggered: Audio processing fails Common error codes:
  • text_too_long - Input text exceeds maximum length
  • invalid_voice - Requested voice not available
  • insufficient_credits - Not enough credits
  • processing_timeout - Audio generation took too long

Event Handling Patterns

Basic Event Router

async def handle_webhook_event(event):
    event_type = event['type']
    payload = event['payload']

    match event_type:
        # Video events
        case 'video.started':
            await handle_video_started(payload)
        case 'video.completed':
            await handle_video_completed(payload)
        case 'video.errored':
            await handle_video_errored(payload)

        # Image events
        case 'image.started':
            await handle_image_started(payload)
        case 'image.completed':
            await handle_image_completed(payload)
        case 'image.errored':
            await handle_image_errored(payload)

        # Audio events
        case 'audio.started':
            await handle_audio_started(payload)
        case 'audio.completed':
            await handle_audio_completed(payload)
        case 'audio.errored':
            await handle_audio_errored(payload)

        case _:
            print(f"Unknown event type: {event_type}")

Database Update Pattern

async def handle_completion_event(event):
    payload = event['payload']
    job_id = payload['id']

    # Update job status in database
    await db.execute(
        "UPDATE jobs SET status = ?, download_url = ?, completed_at = ? WHERE magic_hour_id = ?",
        (payload['status'], payload['downloads'][0]['url'], payload['completed_at'], job_id)
    )

    # Notify user
    await notify_user_completion(job_id)

Error Handling Pattern

async def handle_error_event(event):
    payload = event['payload']
    error = payload['error']

    # Log error details
    logger.error(f"Job {payload['id']} failed", extra={
        'job_id': payload['id'],
        'error_code': error['code'],
        'error_message': error['message'],
        'job_type': payload['type']
    })

    # Update database
    await db.execute(
        "UPDATE jobs SET status = 'failed', error_code = ?, error_message = ? WHERE magic_hour_id = ?",
        (error['code'], error['message'], payload['id'])
    )

    # Notify user of failure
    await notify_user_error(payload['id'], error)

Next Steps