{
  "openapi": "3.1.0",
  "info": {
    "title": "Xclusive VR API",
    "version": "1.0.0",
    "summary": "Send a video, get 3D back.",
    "description": "Convert regular 2D, VR180 and 360° video to 3D for Meta Quest, other VR headsets and Apple Vision Pro. One base URL, a key, JSON in and JSON out.\n\nFull documentation: https://www.xclusivevr.com/business/api/docs\n\n**Authentication.** Send your key in the `Authorization` header of every request: `Authorization: Bearer xvr_live_...`. `xvr_live_…` keys convert for real and use minutes from your balance; `xvr_test_…` keys use the sandbox: free, and nothing is converted. Live and test are separate worlds: a live key never sees test conversions, and the other way round.\n\n**Sandbox.** A test conversion goes from `queued` to `processing` and is `completed` after about 20 seconds. Requests are checked exactly as in live mode, with the same error codes (a source URL must be a public https address), but nothing is downloaded. No video at hand? Use `https://sandbox.xclusivevr.com/trailer.mp4`: that host does not exist, the sandbox only recognises it. `POST /uploads` gives a sandbox `upload_id` (starting with `sandbox/`) to convert straight away; nothing is uploaded and its `upload_url` accepts no files. Any other `upload_id` fails with `upload_not_found`. `outputs` links to real sample files (MP4 for `sbs`, MOV for `spatial`). To test a failure, send `https://sandbox.xclusivevr.com/fail/<code>` as source, with `<code>` one of source_unreachable, source_redirect, source_too_large, unreadable_video, no_video_stream, video_too_long, source_not_360, insufficient_balance, daily_limit_reached, import_timeout, processing_error or conversion_failed: the conversion fails with that `error.code`. Any other source URL with `fail` in it fails with `sandbox_failure`.\n\n**Errors** always come back as JSON: `{ \"error\": { \"code\": \"...\", \"message\": \"...\" } }`, with a stable `code` for your code and a `message` for people.\n\n**Rate limit.** 120 requests a minute per key. Every authenticated response carries the `RateLimit-*` headers; a `429` also carries `Retry-After`.",
    "contact": {
      "name": "Xclusive VR API support",
      "email": "info@xclusivevr.com",
      "url": "https://www.xclusivevr.com/business/api/docs"
    }
  },
  "externalDocs": {
    "description": "API documentation",
    "url": "https://www.xclusivevr.com/business/api/docs"
  },
  "servers": [
    {
      "url": "https://api.xclusivevr.com/v1",
      "description": "Live and sandbox (the key decides which)"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Conversions",
      "description": "Start a conversion, follow its status and get the 3D files."
    },
    {
      "name": "Uploads",
      "description": "Upload your own file when your video is not online."
    },
    {
      "name": "Balance",
      "description": "Your prepaid credit, test minutes and prices."
    },
    {
      "name": "Info",
      "description": "API information."
    },
    {
      "name": "Player",
      "description": "Who can play your hosted videos: websites, signed links and spike alerts."
    }
  ],
  "paths": {
    "/": {
      "get": {
        "tags": [
          "Info"
        ],
        "operationId": "getApiInfo",
        "summary": "API information",
        "description": "Returns the API name and version. Needs no key.",
        "security": [],
        "responses": {
          "200": {
            "description": "API information.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "api": {
                      "type": "string",
                      "examples": [
                        "Xclusive VR"
                      ]
                    },
                    "version": {
                      "type": "string",
                      "examples": [
                        "v1"
                      ]
                    },
                    "docs": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                },
                "example": {
                  "api": "Xclusive VR",
                  "version": "v1",
                  "docs": "https://www.xclusivevr.com/business/api"
                }
              }
            }
          }
        }
      }
    },
    "/conversions": {
      "post": {
        "tags": [
          "Conversions"
        ],
        "operationId": "createConversion",
        "summary": "Create a conversion",
        "description": "Starts a conversion and answers right away with `202 Accepted`. We fetch your video, measure its length and start converting. Give exactly one of `source` or `upload_id`.\n\nWith the `Idempotency-Key` header a repeated request within 24 hours returns the original response, without a second conversion or charge.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateConversionRequest"
              },
              "examples": {
                "full": {
                  "summary": "Side-by-side and spatial, with webhook and streaming",
                  "value": {
                    "source": "https://cdn.example.com/trailer.mp4",
                    "output": [
                      "sbs",
                      "spatial"
                    ],
                    "format": "2d",
                    "webhook": "https://example.com/hooks/xvr",
                    "stream": true,
                    "metadata": {
                      "catalog_id": "A-1042",
                      "batch": "Season 2"
                    }
                  }
                },
                "minimal": {
                  "summary": "Just a link",
                  "value": {
                    "source": "https://cdn.example.com/trailer.mp4",
                    "output": "sbs"
                  }
                },
                "upload": {
                  "summary": "From your own upload",
                  "value": {
                    "upload_id": "api/66f1.../1791633600000_trailer.mp4",
                    "output": "sbs"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The conversion is accepted and queued.",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Conversion"
                },
                "example": {
                  "id": "conv_8f2kq1Lx0aZb3YtQwE1r",
                  "object": "conversion",
                  "mode": "live",
                  "status": "queued",
                  "output": [
                    "sbs",
                    "spatial"
                  ],
                  "format": "2d",
                  "source": {
                    "url": "https://cdn.example.com/trailer.mp4"
                  },
                  "duration_seconds": null,
                  "stream": true,
                  "minutes": 0,
                  "price_tier": null,
                  "charged": {
                    "amount": 0,
                    "currency": "EUR",
                    "test_minutes": 0,
                    "refunded": false
                  },
                  "progress": 0,
                  "error": null,
                  "metadata": {
                    "catalog_id": "A-1042",
                    "batch": "Season 2"
                  },
                  "created_at": "2026-10-10T11:48:00.000Z",
                  "completed_at": null
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          }
        }
      },
      "get": {
        "tags": [
          "Conversions"
        ],
        "operationId": "listConversions",
        "summary": "List conversions",
        "description": "Your most recent conversions, newest first, in the mode of your key. The list has status and progress but no download links: fetch a single conversion for those.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "How many conversions to return, 1 to 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Your most recent conversions.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Conversion"
                      }
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "conv_8f2kq1Lx0aZb3YtQwE1r",
                      "object": "conversion",
                      "mode": "live",
                      "status": "processing",
                      "output": [
                        "sbs"
                      ],
                      "format": "2d",
                      "source": {
                        "url": "https://cdn.example.com/trailer.mp4"
                      },
                      "duration_seconds": 222.4,
                      "stream": false,
                      "minutes": 3.7067,
                      "price_tier": "above_1080p",
                      "charged": {
                        "amount": 1.48,
                        "currency": "EUR",
                        "test_minutes": 0,
                        "refunded": false
                      },
                      "progress": 42,
                      "error": null,
                      "metadata": null,
                      "created_at": "2026-10-10T11:48:00.000Z",
                      "completed_at": null
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/conversions/{id}": {
      "get": {
        "tags": [
          "Conversions"
        ],
        "operationId": "getConversion",
        "summary": "Retrieve a conversion",
        "description": "The current state of one conversion, with fresh download links when it is completed. Polling every 10 to 30 seconds is fine; webhooks save you the trouble.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The conversion id, starts with conv_.",
            "schema": {
              "type": "string",
              "pattern": "^conv_",
              "examples": [
                "conv_8f2kq1Lx0aZb3YtQwE1r"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The conversion.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Conversion"
                },
                "examples": {
                  "completed": {
                    "summary": "A completed conversion with hosting",
                    "value": {
                      "id": "conv_8f2kq1Lx0aZb3YtQwE1r",
                      "object": "conversion",
                      "mode": "live",
                      "status": "completed",
                      "output": [
                        "sbs",
                        "spatial"
                      ],
                      "format": "2d",
                      "source": {
                        "url": "https://cdn.example.com/trailer.mp4"
                      },
                      "duration_seconds": 222.4,
                      "stream": true,
                      "minutes": 3.7067,
                      "price_tier": "above_1080p",
                      "charged": {
                        "amount": 1.85,
                        "currency": "EUR",
                        "test_minutes": 0,
                        "refunded": false
                      },
                      "progress": 100,
                      "error": null,
                      "metadata": {
                        "catalog_id": "A-1042",
                        "batch": "Season 2"
                      },
                      "created_at": "2026-10-10T11:48:00.000Z",
                      "completed_at": "2026-10-10T12:01:12.000Z",
                      "outputs": {
                        "sbs": "https://.../trailer_3D_sbs.mp4?X-Amz-...",
                        "spatial": "https://.../trailer_3D_spatial.mov?X-Amz-..."
                      },
                      "expires_at": "2026-10-17T12:05:40.000Z",
                      "available_until": "2026-11-09T12:01:12.000Z",
                      "player": {
                        "embed_url": "https://www.xclusivevr.com/embed/k7Qx2aPm",
                        "iframe": "<iframe src=\"https://www.xclusivevr.com/embed/k7Qx2aPm\" width=\"960\" height=\"540\" style=\"border:0;max-width:100%\" allow=\"autoplay; fullscreen; xr-spatial-tracking\" allowfullscreen></iframe>"
                      },
                      "stream_url": "https://.../trailer_stream.mp4?X-Amz-...",
                      "hosted_until": "2027-10-10T12:01:12.000Z"
                    }
                  },
                  "failed": {
                    "summary": "A failed conversion",
                    "value": {
                      "id": "conv_Qm3vT8pLx2cR7nW0sKd5",
                      "object": "conversion",
                      "mode": "test",
                      "status": "failed",
                      "output": [
                        "sbs"
                      ],
                      "format": "2d",
                      "source": {
                        "url": "https://example.com/fail.mp4"
                      },
                      "duration_seconds": null,
                      "stream": false,
                      "minutes": 0,
                      "price_tier": null,
                      "charged": {
                        "amount": 0,
                        "currency": "EUR",
                        "test_minutes": 0,
                        "refunded": false
                      },
                      "progress": null,
                      "error": {
                        "code": "sandbox_failure",
                        "message": "Sandbox: this test video always fails."
                      },
                      "metadata": null,
                      "created_at": "2026-10-10T11:48:00.000Z",
                      "completed_at": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "tags": [
          "Conversions"
        ],
        "operationId": "deleteConversion",
        "summary": "Delete a conversion",
        "description": "Deletes a completed or failed conversion right away: the 3D files, the uploaded source, the streaming version, the player and every playback link stop working at once. Sends a conversion.deleted webhook if you have one. Cannot be undone and is not refunded.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The conversion id, starts with conv_.",
            "schema": {
              "type": "string",
              "pattern": "^conv_",
              "examples": [
                "conv_8f2kq1Lx0aZb3YtQwE1r"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "object",
                    "deleted"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "examples": [
                        "conv_8f2kq1Lx0aZb3YtQwE1r"
                      ]
                    },
                    "object": {
                      "type": "string",
                      "const": "conversion"
                    },
                    "deleted": {
                      "type": "boolean",
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "not_found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "conversion_in_progress: the conversion is still running.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/uploads": {
      "post": {
        "tags": [
          "Uploads"
        ],
        "operationId": "createUpload",
        "summary": "Get an upload URL",
        "description": "Is your video not online? Get an upload URL, `PUT` your file to it within 1 hour, then create a conversion with the `upload_id`. Upload the file once: a second PUT to the same URL replaces the file. We read the video itself, so the `Content-Type` you send is not checked. With a test key nothing is uploaded: you get a sandbox `upload_id` to convert straight away, and `upload_url` accepts no files.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "filename": {
                    "type": "string",
                    "description": "The name of your file.",
                    "examples": [
                      "trailer.mp4"
                    ]
                  },
                  "content_type": {
                    "type": "string",
                    "description": "The content type you will upload with.",
                    "default": "video/mp4",
                    "examples": [
                      "video/mp4"
                    ]
                  },
                  "size": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 5368709120,
                    "description": "Optional file size in bytes. Files above 5 GB get file_too_large straight away; send a source URL or use the dashboard for files up to 150 GB."
                  }
                }
              },
              "example": {
                "filename": "trailer.mp4",
                "content_type": "video/mp4"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "An upload URL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Upload"
                },
                "example": {
                  "upload_id": "api/66f1.../1791633600000_trailer.mp4",
                  "upload_url": "https://...",
                  "method": "PUT",
                  "expires_in": 3600,
                  "note": "PUT your file to upload_url, then create a conversion with this upload_id."
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/balance": {
      "get": {
        "tags": [
          "Balance"
        ],
        "operationId": "getBalance",
        "summary": "Get your balance",
        "description": "Your prepaid credit in your account currency (EUR, USD, GBP, CHF, CAD, AUD, SEK, NOK or DKK), your test minutes, your prices per minute and this month's watched minutes. A conversion costs its length in minutes times your price per minute: one price up to 1080p and one above, each with and without hosting, plus the spatial surcharge when you ask for spatial. Test minutes are used first, before your credit.",
        "responses": {
          "200": {
            "description": "Your balance.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Balance"
                },
                "example": {
                  "mode": "live",
                  "balance": {
                    "amount": 412.35,
                    "currency": "EUR"
                  },
                  "test_minutes": 0,
                  "prices_per_minute": {
                    "convert": 0.4,
                    "convert_up_to_1080p": 0.25,
                    "stream": 0.5,
                    "stream_up_to_1080p": 0.35,
                    "spatial_surcharge": 0.1,
                    "currency": "EUR"
                  },
                  "daily_limit_minutes": null,
                  "watched_minutes_this_month": 1840,
                  "included_watched_minutes": 10000,
                  "top_up_url": "https://www.xclusivevr.com/business/api/login"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/conversions/{id}/deliver": {
      "post": {
        "operationId": "deliverConversion",
        "summary": "Deliver to your own storage (again)",
        "description": "Delivers a completed conversion to the storage connected on your account page, for example after you replaced a revoked key. Afterwards you get conversion.delivered or conversion.delivery_failed.",
        "tags": [
          "Conversions"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The conversion id, starts with conv_.",
            "schema": {
              "type": "string",
              "pattern": "^conv_",
              "examples": [
                "conv_8f2kq1Lx0aZb3YtQwE1r"
              ]
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "path": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Optional new subfolder."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Delivery queued.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Conversion"
                },
                "examples": {
                  "completed": {
                    "summary": "A completed conversion with hosting",
                    "value": {
                      "id": "conv_8f2kq1Lx0aZb3YtQwE1r",
                      "object": "conversion",
                      "mode": "live",
                      "status": "completed",
                      "output": [
                        "sbs",
                        "spatial"
                      ],
                      "format": "2d",
                      "source": {
                        "url": "https://cdn.example.com/trailer.mp4"
                      },
                      "duration_seconds": 222.4,
                      "stream": true,
                      "minutes": 3.7067,
                      "price_tier": "above_1080p",
                      "charged": {
                        "amount": 1.85,
                        "currency": "EUR",
                        "test_minutes": 0,
                        "refunded": false
                      },
                      "progress": 100,
                      "error": null,
                      "metadata": {
                        "catalog_id": "A-1042",
                        "batch": "Season 2"
                      },
                      "created_at": "2026-10-10T11:48:00.000Z",
                      "completed_at": "2026-10-10T12:01:12.000Z",
                      "outputs": {
                        "sbs": "https://.../trailer_3D_sbs.mp4?X-Amz-...",
                        "spatial": "https://.../trailer_3D_spatial.mov?X-Amz-..."
                      },
                      "expires_at": "2026-10-17T12:05:40.000Z",
                      "available_until": "2026-11-09T12:01:12.000Z",
                      "player": {
                        "embed_url": "https://www.xclusivevr.com/embed/k7Qx2aPm",
                        "iframe": "<iframe src=\"https://www.xclusivevr.com/embed/k7Qx2aPm\" width=\"960\" height=\"540\" style=\"border:0;max-width:100%\" allow=\"autoplay; fullscreen; xr-spatial-tracking\" allowfullscreen></iframe>"
                      },
                      "stream_url": "https://.../trailer_stream.mp4?X-Amz-...",
                      "hosted_until": "2027-10-10T12:01:12.000Z"
                    }
                  },
                  "failed": {
                    "summary": "A failed conversion",
                    "value": {
                      "id": "conv_Qm3vT8pLx2cR7nW0sKd5",
                      "object": "conversion",
                      "mode": "test",
                      "status": "failed",
                      "output": [
                        "sbs"
                      ],
                      "format": "2d",
                      "source": {
                        "url": "https://example.com/fail.mp4"
                      },
                      "duration_seconds": null,
                      "stream": false,
                      "minutes": 0,
                      "price_tier": null,
                      "charged": {
                        "amount": 0,
                        "currency": "EUR",
                        "test_minutes": 0,
                        "refunded": false
                      },
                      "progress": null,
                      "error": {
                        "code": "sandbox_failure",
                        "message": "Sandbox: this test video always fails."
                      },
                      "metadata": null,
                      "created_at": "2026-10-10T11:48:00.000Z",
                      "completed_at": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "storage_not_connected or invalid_destination.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "not_completed or delivery_in_progress.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/conversions/{id}/hosting": {
      "post": {
        "operationId": "setHostingRenewal",
        "summary": "Turn automatic hosting renewal on or off",
        "description": "Hosting includes 12 months. After that it renews every year at `prices_per_minute.hosting_renewal_per_year` (see GET /balance), taken from your balance the day before the end date, unless you turn it off here. With renewal off, the video goes offline on its end date.",
        "tags": [
          "Conversions"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The conversion id, starts with conv_.",
            "schema": {
              "type": "string",
              "pattern": "^conv_",
              "examples": [
                "conv_8f2kq1Lx0aZb3YtQwE1r"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "auto_renew"
                ],
                "properties": {
                  "auto_renew": {
                    "type": "boolean",
                    "description": "true = renew every year automatically (default), false = let it go offline at the end date."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "400": {
            "description": "storage_not_connected or invalid_destination.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "200": {
            "description": "The conversion, with `hosting.auto_renew` updated.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Conversion"
                },
                "examples": {
                  "completed": {
                    "summary": "A completed conversion with hosting",
                    "value": {
                      "id": "conv_8f2kq1Lx0aZb3YtQwE1r",
                      "object": "conversion",
                      "mode": "live",
                      "status": "completed",
                      "output": [
                        "sbs",
                        "spatial"
                      ],
                      "format": "2d",
                      "source": {
                        "url": "https://cdn.example.com/trailer.mp4"
                      },
                      "duration_seconds": 222.4,
                      "stream": true,
                      "minutes": 3.7067,
                      "price_tier": "above_1080p",
                      "charged": {
                        "amount": 1.85,
                        "currency": "EUR",
                        "test_minutes": 0,
                        "refunded": false
                      },
                      "progress": 100,
                      "error": null,
                      "metadata": {
                        "catalog_id": "A-1042",
                        "batch": "Season 2"
                      },
                      "created_at": "2026-10-10T11:48:00.000Z",
                      "completed_at": "2026-10-10T12:01:12.000Z",
                      "outputs": {
                        "sbs": "https://.../trailer_3D_sbs.mp4?X-Amz-...",
                        "spatial": "https://.../trailer_3D_spatial.mov?X-Amz-..."
                      },
                      "expires_at": "2026-10-17T12:05:40.000Z",
                      "available_until": "2026-11-09T12:01:12.000Z",
                      "player": {
                        "embed_url": "https://www.xclusivevr.com/embed/k7Qx2aPm",
                        "iframe": "<iframe src=\"https://www.xclusivevr.com/embed/k7Qx2aPm\" width=\"960\" height=\"540\" style=\"border:0;max-width:100%\" allow=\"autoplay; fullscreen; xr-spatial-tracking\" allowfullscreen></iframe>"
                      },
                      "stream_url": "https://.../trailer_stream.mp4?X-Amz-...",
                      "hosted_until": "2027-10-10T12:01:12.000Z"
                    }
                  },
                  "failed": {
                    "summary": "A failed conversion",
                    "value": {
                      "id": "conv_Qm3vT8pLx2cR7nW0sKd5",
                      "object": "conversion",
                      "mode": "test",
                      "status": "failed",
                      "output": [
                        "sbs"
                      ],
                      "format": "2d",
                      "source": {
                        "url": "https://example.com/fail.mp4"
                      },
                      "duration_seconds": null,
                      "stream": false,
                      "minutes": 0,
                      "price_tier": null,
                      "charged": {
                        "amount": 0,
                        "currency": "EUR",
                        "test_minutes": 0,
                        "refunded": false
                      },
                      "progress": null,
                      "error": {
                        "code": "sandbox_failure",
                        "message": "Sandbox: this test video always fails."
                      },
                      "metadata": null,
                      "created_at": "2026-10-10T11:48:00.000Z",
                      "completed_at": null
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/playback-settings": {
      "get": {
        "operationId": "getPlaybackSettings",
        "summary": "Get the playback protection settings",
        "tags": [
          "Player"
        ],
        "responses": {
          "200": {
            "description": "The settings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaybackSettings"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updatePlaybackSettings",
        "summary": "Change the playback protection settings",
        "description": "Send only what you want to change. Turn on signed_links_required only once your site requests signed links: embed codes without one stop playing straight away.",
        "tags": [
          "Player"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "allowed_domains": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "signed_links_required": {
                    "type": "boolean"
                  },
                  "spike_alerts": {
                    "type": "object",
                    "properties": {
                      "enabled": {
                        "type": "boolean"
                      },
                      "sensitivity": {
                        "type": "string",
                        "enum": [
                          "low",
                          "normal",
                          "high"
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new settings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaybackSettings"
                }
              }
            }
          },
          "400": {
            "description": "invalid_domain, too_many_domains, invalid_signed_links_required, invalid_spike_alerts or invalid_sensitivity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "These settings apply to your live videos: use a live key (`live_key_required`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/conversions/{id}/playback-links": {
      "post": {
        "operationId": "createPlaybackLink",
        "summary": "Create a signed player link for one viewer",
        "description": "Call this from your server when a viewer opens the page and use the returned iframe instead of the normal one. Needed when signed_links_required is on. The link expires on its own. Player settings can be added with &, for example &autoplay=1. In the sandbox the link points to the demo video.",
        "tags": [
          "Player"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The conversion id, starts with conv_.",
            "schema": {
              "type": "string",
              "pattern": "^conv_",
              "examples": [
                "conv_8f2kq1Lx0aZb3YtQwE1r"
              ]
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "expires_in": {
                    "type": "integer",
                    "minimum": 60,
                    "maximum": 604800,
                    "default": 3600,
                    "description": "How long the link works, in seconds."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The signed link.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "description": "The embed URL with ?token=…"
                    },
                    "iframe": {
                      "type": "string",
                      "description": "The embed code with that URL."
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_expires_in or not_hosted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "not_ready: the conversion is not finished yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/signing-keys": {
      "get": {
        "operationId": "listSigningKeys",
        "summary": "List signing keys",
        "description": "Your signing keys for self-signed playback links. Never includes the private key.",
        "tags": [
          "Player"
        ],
        "responses": {
          "200": {
            "description": "Your keys.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SigningKey"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "operationId": "createSigningKey",
        "summary": "Create a signing key",
        "description": "Creates an RSA key pair for signing playback links on your own server (JWT, RS256, `kid` = id, `sub` = conversion id, `aud` = \"playback\", `exp` required and at most one year ahead). The private key is returned only once. Needs a live key. Up to 5 active keys.",
        "tags": [
          "Player"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 100
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new key, with the private key (only now).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewSigningKey"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/signing-keys/{id}": {
      "delete": {
        "operationId": "revokeSigningKey",
        "summary": "Revoke a signing key",
        "description": "Links signed with this key stop playing straight away. Needs a live key.",
        "tags": [
          "Player"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SigningKey"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "webhooks": {
    "conversion.completed": {
      "post": {
        "operationId": "conversionCompleted",
        "summary": "conversion.completed",
        "description": "The 3D files are ready. `data.outputs` holds the download links.\n\nWe call the `webhook` URL of the conversion, or else the default webhook of your account. Answer with any `2xx` within 10 seconds, then do the work; anything else, a timeout or a redirect counts as not delivered. We try again after 1 minute, 5 minutes, 30 minutes, 2, 6, 12 and 24 hours: 8 attempts over about two days. The same event can arrive more than once: use `X-Xvr-Event-Id` to skip events you already handled.\n\n**Check the signature.** `X-Xvr-Signature` holds a timestamp `t` and `v1`, an HMAC-SHA256 of `\"<t>.<raw body>\"` with your webhook secret (`whsec_…`), in hex. Compute it yourself and compare. Also check that `t` is recent, so an old message cannot be replayed.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/XvrEvent"
          },
          {
            "$ref": "#/components/parameters/XvrEventId"
          },
          {
            "$ref": "#/components/parameters/XvrSignature"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "id": "evt_6702a9c1f0e4d2b8a1c3e5f7",
                "type": "conversion.completed",
                "created_at": "2026-10-10T12:01:12.000Z",
                "data": {
                  "id": "conv_8f2kq1Lx0aZb3YtQwE1r",
                  "object": "conversion",
                  "mode": "live",
                  "status": "completed",
                  "output": [
                    "sbs"
                  ],
                  "format": "2d",
                  "source": {
                    "url": "https://cdn.example.com/trailer.mp4"
                  },
                  "duration_seconds": 222.4,
                  "stream": false,
                  "minutes": 3.7067,
                  "price_tier": "above_1080p",
                  "charged": {
                    "amount": 1.48,
                    "currency": "EUR",
                    "test_minutes": 0,
                    "refunded": false
                  },
                  "progress": 100,
                  "error": null,
                  "metadata": {
                    "catalog_id": "A-1042"
                  },
                  "created_at": "2026-10-10T11:48:00.000Z",
                  "completed_at": "2026-10-10T12:01:12.000Z",
                  "outputs": {
                    "sbs": "https://.../trailer_3D_sbs.mp4?X-Amz-..."
                  },
                  "expires_at": "2026-10-17T12:01:12.000Z",
                  "available_until": "2026-11-09T12:01:12.000Z"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Any 2xx within 10 seconds counts as delivered."
          }
        }
      }
    },
    "conversion.failed": {
      "post": {
        "operationId": "conversionFailed",
        "summary": "conversion.failed",
        "description": "The conversion failed. `data.error` says why. Minutes already charged have been refunded. Delivery, retries and the signature work as for `conversion.completed`.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/XvrEvent"
          },
          {
            "$ref": "#/components/parameters/XvrEventId"
          },
          {
            "$ref": "#/components/parameters/XvrSignature"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "id": "evt_6702a9c1f0e4d2b8a1c3e5f8",
                "type": "conversion.failed",
                "created_at": "2026-10-10T11:52:30.000Z",
                "data": {
                  "id": "conv_8f2kq1Lx0aZb3YtQwE1r",
                  "object": "conversion",
                  "mode": "live",
                  "status": "failed",
                  "output": [
                    "sbs"
                  ],
                  "format": "2d",
                  "source": {
                    "url": "https://cdn.example.com/trailer.mp4"
                  },
                  "duration_seconds": 222.4,
                  "stream": false,
                  "minutes": 3.7067,
                  "price_tier": "above_1080p",
                  "charged": {
                    "amount": 0,
                    "currency": "EUR",
                    "test_minutes": 0,
                    "refunded": true
                  },
                  "progress": null,
                  "error": {
                    "code": "conversion_failed",
                    "message": "The conversion failed. Your minutes have been refunded."
                  },
                  "metadata": {
                    "catalog_id": "A-1042"
                  },
                  "created_at": "2026-10-10T11:48:00.000Z",
                  "completed_at": null
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Any 2xx within 10 seconds counts as delivered."
          }
        }
      }
    },
    "conversion.delivered": {
      "post": {
        "operationId": "conversionDelivered",
        "summary": "conversion.delivered",
        "description": "Only after `POST /v1/conversions/{id}/deliver`: the files are now in your own storage. Delivery, retries and the signature work as for `conversion.completed`.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/XvrEvent"
          },
          {
            "$ref": "#/components/parameters/XvrEventId"
          },
          {
            "$ref": "#/components/parameters/XvrSignature"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "id": "evt_6702a9c1f0e4d2b8a1c3e5f8",
                "type": "conversion.delivered",
                "created_at": "2026-10-10T11:52:30.000Z",
                "data": {
                  "id": "conv_8f2kq1Lx0aZb3YtQwE1r",
                  "object": "conversion",
                  "mode": "live",
                  "status": "failed",
                  "output": [
                    "sbs"
                  ],
                  "format": "2d",
                  "source": {
                    "url": "https://cdn.example.com/trailer.mp4"
                  },
                  "duration_seconds": 222.4,
                  "stream": false,
                  "minutes": 3.7067,
                  "price_tier": "above_1080p",
                  "charged": {
                    "amount": 0,
                    "currency": "EUR",
                    "test_minutes": 0,
                    "refunded": true
                  },
                  "progress": null,
                  "error": {
                    "code": "conversion_failed",
                    "message": "The conversion failed. Your minutes have been refunded."
                  },
                  "metadata": {
                    "catalog_id": "A-1042"
                  },
                  "created_at": "2026-10-10T11:48:00.000Z",
                  "completed_at": null
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Any 2xx within 10 seconds counts as delivered."
          }
        }
      }
    },
    "conversion.delivery_failed": {
      "post": {
        "operationId": "conversionDeliveryFailed",
        "summary": "conversion.delivery_failed",
        "description": "Only after `POST /v1/conversions/{id}/deliver`: delivering to your own storage failed again. `data.delivery.error` says why.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/XvrEvent"
          },
          {
            "$ref": "#/components/parameters/XvrEventId"
          },
          {
            "$ref": "#/components/parameters/XvrSignature"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "id": "evt_6702a9c1f0e4d2b8a1c3e5f8",
                "type": "conversion.delivery_failed",
                "created_at": "2026-10-10T11:52:30.000Z",
                "data": {
                  "id": "conv_8f2kq1Lx0aZb3YtQwE1r",
                  "object": "conversion",
                  "mode": "live",
                  "status": "failed",
                  "output": [
                    "sbs"
                  ],
                  "format": "2d",
                  "source": {
                    "url": "https://cdn.example.com/trailer.mp4"
                  },
                  "duration_seconds": 222.4,
                  "stream": false,
                  "minutes": 3.7067,
                  "price_tier": "above_1080p",
                  "charged": {
                    "amount": 0,
                    "currency": "EUR",
                    "test_minutes": 0,
                    "refunded": true
                  },
                  "progress": null,
                  "error": {
                    "code": "conversion_failed",
                    "message": "The conversion failed. Your minutes have been refunded."
                  },
                  "metadata": {
                    "catalog_id": "A-1042"
                  },
                  "created_at": "2026-10-10T11:48:00.000Z",
                  "completed_at": null
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Any 2xx within 10 seconds counts as delivered."
          }
        }
      }
    },
    "test.ping": {
      "post": {
        "operationId": "testPing",
        "summary": "test.ping",
        "description": "Only when you press \"Send test event\" on your account page. `data.message` holds a short text; use it to check your endpoint and signature.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/XvrEvent"
          },
          {
            "$ref": "#/components/parameters/XvrEventId"
          },
          {
            "$ref": "#/components/parameters/XvrSignature"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "id": "evt_6702a9c1f0e4d2b8a1c3e5f8",
                "type": "test.ping",
                "created_at": "2026-10-10T11:52:30.000Z",
                "data": {
                  "message": "This is a test event from Xclusive VR."
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Any 2xx within 10 seconds counts as delivered."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Your API key: `xvr_live_…` (converts for real and uses your balance) or `xvr_test_…` (sandbox: free, nothing is converted). You create keys on your account page. Keep keys on your server, never in an app, a browser or a public repository."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "A unique value per conversion, for example a UUID or your own id for the video. Up to 255 characters. Remembered for 24 hours, per account. Same key and same body: the original response, with header `Idempotent-Replayed: true`. Same key and a different body: `409 idempotency_key_reused`. Still being handled: `409 request_in_progress`. After a `5xx` error on our side the key is released, so retrying with it really tries again.",
        "schema": {
          "type": "string",
          "maxLength": 255,
          "examples": [
            "trailer-042"
          ]
        }
      },
      "XvrEvent": {
        "name": "X-Xvr-Event",
        "in": "header",
        "required": true,
        "description": "The event type.",
        "schema": {
          "type": "string",
          "enum": [
            "conversion.completed",
            "conversion.failed"
          ]
        }
      },
      "XvrEventId": {
        "name": "X-Xvr-Event-Id",
        "in": "header",
        "required": true,
        "description": "The event id, the same as `id` in the body. Use it to skip events you already handled.",
        "schema": {
          "type": "string",
          "examples": [
            "evt_6702a9c1f0e4d2b8a1c3e5f7"
          ]
        }
      },
      "XvrSignature": {
        "name": "X-Xvr-Signature",
        "in": "header",
        "required": true,
        "description": "`t=<unix timestamp>,v1=<hex HMAC-SHA256 of \"<t>.<raw body>\" with your webhook secret>`.",
        "schema": {
          "type": "string",
          "examples": [
            "t=1791633672,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd"
          ]
        }
      }
    },
    "headers": {
      "IdempotentReplayed": {
        "description": "`true` when this is the stored response to an earlier request with the same Idempotency-Key.",
        "schema": {
          "type": "string",
          "enum": [
            "true"
          ]
        }
      },
      "RateLimitPolicy": {
        "description": "The rate limit policy: 120 requests per 60 seconds per key.",
        "schema": {
          "type": "string",
          "examples": [
            "120;w=60"
          ]
        }
      },
      "RateLimitLimit": {
        "description": "Requests allowed per minute with this key.",
        "schema": {
          "type": "integer",
          "examples": [
            120
          ]
        }
      },
      "RateLimitRemaining": {
        "description": "Requests left in the current minute.",
        "schema": {
          "type": "integer",
          "examples": [
            117
          ]
        }
      },
      "RateLimitReset": {
        "description": "Seconds until the current window resets.",
        "schema": {
          "type": "integer",
          "examples": [
            42
          ]
        }
      },
      "RetryAfter": {
        "description": "Seconds to wait before you retry.",
        "schema": {
          "type": "integer",
          "examples": [
            42
          ]
        }
      }
    },
    "schemas": {
      "CreateConversionRequest": {
        "type": "object",
        "description": "Give exactly one of `source` or `upload_id`.",
        "properties": {
          "source": {
            "type": "string",
            "format": "uri",
            "description": "An https link to your video. Must be publicly reachable, without redirects. Give either source or upload_id.",
            "examples": [
              "https://cdn.example.com/trailer.mp4"
            ]
          },
          "upload_id": {
            "type": "string",
            "description": "The id from POST /v1/uploads, if you uploaded the file yourself.",
            "examples": [
              "api/66f1.../1791633600000_trailer.mp4"
            ]
          },
          "output": {
            "description": "\"sbs\" (side-by-side, for Meta Quest, other VR headsets and most players), \"spatial\" (Apple Vision Pro) or both: [\"sbs\", \"spatial\"]. Default \"sbs\". Side-by-side is included in your price per minute; spatial adds €0.10 per minute (see prices_per_minute.spatial_surcharge in GET /v1/balance).",
            "oneOf": [
              {
                "$ref": "#/components/schemas/OutputType"
              },
              {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/OutputType"
                },
                "minItems": 1,
                "maxItems": 2,
                "uniqueItems": true
              }
            ],
            "default": "sbs"
          },
          "format": {
            "type": "string",
            "enum": [
              "2d",
              "180",
              "360"
            ],
            "default": "2d",
            "description": "\"2d\" for regular video (default), \"180\" for VR180, \"360\" for 360° video. A 360° source must be exactly twice as wide as it is tall. 360° is delivered as one file that plays in every headset: use output \"sbs\"."
          },
          "depth": {
            "type": "string",
            "enum": [
              "comfort",
              "cinema",
              "aggressive"
            ],
            "default": "cinema",
            "description": "How strong the 3D depth is. comfort: gentle, easy on the eyes for long viewing; cinema: the default, natural depth like a 3D film; aggressive: the strongest pop-out effect."
          },
          "webhook": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Optional. An https URL we call when the conversion is completed or failed. Without it we use the default webhook of your account, if you have one. Must be https on port 443; we do not follow redirects.",
            "examples": [
              "https://example.com/hooks/xvr"
            ]
          },
          "stream": {
            "type": "boolean",
            "default": false,
            "description": "Optional. true = with hosting (+€0.10 per minute): we host the 3D video for 12 months and you get a player to embed and a stream link. Needs \"sbs\" in output. Default false (files to download only)."
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "Optional. Your own data, up to 2 KB, returned unchanged on the conversion and in every webhook. Add \"batch\" (for example \"Season 2\") to group videos on your account page, with \"3 of 24 ready\".",
            "examples": [
              {
                "catalog_id": "A-1042",
                "batch": "Season 2"
              }
            ]
          },
          "destination": {
            "description": "Deliver the 3D files to your own S3-compatible storage (connect it on your account page). true = the folder of your connection, { path } = a subfolder, false = not. Without it, your account setting applies.",
            "oneOf": [
              {
                "type": "boolean"
              },
              {
                "type": "object",
                "properties": {
                  "path": {
                    "type": "string",
                    "maxLength": 200,
                    "pattern": "^[A-Za-z0-9._/-]*$",
                    "examples": [
                      "series/s02e01"
                    ]
                  }
                },
                "additionalProperties": false
              }
            ]
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 150,
            "description": "Your own name for the video in the dashboard. Leave it out to use the file name."
          }
        },
        "oneOf": [
          {
            "required": [
              "source"
            ]
          },
          {
            "required": [
              "upload_id"
            ]
          }
        ]
      },
      "OutputType": {
        "type": "string",
        "enum": [
          "sbs",
          "spatial"
        ],
        "description": "sbs: side-by-side, for Meta Quest, other VR headsets and most players. spatial: Apple Vision Pro."
      },
      "Conversion": {
        "type": "object",
        "required": [
          "id",
          "object",
          "mode",
          "status",
          "output",
          "format",
          "source",
          "duration_seconds",
          "stream",
          "minutes",
          "charged",
          "progress",
          "error",
          "metadata",
          "created_at",
          "completed_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique id, starts with conv_.",
            "examples": [
              "conv_8f2kq1Lx0aZb3YtQwE1r"
            ]
          },
          "object": {
            "type": "string",
            "const": "conversion"
          },
          "mode": {
            "type": "string",
            "enum": [
              "live",
              "test"
            ],
            "description": "\"live\" or \"test\"."
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "processing",
              "completed",
              "failed"
            ],
            "description": "queued → processing → completed, or failed."
          },
          "output": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OutputType"
            },
            "description": "What you asked for."
          },
          "format": {
            "type": "string",
            "enum": [
              "2d",
              "180",
              "360"
            ],
            "description": "What you asked for."
          },
          "source": {
            "description": "Where the video came from.",
            "oneOf": [
              {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  }
                }
              },
              {
                "type": "object",
                "required": [
                  "upload_id"
                ],
                "properties": {
                  "upload_id": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              }
            ]
          },
          "duration_seconds": {
            "type": [
              "number",
              "null"
            ],
            "description": "The length of your video, as we measured it. null until measured."
          },
          "stream": {
            "type": "boolean",
            "description": "true when the video is hosted."
          },
          "minutes": {
            "type": "number",
            "description": "The billable length in minutes: per second, at least 0.5."
          },
          "price_tier": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "up_to_1080p",
              "above_1080p",
              null
            ],
            "description": "Which price per minute applied, from the resolution we measured. null until measured."
          },
          "charged": {
            "type": "object",
            "description": "What this conversion cost, from your credit and your test minutes. 0 after a refund.",
            "required": [
              "amount",
              "currency",
              "test_minutes",
              "refunded"
            ],
            "properties": {
              "amount": {
                "type": "number",
                "description": "Charged from your credit."
              },
              "currency": {
                "type": "string",
                "const": "EUR"
              },
              "test_minutes": {
                "type": "number",
                "description": "Test minutes used."
              },
              "refunded": {
                "type": "boolean",
                "description": "true when the conversion failed and the charge went back."
              }
            }
          },
          "progress": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 100,
            "description": "0 to 100. Never goes down; 100 only when completed. null when failed."
          },
          "error": {
            "description": "When failed: { code, message }. null otherwise.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/ConversionError"
              },
              {
                "type": "null"
              }
            ]
          },
          "metadata": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true,
            "description": "Your own data, unchanged."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp, UTC."
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "ISO 8601 timestamp, UTC. null until completed."
          },
          "outputs": {
            "type": "object",
            "description": "When completed (single conversion and webhooks, not in the list): a download link per output. Each link works for 7 days; fetch the conversion again for fresh links. In the sandbox: fixed links to real sample files (MP4 for sbs, MOV for spatial).",
            "properties": {
              "sbs": {
                "type": "string",
                "format": "uri"
              },
              "spatial": {
                "type": "string",
                "format": "uri"
              }
            }
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the links in outputs stop working."
          },
          "available_until": {
            "type": "string",
            "format": "date-time",
            "description": "Until when the files can be downloaded: 30 days after completion. After that they are deleted."
          },
          "player": {
            "description": "With hosting, when completed: { embed_url, iframe }.",
            "oneOf": [
              {
                "type": "object",
                "required": [
                  "embed_url",
                  "iframe"
                ],
                "properties": {
                  "embed_url": {
                    "type": "string",
                    "format": "uri",
                    "examples": [
                      "https://www.xclusivevr.com/embed/k7Qx2aPm"
                    ]
                  },
                  "iframe": {
                    "type": "string",
                    "description": "Paste this into your page."
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "stream_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "With hosting, when completed: a link to the streaming version for your own player or app. Works for 7 days; fetch the conversion again for a fresh one."
          },
          "hosted_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "With hosting: until when we host the video and the player plays it. null in the sandbox."
          },
          "depth": {
            "type": "string",
            "enum": [
              "comfort",
              "cinema",
              "aggressive"
            ],
            "description": "The depth preset used."
          },
          "destination": {
            "type": [
              "object",
              "null"
            ],
            "description": "With your own storage: where the files go. Otherwise null.",
            "properties": {
              "path": {
                "type": "string"
              }
            }
          },
          "delivery": {
            "type": [
              "object",
              "null"
            ],
            "description": "With your own storage: how far delivery is. Otherwise null.",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "pending",
                  "delivering",
                  "delivered",
                  "failed"
                ]
              },
              "files": {
                "type": "object",
                "description": "The key of each output in your bucket.",
                "additionalProperties": {
                  "type": "string"
                }
              },
              "error": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "attempts": {
                "type": "integer"
              },
              "delivered_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "sandbox": {
                "type": "boolean",
                "description": "Sandbox only: nothing was written."
              }
            }
          },
          "hosting": {
            "type": [
              "object",
              "null"
            ],
            "description": "With hosting: whether it renews automatically after each year, and how often it has.",
            "properties": {
              "auto_renew": {
                "type": "boolean"
              },
              "renewals": {
                "type": "integer"
              }
            }
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "The name you gave, or null."
          }
        }
      },
      "ConversionError": {
        "type": "object",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "Why the conversion failed. The complete list: source_unreachable, source_redirect, source_too_large, upload_not_found, unreadable_video, no_video_stream, video_too_long, source_not_360, invalid_source, insufficient_balance, daily_limit_reached, import_timeout, conversion_failed, processing_error, sandbox_failure. We never put an internal error code here.",
            "examples": [
              "conversion_failed"
            ]
          },
          "message": {
            "type": "string",
            "description": "For people."
          }
        }
      },
      "Upload": {
        "type": "object",
        "required": [
          "upload_id",
          "upload_url",
          "method",
          "expires_in"
        ],
        "properties": {
          "upload_id": {
            "type": "string",
            "description": "Use this as upload_id when you create the conversion."
          },
          "upload_url": {
            "type": "string",
            "format": "uri",
            "description": "PUT your file here, within 1 hour. In the sandbox this address accepts no files."
          },
          "method": {
            "type": "string",
            "const": "PUT"
          },
          "expires_in": {
            "type": "integer",
            "description": "Seconds until upload_url stops working.",
            "examples": [
              3600
            ]
          },
          "note": {
            "type": "string"
          }
        }
      },
      "Balance": {
        "type": "object",
        "required": [
          "mode",
          "balance",
          "test_minutes",
          "prices_per_minute",
          "daily_limit_minutes",
          "watched_minutes_this_month",
          "included_watched_minutes",
          "top_up_url"
        ],
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "live",
              "test"
            ]
          },
          "balance": {
            "type": "object",
            "description": "Your prepaid credit.",
            "required": [
              "amount",
              "currency"
            ],
            "properties": {
              "amount": {
                "type": "number"
              },
              "currency": {
                "type": "string",
                "const": "EUR"
              }
            }
          },
          "test_minutes": {
            "type": "number",
            "description": "Test minutes left. Used first, before your credit."
          },
          "prices_per_minute": {
            "type": "object",
            "description": "Your prices per minute, excluding VAT. Hosting and spatial each add the same amount to the price of the resolution tier.",
            "required": [
              "convert",
              "convert_up_to_1080p",
              "stream",
              "stream_up_to_1080p",
              "spatial_surcharge",
              "currency"
            ],
            "properties": {
              "convert": {
                "type": "number",
                "description": "Without hosting, video above 1080p."
              },
              "convert_up_to_1080p": {
                "type": "number",
                "description": "Without hosting, video up to 1080p (short side 1080 pixels or less)."
              },
              "stream": {
                "type": "number",
                "description": "With hosting, video above 1080p."
              },
              "stream_up_to_1080p": {
                "type": "number",
                "description": "With hosting, video up to 1080p."
              },
              "spatial_surcharge": {
                "type": "number",
                "description": "Added per minute when output includes \"spatial\"."
              },
              "currency": {
                "type": "string",
                "const": "EUR"
              },
              "hosting_renewal_per_year": {
                "type": "number",
                "description": "One more year of hosting after the first 12 months, per video minute (lower with Player Pro)."
              }
            }
          },
          "daily_limit_minutes": {
            "type": [
              "number",
              "null"
            ],
            "description": "A daily limit, if you agreed one with us. Resets at 00:00 UTC."
          },
          "watched_minutes_this_month": {
            "type": "integer",
            "description": "Minutes watched in the embedded player this month."
          },
          "included_watched_minutes": {
            "type": "integer",
            "description": "Watched minutes a month included; beyond that €0.0015 per minute."
          },
          "top_up_url": {
            "type": "string",
            "format": "uri",
            "description": "The login page of your dashboard (we email you a code): top up by payment card and download invoices."
          }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "required": [
          "id",
          "type",
          "created_at",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The event id, the same as X-Xvr-Event-Id.",
            "examples": [
              "evt_6702a9c1f0e4d2b8a1c3e5f7"
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "conversion.completed",
              "conversion.failed",
              "conversion.delivered",
              "conversion.delivery_failed"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "$ref": "#/components/schemas/Conversion"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable, for your code."
              },
              "message": {
                "type": "string",
                "description": "For people."
              }
            }
          }
        }
      },
      "PlaybackSettings": {
        "type": "object",
        "description": "Who can play your hosted videos. Applies to your whole account and to every hosted video straight away.",
        "properties": {
          "allowed_domains": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The player only plays when embedded on these sites (subdomains included). Empty = every website.",
            "examples": [
              [
                "yourwebsite.com"
              ]
            ]
          },
          "plays_everywhere": {
            "type": "boolean",
            "readOnly": true,
            "description": "true when allowed_domains is empty."
          },
          "signed_links_required": {
            "type": "boolean",
            "description": "true = the player only plays with a link from POST /conversions/{id}/playback-links."
          },
          "spike_alerts": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Email when a video is suddenly watched far more than in a normal hour (at most once per video per day)."
              },
              "sensitivity": {
                "type": "string",
                "enum": [
                  "low",
                  "normal",
                  "high"
                ],
                "description": "low = 10× a normal hour and at least 3,000 minutes, normal = 5× and at least 1,000, high = 3× and at least 300."
              }
            }
          }
        }
      },
      "SigningKey": {
        "type": "object",
        "required": [
          "id",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Use as `kid` in the header of the tokens you sign.",
            "examples": [
              "xvrsk_Qm3x9aPz0LkT5vRw"
            ]
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_used_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When a token signed with this key last played a video."
          },
          "revoked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "NewSigningKey": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SigningKey"
          },
          {
            "type": "object",
            "required": [
              "private_key",
              "private_key_pem"
            ],
            "properties": {
              "private_key": {
                "type": "string",
                "description": "The private key (PKCS#8 PEM), base64-encoded. Shown only once; we keep only the public key."
              },
              "private_key_pem": {
                "type": "string",
                "description": "The same private key as PEM text."
              },
              "note": {
                "type": "string"
              }
            }
          }
        ]
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Something in the request is not right; the message says what. Codes: invalid_json, invalid_field_name, invalid_source, invalid_output, invalid_format, invalid_depth, invalid_stream, invalid_webhook, invalid_metadata, invalid_title, invalid_upload_id, invalid_idempotency_key, invalid_destination, storage_not_connected, invalid_size, file_too_large (uploads), invalid_expires_in, not_hosted (playback links), invalid_auto_renew (hosting), invalid_domain, too_many_domains, invalid_signed_links_required, invalid_spike_alerts, invalid_sensitivity (playback settings), invalid_name (signing keys).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "invalid_source",
                "message": "source must be an https URL."
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "No key, an unknown key or a revoked key (unauthorized), or a key past its expiry date (key_expired).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "unauthorized",
                "message": "Missing or invalid API key. Use: Authorization: Bearer xvr_live_…"
              }
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "Your balance is empty: no credit and no test minutes.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "insufficient_balance",
                "message": "Your balance is empty. Top up to continue."
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "Your account is suspended (account_suspended), or you changed playback settings or signing keys with a test key (live_key_required).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "account_suspended",
                "message": "This account is suspended. Contact us."
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "No such route, or no conversion with this id in this mode.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "not_found",
                "message": "No conversion with this id."
              }
            }
          }
        }
      },
      "Conflict": {
        "description": "idempotency_key_reused: the Idempotency-Key was used for a different request. request_in_progress: the original request is still being handled; retry in a moment. not_completed, delivery_in_progress: POST …/deliver before the conversion is completed or while a delivery runs. not_ready: POST …/playback-links before the player is available. too_many_signing_keys: you already have 5 active signing keys; revoke one first.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "idempotency_key_reused",
                "message": "This Idempotency-Key was used for a different request."
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "rate_limited: more than 120 requests a minute with this key. too_many_active_conversions: your account already has its maximum number of conversions running (2 for self-service accounts). daily_limit_reached: your daily video minutes are used up until 00:00 UTC. Wait for Retry-After and retry.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "rate_limited",
                "message": "Too many requests. Slow down and retry after the Retry-After header."
              }
            }
          }
        }
      },
      "InternalError": {
        "description": "Something went wrong on our side. Retry with the same Idempotency-Key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "internal_error",
                "message": "Something went wrong on our side. Retry with the same Idempotency-Key."
              }
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "The request body is too large (request_too_large).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "UnsupportedMediaType": {
        "description": "Send the body as UTF-8 JSON with Content-Type: application/json (unsupported_media_type).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}
