{
  "openapi": "3.0.3",
  "info": {
    "title": "DigiCMS integrations API",
    "version": "1.0.0",
    "description": "Access to one merchant's media, campaigns, playlists and tags for a third party's system: reading, and with the write scopes uploading media and adding tags to it. Authenticate with a merchant's integration token (`Authorization: Bearer dcmi_…`), created by an owner or admin in Settings → Integrations. The token decides the merchant: no URL names one. See docs/integrations.md."
  },
  "servers": [
    {
      "url": "/api/integrations/v1"
    }
  ],
  "security": [
    {
      "integrationToken": []
    }
  ],
  "paths": {
    "/media": {
      "get": {
        "operationId": "mediaList",
        "summary": "List ready media, newest first",
        "description": "Needs the `media:read` scope. With `media:files` too, each media item carries `fileUrl`.",
        "security": [
          {
            "integrationToken": [
              "media:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The `nextCursor` of the previous page."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "tag",
            "in": "query",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "format": "uuid"
              }
            },
            "style": "form",
            "explode": true,
            "description": "A tag id from GET /tags. Repeat it to require every tag listed."
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Matches the media's name or description, ignoring case."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MediaPage"
                }
              }
            }
          },
          "400": {
            "description": "A query parameter is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, unknown, expired or revoked, or its merchant is suspended or its plan has ended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs; the message names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 120 requests in a minute with this token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until the next minute."
              }
            }
          }
        }
      }
    },
    "/media/{id}": {
      "get": {
        "operationId": "mediaGet",
        "summary": "One media item",
        "description": "Needs the `media:read` scope. With `media:files` too, each media item carries `fileUrl`.",
        "security": [
          {
            "integrationToken": [
              "media:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "A media id."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Media"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, unknown, expired or revoked, or its merchant is suspended or its plan has ended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs; the message names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such item in the token's merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 120 requests in a minute with this token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until the next minute."
              }
            }
          }
        }
      }
    },
    "/campaigns": {
      "get": {
        "operationId": "campaignsList",
        "summary": "List campaigns (not archived), most recently changed first",
        "description": "Needs the `campaigns:read` scope.",
        "security": [
          {
            "integrationToken": [
              "campaigns:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The `nextCursor` of the previous page."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "tag",
            "in": "query",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "format": "uuid"
              }
            },
            "style": "form",
            "explode": true,
            "description": "A tag id from GET /tags. Repeat it to require every tag listed."
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Matches the campaign's name, ignoring case."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignPage"
                }
              }
            }
          },
          "400": {
            "description": "A query parameter is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, unknown, expired or revoked, or its merchant is suspended or its plan has ended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs; the message names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 120 requests in a minute with this token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until the next minute."
              }
            }
          }
        }
      }
    },
    "/campaigns/{lineageId}": {
      "get": {
        "operationId": "campaignsGet",
        "summary": "One campaign, with the media it plays",
        "description": "Needs the `campaigns:read` scope.",
        "security": [
          {
            "integrationToken": [
              "campaigns:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "lineageId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "A campaign's lineage id."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignDetail"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, unknown, expired or revoked, or its merchant is suspended or its plan has ended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs; the message names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such item in the token's merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 120 requests in a minute with this token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until the next minute."
              }
            }
          }
        }
      }
    },
    "/playlists": {
      "get": {
        "operationId": "playlistsList",
        "summary": "List shared playlists, most recently updated first",
        "description": "Needs the `playlists:read` scope.",
        "security": [
          {
            "integrationToken": [
              "playlists:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The `nextCursor` of the previous page."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Matches the playlist's name, ignoring case."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaylistPage"
                }
              }
            }
          },
          "400": {
            "description": "A query parameter is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, unknown, expired or revoked, or its merchant is suspended or its plan has ended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs; the message names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 120 requests in a minute with this token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until the next minute."
              }
            }
          }
        }
      }
    },
    "/playlists/{id}": {
      "get": {
        "operationId": "playlistsGet",
        "summary": "One playlist with its items in play order",
        "description": "Needs the `playlists:read` scope.",
        "security": [
          {
            "integrationToken": [
              "playlists:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "A playlist id, e.g. a campaign's content.landscape.playlistId."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaylistDetail"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, unknown, expired or revoked, or its merchant is suspended or its plan has ended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs; the message names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such item in the token's merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 120 requests in a minute with this token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until the next minute."
              }
            }
          }
        }
      }
    },
    "/media/uploads": {
      "post": {
        "operationId": "mediaUpload",
        "summary": "Start uploading a file to the media library",
        "description": "Needs the `media:write` scope. Then PUT the file (and thumbnail) to the returned URLs and call finalize. Uploading an exact file the merchant already has returns `duplicateOf` and nothing to upload.",
        "security": [
          {
            "integrationToken": [
              "media:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UploadRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadTicket"
                }
              }
            }
          },
          "400": {
            "description": "The body is not valid JSON or not what this endpoint takes; the message names the fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, unknown, expired or revoked, or its merchant is suspended or its plan has ended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs, or the merchant's plan does not allow it (`code: plan`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`code: storage_quota_exceeded`: the merchant's storage is full. `code: id_conflict`: the id is another item's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 16 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The file is not accepted: type, size, checksum or name; the message says which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 30 writes in a minute with this token (counted apart from reads).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until the next minute."
              }
            }
          }
        }
      }
    },
    "/media/uploads/{id}/finalize": {
      "post": {
        "operationId": "mediaFinalizeUpload",
        "summary": "Finish an upload: the item becomes ready",
        "description": "Needs the `media:write` scope. Checks that the file was stored with the declared size. Returns the item as GET /media/{id} does. Finalizing a ready item again returns it.",
        "security": [
          {
            "integrationToken": [
              "media:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The mediaId from POST /media/uploads."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Media"
                }
              }
            }
          },
          "400": {
            "description": "The body is not valid JSON or not what this endpoint takes; the message names the fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, unknown, expired or revoked, or its merchant is suspended or its plan has ended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs, or the merchant's plan does not allow it (`code: plan`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such upload in the token's merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 16 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The file was not stored, or not with the declared size; PUT it again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 30 writes in a minute with this token (counted apart from reads).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until the next minute."
              }
            }
          }
        }
      }
    },
    "/media/{id}/tags": {
      "post": {
        "operationId": "mediaAddTags",
        "summary": "Add tags to a media item",
        "description": "Needs the `tags:write` scope. Up to 10 names a call. Only adds: taking tags off is not offered.",
        "security": [
          {
            "integrationToken": [
              "tags:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "A media id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddTagsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AddTagsResult"
                }
              }
            }
          },
          "400": {
            "description": "The body is not valid JSON or not what this endpoint takes; the message names the fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, unknown, expired or revoked, or its merchant is suspended or its plan has ended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs, or the merchant's plan does not allow it (`code: plan`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such media item in the token's merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 16 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The body asks to remove tags, or the item would have too many tags.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 30 writes in a minute with this token (counted apart from reads).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until the next minute."
              }
            }
          }
        }
      }
    },
    "/tags": {
      "get": {
        "operationId": "tagsList",
        "summary": "List tags by name",
        "description": "Needs the `tags:read` scope.",
        "security": [
          {
            "integrationToken": [
              "tags:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The `nextCursor` of the previous page."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Matches the tag's name, ignoring case."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TagPage"
                }
              }
            }
          },
          "400": {
            "description": "A query parameter is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, unknown, expired or revoked, or its merchant is suspended or its plan has ended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs; the message names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 120 requests in a minute with this token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until the next minute."
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "integrationToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "A merchant's integration token, `dcmi_…`. Scopes: media:read, media:files, media:write, campaigns:read, playlists:read, tags:read, tags:write."
      }
    },
    "schemas": {
      "Tag": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "name": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name"
        ]
      },
      "Media": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "image",
              "video",
              "web"
            ]
          },
          "width": {
            "nullable": true,
            "description": "Pixels; null when unknown (and for web pages).",
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "height": {
            "nullable": true,
            "description": "Pixels; null when unknown (and for web pages).",
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "durationSeconds": {
            "nullable": true,
            "description": "A video's length; null for images and web pages.",
            "type": "number"
          },
          "tags": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Tag"
            }
          },
          "thumbnailUrl": {
            "nullable": true,
            "description": "A signed URL that stops working 15 minutes after the response; null when there is no thumbnail.",
            "type": "string",
            "format": "uri"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          },
          "fileUrl": {
            "description": "Only with the media:files scope. The file itself: a signed URL that stops working 15 minutes after the response, or a web page's own address. Absent without that scope.",
            "nullable": true,
            "type": "string",
            "format": "uri"
          }
        },
        "required": [
          "id",
          "name",
          "type",
          "width",
          "height",
          "durationSeconds",
          "tags",
          "thumbnailUrl",
          "createdAt"
        ]
      },
      "PlayItem": {
        "type": "object",
        "properties": {
          "mediaId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "type": {
            "type": "string",
            "enum": [
              "image",
              "video",
              "web"
            ]
          },
          "durationSeconds": {
            "type": "number",
            "description": "How long it plays, in seconds: the item's own setting, else the media's length or the default."
          }
        },
        "required": [
          "mediaId",
          "type",
          "durationSeconds"
        ]
      },
      "Playlist": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "name": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          }
        },
        "required": [
          "id",
          "name",
          "createdAt",
          "updatedAt"
        ]
      },
      "PlaylistDetail": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "name": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
          },
          "totalDurationSeconds": {
            "type": "number"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PlayItem"
            },
            "description": "In play order."
          }
        },
        "required": [
          "id",
          "name",
          "createdAt",
          "updatedAt",
          "totalDurationSeconds",
          "items"
        ]
      },
      "CampaignContent": {
        "type": "object",
        "properties": {
          "playlistId": {
            "nullable": true,
            "description": "The playlist it was published from (GET /playlists/{id}); null when that playlist was deleted since.",
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PlayItem"
            },
            "description": "In play order, as frozen at publish: what screens play now."
          }
        },
        "required": [
          "playlistId",
          "items"
        ]
      },
      "Campaign": {
        "type": "object",
        "properties": {
          "lineageId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "The campaign's stable id: it stays the same across versions."
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "running",
              "upcoming",
              "draft",
              "ended",
              "withdrawn"
            ],
            "description": "running, upcoming or ended while live; else draft (never published) or withdrawn."
          },
          "flight": {
            "type": "object",
            "properties": {
              "start": {
                "nullable": true,
                "description": "`YYYY-MM-DD` or `YYYY-MM-DDTHH:MM`, local time of each screen; null = open.",
                "type": "string"
              },
              "end": {
                "nullable": true,
                "description": "`YYYY-MM-DD` or `YYYY-MM-DDTHH:MM`, local time of each screen; null = open.",
                "type": "string"
              }
            },
            "required": [
              "start",
              "end"
            ],
            "description": "The live version's flight, else the newest version's."
          },
          "tags": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Tag"
            }
          }
        },
        "required": [
          "lineageId",
          "name",
          "status",
          "flight",
          "tags"
        ]
      },
      "CampaignDetail": {
        "type": "object",
        "properties": {
          "lineageId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "The campaign's stable id: it stays the same across versions."
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "running",
              "upcoming",
              "draft",
              "ended",
              "withdrawn"
            ],
            "description": "running, upcoming or ended while live; else draft (never published) or withdrawn."
          },
          "flight": {
            "type": "object",
            "properties": {
              "start": {
                "nullable": true,
                "description": "`YYYY-MM-DD` or `YYYY-MM-DDTHH:MM`, local time of each screen; null = open.",
                "type": "string"
              },
              "end": {
                "nullable": true,
                "description": "`YYYY-MM-DD` or `YYYY-MM-DDTHH:MM`, local time of each screen; null = open.",
                "type": "string"
              }
            },
            "required": [
              "start",
              "end"
            ],
            "description": "The live version's flight, else the newest version's."
          },
          "tags": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Tag"
            }
          },
          "mediaIds": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            },
            "description": "The media the live version plays (landscape and portrait), in play order, each once; empty when nothing is live."
          },
          "content": {
            "nullable": true,
            "description": "The live version's content in play order; null when nothing is live.",
            "type": "object",
            "properties": {
              "landscape": {
                "$ref": "#/components/schemas/CampaignContent"
              },
              "portrait": {
                "nullable": true,
                "description": "What portrait screens play instead; null when they play the landscape content.",
                "allOf": [
                  {
                    "$ref": "#/components/schemas/CampaignContent"
                  }
                ]
              }
            },
            "required": [
              "landscape",
              "portrait"
            ]
          }
        },
        "required": [
          "lineageId",
          "name",
          "status",
          "flight",
          "tags",
          "mediaIds",
          "content"
        ]
      },
      "MediaPage": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Media"
            }
          },
          "nextCursor": {
            "nullable": true,
            "description": "Pass as `cursor` for the next page; null on the last page.",
            "type": "string"
          }
        },
        "required": [
          "items",
          "nextCursor"
        ]
      },
      "CampaignPage": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Campaign"
            }
          },
          "nextCursor": {
            "nullable": true,
            "description": "Pass as `cursor` for the next page; null on the last page.",
            "type": "string"
          }
        },
        "required": [
          "items",
          "nextCursor"
        ]
      },
      "TagPage": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Tag"
            }
          },
          "nextCursor": {
            "nullable": true,
            "description": "Pass as `cursor` for the next page; null on the last page.",
            "type": "string"
          }
        },
        "required": [
          "items",
          "nextCursor"
        ]
      },
      "PlaylistPage": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Playlist"
            }
          },
          "nextCursor": {
            "nullable": true,
            "description": "Pass as `cursor` for the next page; null on the last page.",
            "type": "string"
          }
        },
        "required": [
          "items",
          "nextCursor"
        ]
      },
      "PresignedUpload": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "PUT the bytes here within 15 minutes."
          },
          "headers": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Send every one of these headers unchanged: they are part of the signature."
          }
        },
        "required": [
          "url",
          "headers"
        ]
      },
      "UploadRequest": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "Your id for this file, a UUID (v7 is best). Sending the same id again is a safe retry: it never adds a second item."
          },
          "name": {
            "type": "string",
            "maxLength": 200,
            "description": "The name shown in the media library."
          },
          "fileName": {
            "type": "string",
            "maxLength": 500
          },
          "mimeType": {
            "type": "string",
            "description": "image/jpeg, image/png, image/webp, video/mp4 or video/webm."
          },
          "fileSize": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991,
            "description": "Bytes. Images up to 50 MB, videos up to 2 GB."
          },
          "checksum": {
            "type": "string",
            "description": "The file's SHA-256, lower-case hex."
          },
          "duration": {
            "description": "A video's length in seconds.",
            "nullable": true,
            "type": "number",
            "minimum": 0,
            "exclusiveMinimum": true
          },
          "width": {
            "nullable": true,
            "type": "integer",
            "minimum": 0,
            "exclusiveMinimum": true,
            "maximum": 9007199254740991
          },
          "height": {
            "nullable": true,
            "type": "integer",
            "minimum": 0,
            "exclusiveMinimum": true,
            "maximum": 9007199254740991
          },
          "hasThumbnail": {
            "description": "True when you will PUT a JPEG thumbnail (480 px wide is best) too.",
            "type": "boolean"
          },
          "thumbnailSize": {
            "description": "The thumbnail's bytes, at most 1 MB; required with hasThumbnail.",
            "nullable": true,
            "type": "integer",
            "minimum": 0,
            "exclusiveMinimum": true,
            "maximum": 9007199254740991
          },
          "codec": {
            "description": "A video's codec (h264, hevc, …); only used for warnings.",
            "nullable": true,
            "type": "string",
            "maxLength": 50
          }
        },
        "required": [
          "id",
          "name",
          "fileName",
          "mimeType",
          "fileSize",
          "checksum"
        ]
      },
      "UploadTicket": {
        "type": "object",
        "properties": {
          "mediaId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "The item's id: yours, or the existing item's when duplicateOf is set."
          },
          "duplicateOf": {
            "nullable": true,
            "description": "Set when the merchant already has this exact file: nothing to upload or finalize, use mediaId.",
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
              },
              "name": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "image",
                  "video",
                  "web"
                ]
              }
            },
            "required": [
              "id",
              "name",
              "type"
            ]
          },
          "upload": {
            "nullable": true,
            "description": "Where to PUT the file; null for a duplicate.",
            "allOf": [
              {
                "$ref": "#/components/schemas/PresignedUpload"
              }
            ]
          },
          "thumbnailUpload": {
            "nullable": true,
            "description": "Where to PUT the thumbnail; null without one.",
            "allOf": [
              {
                "$ref": "#/components/schemas/PresignedUpload"
              }
            ]
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "code": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                }
              },
              "required": [
                "code",
                "message"
              ]
            },
            "description": "Video compatibility warnings for the merchant's screens. Never blocking."
          }
        },
        "required": [
          "mediaId",
          "duplicateOf",
          "upload",
          "thumbnailUpload",
          "warnings"
        ]
      },
      "AddTagsRequest": {
        "type": "object",
        "properties": {
          "add": {
            "minItems": 1,
            "maxItems": 10,
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Tag names. A name the merchant already has (ignoring case and spacing) reuses that tag; others are created."
          }
        },
        "required": [
          "add"
        ]
      },
      "AddTagsResult": {
        "type": "object",
        "properties": {
          "mediaId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "tags": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Tag"
            },
            "description": "The tags the names resolved to."
          },
          "added": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "How many of them the item did not have yet."
          }
        },
        "required": [
          "mediaId",
          "tags",
          "added"
        ]
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "invalid_request",
              "unauthorized",
              "forbidden",
              "not_found",
              "method_not_allowed",
              "conflict",
              "payload_too_large",
              "unprocessable",
              "rate_limited",
              "internal"
            ]
          },
          "message": {
            "type": "string"
          },
          "code": {
            "description": "Which reason behind the status: for a 409 `storage_quota_exceeded` or `id_conflict`; for a 403 `plan`; for a 422 `invalid`.",
            "type": "string"
          }
        },
        "required": [
          "error",
          "message"
        ]
      }
    }
  }
}
