{
  "openapi": "3.0.1",
  "info": {
    "title": "Sales Layer DAM REST API",
    "version": "2.0.0",
    "description": "The Sales Layer DAM REST API lets external systems work with digital assets managed by Sales Layer: images and non-image files (documents, spreadsheets, archives, and more).\n\nUse this reference when you need to create, list, filter, retrieve, update, or delete image or file records — including registering assets from public URLs, replacing their source content or tags, and removing assets that are no longer required by your catalog workflows.\n\nDAM operations are designed for integrations that keep media libraries, product imagery, product documentation, commerce platforms, supplier portals, and other business systems aligned with Sales Layer. For implementation guidance, review the authentication, pagination, rate limiting, and error handling guides before calling the endpoints below.\n"
  },
  "servers": [
    {
      "url": "https://api2.saleslayer.com/dam",
      "description": "Sales Layer DAM API"
    }
  ],
  "paths": {
    "/files": {
      "get": {
        "tags": [
          "Files"
        ],
        "summary": "Filter Files",
        "description": "Returns a paginated collection of the tenant's non-image files (documents, spreadsheets, archives, …),\r\nfiltered and sorted using OData query options (`$filter`, `$orderby`, `$top`, `$skip`).\r\n            \r\n## Authentication\r\n            \r\nRequires a valid API key in the `X-API-KEY` header.\r\n            \r\n## Response (200 OK)\r\n            \r\nThe page is wrapped in the house collection envelope — the files are under `value`, never at the\r\ntop level:\r\n            \r\n```json\r\n{\r\n  \"value\": [\r\n    {\r\n      \"reference\": \"product-datasheet.pdf\",\r\n      \"numLinks\": 3,\r\n      \"status\": \"Ok\",\r\n      \"modifiedOn\": \"2024-04-05T08:44:33\",\r\n      \"fileType\": \"pdf\",\r\n      \"createdOn\": \"2024-04-05T08:44:33\",\r\n      \"tags\": [\"datasheets\", \"catalog\"],\r\n      \"url\": \"https://cdn.example.com/product-datasheet.pdf\",\r\n      \"sizeInBytes\": 102400\r\n    }\r\n  ],\r\n  \"@count\": 137,\r\n  \"@readLink\": \"https://api2.saleslayer.com/dam/files?$top=50\",\r\n  \"@nextLink\": \"https://api2.saleslayer.com/dam/files?$top=50&$skip=50\"\r\n}\r\n```\r\n            \r\nNotes on the payload:\r\n            \r\n- `status` tells you whether the file is usable yet — see the status values below.\r\n- `url` and `sizeInBytes` come from the stored metadata and are empty / `0` until processing has\r\n  recorded them.\r\n- `tags` is stored as a comma-separated string, so a tag that itself contains a comma comes back\r\n  split into several tags.\r\n- `@count` is the total number of matches, ignoring `$top`/`$skip`.\r\n- `@readLink` is the canonical URL of this collection — this request, echoed back.\r\n- `@nextLink` is the URL of the next page and is omitted on the last page.\r\n            \r\nThe `@`-prefixed hypermedia links (`@readLink`, `@nextLink`) are only emitted when hypermedia\r\nenrichment is configured for the deployment; `@count` is always present. Where the links are\r\nabsent, page forward by incrementing `$skip` yourself until fewer than `$top` items come back.\r\n            \r\n## Supported fields\r\n            \r\nOnly these fields can be used in `$filter` and `$orderby`:\r\n            \r\n`reference`, `numLinks`, `status`, `modifiedOn`, `fileType`, `createdOn`\r\n            \r\nField names are case-insensitive. Every other field — including `tags`, `url` and\r\n`sizeInBytes` — is **not** queryable: naming one in `$filter` or `$orderby` returns 400.\r\n`$orderby` honours only the first sort expression.\r\n            \r\n`$select` and `$expand` are not supported and are ignored.\r\n            \r\n## Supported operators and functions\r\n            \r\n- Comparison: `eq`, `ne`, `gt`, `ge`, `lt`, `le`\r\n- Logical: `and`, `or`, `not`\r\n- Sets: `in`\r\n- Functions: `contains`, `startswith`, `endswith`\r\n            \r\n## Status field values\r\n            \r\n- `Vd` - Void (not yet started processing)\r\n- `Up` - Updating (being uploaded/updated)\r\n- `Ok` - Processed correctly (ready to use)\r\n- `Re` - Reprocessing\r\n- `Er` - Error (processing failed)\r\n- `Dv` - Deleted/void\r\n            \r\nResults are not filtered by status, so files in any of these states can be returned. Filter on\r\n`status` explicitly if you only want usable files.\r\n            \r\n## Examples\r\n            \r\n```\r\nGET /files?$filter=fileType eq 'pdf'\r\nGET /files?$filter=status eq 'Ok'\r\nGET /files?$filter=fileType eq 'csv' and status eq 'Ok'\r\nGET /files?$filter=status in ('Ok', 'Re')\r\nGET /files?$filter=contains(reference, 'datasheet')\r\nGET /files?$filter=startswith(reference, 'product-') and numLinks gt 0\r\nGET /files?$orderby=createdOn desc\r\nGET /files?$filter=fileType ne 'txt'&$orderby=numLinks desc&$skip=0&$top=50\r\n```\r\n            \r\n## Error Responses\r\n            \r\n- **400 Bad Request**: Invalid `$filter` syntax, a field that is not queryable in `$filter` or\r\n  `$orderby`, a negative `$skip`, or a `$top` that is not between 1 and 100\r\n- **401 Unauthorized**: Missing or invalid API key. Returned by the API gateway as\r\n  `{ \"message\": \"Unauthorized\", \"request_id\": \"d8aafa5b8f3e400b60bea0123dd33317\" }`\r\n- **403 Forbidden**: The API key does not have read permissions for this operation. Same body\r\n  shape as the 401\r\n- **500 Internal Server Error**: Technical error (e.g. database unavailable)\r\n            \r\nA 400 uses the shared validation envelope, keyed by the offending parameter:\r\n            \r\n```json\r\n{\r\n  \"validationFailures\": {\r\n    \"$orderby\": [\r\n      {\r\n        \"PropertyName\": \"$orderby\",\r\n        \"ErrorMessage\": \"Ordering by 'tags' is not supported. Supported fields: reference, numlinks, status, modifiedon, filetype, createdon (Parameter 'propertyName')\",\r\n        \"AttemptedValue\": null\r\n      }\r\n    ]\r\n  }\r\n}\r\n```",
        "parameters": [
          {
            "name": "X-API-KEY",
            "in": "header",
            "description": "Tenant's API key (required)",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "$filter",
            "in": "query",
            "description": "OData $filter expression — see the supported fields, operators and functions below",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "$orderby",
            "in": "query",
            "description": "OData $orderby expression — only the fields listed below are supported",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "$top",
            "in": "query",
            "description": "Maximum number of records to return (default: 100, max: 100)",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "$skip",
            "in": "query",
            "description": "Number of records to skip for pagination (default: 0)",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileCollectionRepresentation"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationFailureRepresentation"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnauthorizedRepresentation"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForbiddenRepresentation"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorRepresentation"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Files"
        ],
        "summary": "Create File",
        "description": "Registers a new file from a publicly accessible URL, optionally tagging it. The file is fetched, checked against the supported formats and the size limit, and stored.\r\n            \r\n## Authentication\r\n            \r\nRequires a valid API key in the `X-API-KEY` header.\r\n            \r\n## Request Body\r\n            \r\n```json\r\n{\r\n  \"url\": \"https://example.com/docs/product-datasheet.pdf\",\r\n  \"tags\": [\"datasheets\", \"catalog\", \"summer-2024\"]\r\n}\r\n```\r\n            \r\n### URL Field (Required)\r\n            \r\nThe URL must meet the following requirements:\r\n            \r\n**Valid URL formats:**\r\n- Must be a valid HTTP or HTTPS URL\r\n- Must be absolute (e.g., `https://example.com/manual.pdf`)\r\n- Relative URLs are NOT supported (e.g., `/docs/manual.pdf`)\r\n- The filename is taken from the URL path and becomes the file's `reference`\r\n            \r\n**Examples of valid URLs:**\r\n```\r\nhttps://example.com/product-datasheet.pdf\r\nhttps://cdn.example.com/docs/2024/manual.docx\r\nhttp://assets.example.com/exports/catalog_123.csv\r\n```\r\n            \r\n**Examples of invalid URLs:**\r\n```\r\n/docs/manual.pdf                       (relative URL)\r\nexample.com/manual.pdf                 (missing protocol)\r\nftp://server.com/manual.pdf            (FTP not supported)\r\nhttps://example.com/installer.exe      (extension not accepted)\r\nhttps://example.com/datasheet          (no extension to derive the file type from)\r\n```\r\n            \r\n**URL validation rules:**\r\n- Maximum length: 2048 characters\r\n- The URL must be publicly accessible (no authentication supported)\r\n- The host must resolve to a public address. URLs pointing at loopback, private ranges or the\r\n  cloud metadata endpoint are rejected\r\n- The URL is probed with a `HEAD` request; anything other than a 2xx is rejected\r\n- An address that never answers, or that refuses the connection, is rejected the same way — as an\r\n  address the caller has to correct, not as a technical error\r\n            \r\n### Tags Field (Optional)\r\n            \r\nOptional array of strings to categorize and organize your files.\r\n            \r\n**Rules:**\r\n- Each tag cannot be empty or contain only whitespace\r\n- Tags are case-sensitive\r\n- No tag may contain a comma. A comma is the separator the set is stored with, so a tag carrying\r\n  one would be read back as two; it is rejected rather than accepted and mangled\r\n- There is no explicit limit on the number of tags, but the whole comma-separated list must fit in\r\n  64 KB of storage\r\n            \r\n## Supported File Formats\r\n            \r\nThe file type is taken from the URL's file extension and must be one of:\r\n            \r\n| Category | Extensions |\r\n|----------|------------|\r\n| Documents | `pdf`, `doc`, `docx`, `txt`, `rtf`, `odt` |\r\n| Spreadsheets | `xls`, `xlsx`, `csv`, `ods` |\r\n| Presentations | `ppt`, `pptx`, `odp` |\r\n| Archives | `zip`, `rar`, `7z`, `tar`, `gz`, `tgz` |\r\n| Video and audio | `mp4`, `mov`, `webm`, `m4v`, `mpeg`, `mpg`, `avi`, `mp3`, `wav` |\r\n| Images | `jpg`, `jpeg`, `png`, `gif`, `bmp`, `tif`, `tiff`, `psd`, `webp`, `avif`, `heic` |\r\n| Markup and data | `svg`, `html`, `htm`, `xml`, `json` |\r\n| Print production | `ai`, `eps`, `indd`, `idml`, `idms` |\r\n| CAD, BIM and 3D | `dwg`, `dxf`, `step`, `stp`, `iges`, `igs`, `stl`, `sat`, `obj`, `mtl`, `fbx`, `3ds`, `max`, `skp`, `glb`, `gltf`, `usdz`, `dae`, `3dm`, `x3d`, `ifc`, `rfa`, `rvt`, `iam`, `ipt`, `idw` |\r\n| Photometry and product data | `ldt`, `ies`, `uld`, `gll`, `gdtf`, `bc3`, `bom`, `sup` |\r\n| Fonts | `otf`, `ttf`, `woff`, `woff2` |\r\n            \r\nThe extension is matched case-insensitively and decides which format the file is filed as. The\r\n`Content-Type` the URL serves is **not** validated, unlike `POST /images`: several of the accepted\r\nformats have no registered media type and are commonly served as `application/octet-stream`, so a\r\nmedia-type allow-list would reject valid uploads without excluding anything.\r\n            \r\n## File Size Limit\r\n            \r\n- **Maximum file size:** 250 MB (262,144,000 bytes)\r\n- The transfer is cut off as soon as the limit is exceeded, so an oversized file is never stored\r\n            \r\n## Response\r\n            \r\n### Success Response (201 Created)\r\n            \r\nReturns the reference (filename) of the created file, wrapped in the house entity envelope. The\r\nreference is the public, tenant-unique key; the numeric id is internal only and is not exposed:\r\n            \r\n```json\r\n{\r\n  \"value\": {\r\n    \"reference\": \"product-datasheet.pdf\"\r\n  },\r\n  \"@readLink\": \"https://api2.saleslayer.com/dam/files(product-datasheet.pdf)\",\r\n  \"@editLink\": \"https://api2.saleslayer.com/dam/files(product-datasheet.pdf)\"\r\n}\r\n```\r\n            \r\n## Error Responses\r\n            \r\n### 400 Bad Request\r\n            \r\nValidation failures are returned in the shared envelope. Service-level messages carry no field\r\ncontext, so they are grouped under a generic `error` key:\r\n            \r\n```json\r\n{\r\n  \"validationFailures\": {\r\n    \"error\": [\"URL is required\"]\r\n  }\r\n}\r\n```\r\n            \r\nThe messages you can receive, verbatim:\r\n            \r\n| Cause | Message |\r\n|-------|---------|\r\n| Missing or empty URL | `URL is required` |\r\n| URL over 2048 characters | `URL exceeds maximum length of 2048 characters` |\r\n| Not an absolute URL | `URL is not valid` |\r\n| Scheme other than HTTP/HTTPS | `URL must use HTTP or HTTPS protocol` |\r\n| No extension in the URL path | `Could not determine file type from URL. Please ensure the URL contains a filename with an extension.` |\r\n| Unsupported extension | `File extension 'exe' is not allowed. Allowed extensions: 3dm, 3ds, 7z, ai, avi, ...` — the whole accepted set, alphabetically |\r\n| Host is not publicly routable | `URL host 'localhost' is not allowed.` |\r\n| URL not reachable (non-2xx `HEAD`) | `URL is not accessible. HTTP status: 404 Not Found` |\r\n| URL did not answer in time | `URL is not accessible. The address did not respond in time.` |\r\n| Connection refused, DNS or TLS failure | `URL is not accessible. The address 'cdn.example.com' could not be reached.` |\r\n| Empty file | `The uploaded file is empty` |\r\n| Stored copy missing | `The uploaded file could not be found in storage.` |\r\n| File too large | `File size (300.00 MB) exceeds maximum allowed size of 250 MB` |\r\n| Content is a different file type | `The address serves a PNG image, but the file name 'product-datasheet.pdf' announces a 'pdf' file.` |\r\n| Content matches no known file type | `The address does not serve a 'pdf' file: its content matches no known file type.` |\r\n| Empty or whitespace-only tag | `Tags cannot contain empty values` |\r\n| Tag containing a comma | `Tags cannot contain commas` |\r\n            \r\n### 401 Unauthorized\r\n            \r\nMissing or invalid API key in the `X-API-KEY` header. Emitted by the API gateway:\r\n            \r\n```json\r\n{\r\n  \"message\": \"Unauthorized\",\r\n  \"request_id\": \"d8aafa5b8f3e400b60bea0123dd33317\"\r\n}\r\n```\r\n            \r\n### 403 Forbidden\r\n            \r\nThe API key does not have write permissions for this operation. Same body shape as the 401.\r\n            \r\n### 409 Conflict\r\n            \r\nTwo distinct causes, both using the conflict envelope:\r\n            \r\n**A file with the same filename (`reference`) already exists for this tenant:**\r\n            \r\n```json\r\n{\r\n  \"error\": \"A file with the filename 'product-datasheet.pdf' already exists\"\r\n}\r\n```\r\n            \r\n**The tenant has reached its file-library quota:**\r\n            \r\n```json\r\n{\r\n  \"error\": \"File library limit reached. Current: 500, Maximum: 500\"\r\n}\r\n```\r\n            \r\nDeleted files do not count towards the quota. Delete unused files to free\r\ncapacity, or contact Sales Layer to raise the limit. The file and image libraries have separate\r\nquotas, so files never consume image capacity or vice versa.\r\n            \r\n            \r\n## Complete Example\r\n            \r\n**Request:**\r\n```http\r\nPOST /files HTTP/1.1\r\nHost: api2.saleslayer.com\r\nX-API-KEY: your-api-key-here\r\nContent-Type: application/json\r\n            \r\n{\r\n  \"url\": \"https://cdn.example.com/docs/2024/product-datasheet.pdf\",\r\n  \"tags\": [\"datasheets\", \"catalog\"]\r\n}\r\n```\r\n            \r\n**Success Response:**\r\n```http\r\nHTTP/1.1 201 Created\r\nContent-Type: application/json\r\nLocation: https://api2.saleslayer.com/dam/files(product-datasheet.pdf)\r\n            \r\n{\r\n  \"value\": {\r\n    \"reference\": \"product-datasheet.pdf\"\r\n  }\r\n}\r\n```\r\n            \r\n**Result:**\r\n```json\r\n{\r\n  \"value\": {\r\n    \"reference\": \"product-datasheet.pdf\",\r\n    \"status\": \"Ok\",\r\n    \"fileType\": \"pdf\",\r\n    \"sizeInBytes\": 524288,\r\n    \"numLinks\": 0,\r\n    \"tags\": [\"datasheets\", \"catalog\"],\r\n    \"url\": \"https://cdn.example.com/CMP2009/files/product-datasheet.pdf\",\r\n    \"createdOn\": \"2024-04-02T10:30:00\",\r\n    \"modifiedOn\": \"2024-04-02T10:30:00\"\r\n  }\r\n}\r\n```",
        "parameters": [
          {
            "name": "X-API-KEY",
            "in": "header",
            "description": "Tenant's API key (required)",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "Request containing the file URL and optional tags",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateFileRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateFileRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CreateFileRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedFileRepresentation"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationFailureRepresentation"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnauthorizedRepresentation"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForbiddenRepresentation"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConflictRepresentation"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorRepresentation"
                }
              }
            }
          }
        }
      }
    },
    "/files({reference})": {
      "get": {
        "tags": [
          "Files"
        ],
        "summary": "Get File",
        "description": "In DAM, files are identified by their filename (`reference`), which is unique per tenant.\r\nUse this endpoint to retrieve a specific file when you already know its filename.\r\n            \r\n## Authentication\r\n            \r\nRequires a valid API key in the `X-API-KEY` header.\r\n            \r\n## Example\r\n            \r\n```\r\nGET /files(product-datasheet.pdf)\r\nX-API-KEY: your-api-key-here\r\n```\r\n            \r\n## Response (200 OK)\r\n            \r\nThe file is wrapped in the house entity envelope — its fields are under `value`, never at the\r\ntop level:\r\n            \r\n```json\r\n{\r\n  \"value\": {\r\n    \"reference\": \"product-datasheet.pdf\",\r\n    \"numLinks\": 3,\r\n    \"status\": \"Ok\",\r\n    \"modifiedOn\": \"2024-04-05T08:44:33\",\r\n    \"fileType\": \"pdf\",\r\n    \"createdOn\": \"2024-04-05T08:44:33\",\r\n    \"tags\": [\"datasheets\", \"catalog\"],\r\n    \"url\": \"https://cdn.example.com/product-datasheet.pdf\",\r\n    \"sizeInBytes\": 102400\r\n  },\r\n  \"@readLink\": \"https://api2.saleslayer.com/dam/files(product-datasheet.pdf)\",\r\n  \"@editLink\": \"https://api2.saleslayer.com/dam/files(product-datasheet.pdf)\"\r\n}\r\n```\r\n            \r\nNotes on the payload:\r\n            \r\n- `status` tells you whether the file is usable yet — see the status values documented on\r\n  `GET /files`.\r\n- `url` and `sizeInBytes` come from the stored metadata and are empty / `0` until processing has\r\n  recorded them.\r\n- `tags` is stored as a comma-separated string, so a tag that itself contains a comma comes back\r\n  split into several tags.\r\n- `@readLink` and `@editLink` are the canonical URL of this file (this request, echoed back), and\r\n  are only emitted when hypermedia enrichment is configured for the deployment.\r\n            \r\n## Error Responses\r\n            \r\n- **401 Unauthorized**: Missing or invalid API key. Returned by the API gateway as\r\n  `{ \"message\": \"Unauthorized\", \"request_id\": \"d8aafa5b8f3e400b60bea0123dd33317\" }`\r\n- **403 Forbidden**: The API key does not have read permissions for this operation. Same body\r\n  shape as the 401\r\n- **404 Not Found**: No file with the given reference exists for this tenant. The body is the\r\n  message string, e.g. `\"File with reference 'missing.pdf' was not found.\"`\r\n- **500 Internal Server Error**: Technical error (e.g. database unavailable)",
        "parameters": [
          {
            "name": "X-API-KEY",
            "in": "header",
            "description": "Tenant's API key (required)",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "reference",
            "in": "path",
            "description": "File name, unique per tenant (e.g. `product-datasheet.pdf`)",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileEntityRepresentation"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnauthorizedRepresentation"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForbiddenRepresentation"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorRepresentation"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Files"
        ],
        "summary": "Patch File",
        "description": "Partially updates an existing file identified by its filename (`reference`), which is unique per tenant — substitute its contents via a new `url` and/or replace its `tags`. At least one of these fields must be supplied.\r\n            \r\nThe replacement `url` can point at a file with any name — the contents are **always stored under the\r\nfile's current `reference`**, because that filename is how the rest of the platform points at the file\r\nand a substitution cannot rename it. What the replacement *cannot* change is the **file type**, or the\r\nfile would end up served under a name that misdescribes it.\r\n            \r\nThat type is decided by **the content of the file**, not by the extension in the `url`: the extension\r\nis yours to write and any file can be published under any name, so it proves nothing. The extension\r\nstill has to be one `POST /files` accepts, but the bytes are what settle the type. `POST /files`\r\nreads the bytes the same way; there the type they are held to is the one the new file name announces,\r\nhere it is the type the file already has.\r\n            \r\n```\r\nPATCH /files(product-datasheet.pdf)\r\n{ \"url\": \"https://cdn.example.com/docs/v2/product-datasheet.pdf\" }   OK   same name, PDF content\r\n{ \"url\": \"https://cdn.example.com/exports/9f21b0-final.pdf\" }        OK   any name, PDF content\r\n{ \"url\": \"https://cdn.example.com/docs/product-datasheet.docx\" }     400  content is not a PDF\r\n{ \"url\": \"https://cdn.example.com/docs/product-datasheet.pdf\" }      400  named .pdf, but not a PDF\r\n```\r\n            \r\n### Changing a file's type\r\n            \r\nNot possible through a substitution, because the filename — and so the extension — is fixed. Do it\r\nclient-side in three steps:\r\n            \r\n1. **Create the new file** — `POST /files` with the new file's `url`.\r\n2. **Repoint the references** — use the Catalog API to find every entity (product, variant, category, …)\r\n   that uses the old filename and update it to the new one.\r\n3. **Delete the old file** — `DELETE /files({old-reference})` once nothing references it any more.\r\n            \r\n## Fields\r\n            \r\n- **url** (optional): public URL of the new file. Triggers a full substitution. May be called\r\n  anything, but must serve a file of the current file type — see above. The address itself is\r\n  validated exactly as on `POST /files`, so only the extensions that endpoint accepts are allowed\r\n  and the served `Content-Type` is not inspected.\r\n- **tags** (optional): array of strings, validated exactly as on `POST /files`.\r\n  - Absent → tags are not modified, **including when `url` is supplied**.\r\n  - `[]` → all tags are cleared.\r\n  - `[\"a\",\"b\"]` → tags are replaced with these values.\r\n            \r\n## Valid combinations\r\n            \r\n| Body | Operation |\r\n|------|-----------|\r\n| `{ \"url\": \"https://.../{any-filename}\" }` | Substitute file contents — **tags are kept** |\r\n| `{ \"url\": \"https://.../{any-filename}\", \"tags\": [\"a\"] }` | Substitute contents and set tags |\r\n| `{ \"tags\": [\"a\"] }` | Update tags only, file contents unchanged |\r\n            \r\n## Invalid combinations\r\n            \r\n- Empty body `{}` → 400.\r\n- `url` serving content that is not of the current file type → 400 (see above).\r\n            \r\n## Response (204 No Content)\r\n            \r\nSuccess returns no body.\r\n            \r\n## Notes\r\n            \r\n- **The replacement is live as soon as this returns.**\r\n- A substitution does not change the file's `reference` or `fileType`, so entities referencing it\r\n  keep working.\r\n- Unlike the image endpoint this operation has no 409 conflict: only images are queued for\r\n  background processing, so a file is never locked by an in-flight process.\r\n- Tags behave as standard PATCH fields — absent means unchanged, exactly as on\r\n  `PATCH /images({reference})`.\r\n            \r\n## Error Responses\r\n            \r\n- **400 Bad Request**: Invalid request body (e.g., both fields missing), a replacement whose content\r\n  is not of the current `fileType`, or any of the `url`/`tags` validation failures documented on\r\n  `POST /files` (format, accessibility, extension, size). Same\r\n  `{ \"validationFailures\": { \"error\": [...] } }` envelope\r\n- **401 Unauthorized**: Missing or invalid API key. Returned by the API gateway as\r\n  `{ \"message\": \"Unauthorized\", \"request_id\": \"d8aafa5b8f3e400b60bea0123dd33317\" }`\r\n- **403 Forbidden**: The API key does not have write permissions for this operation. Same body\r\n  shape as the 401\r\n- **404 Not Found**: No file with the given reference exists for this tenant. The body is the\r\n  message string, e.g. `\"File with reference 'missing.pdf' not found\"`\r\n- **500 Internal Server Error**: Technical error (network failure fetching the replacement file,\r\n  storage or database unavailable). The already-uploaded replacement object is cleaned up, so the\r\n  file is left on its previous contents",
        "parameters": [
          {
            "name": "X-API-KEY",
            "in": "header",
            "description": "Tenant's API key (required)",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "reference",
            "in": "path",
            "description": "File name (reference), unique per tenant (e.g. `product-datasheet.pdf`)",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "Fields to update. At least one of 'url' or 'tags' must be present.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PatchFileRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PatchFileRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PatchFileRequest"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "No Content"
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationFailureRepresentation"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnauthorizedRepresentation"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForbiddenRepresentation"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorRepresentation"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Files"
        ],
        "summary": "Delete File",
        "description": "Deletes a file by its filename (`reference`), which is unique per tenant.\r\n            \r\n## Authentication\r\n            \r\nRequires a valid API key in the `X-API-KEY` header.\r\n            \r\n## Example\r\n            \r\n```\r\nDELETE /files(product-datasheet.pdf)\r\nX-API-KEY: your-api-key-here\r\n```\r\n            \r\n## Response (204 No Content)\r\n            \r\nSuccess returns no body.\r\n            \r\n## Important: deletion does not cascade\r\n            \r\nThe file record is removed outright. The deletion is **not** propagated to the entities that use\r\nthe file — check `numLinks` on the file first (`GET /files({reference})`): if it is greater than\r\nzero, the products, variants and categories still referencing the filename will be left pointing\r\nat a file that no longer exists. Repoint them via the Catalog API before deleting.\r\n            \r\n## Error Responses\r\n            \r\n- **401 Unauthorized**: Missing or invalid API key. Returned by the API gateway as\r\n  `{ \"message\": \"Unauthorized\", \"request_id\": \"d8aafa5b8f3e400b60bea0123dd33317\" }`\r\n- **403 Forbidden**: The API key does not have write permissions for this operation. Same body\r\n  shape as the 401\r\n- **404 Not Found**: No file with the given reference exists for this tenant. The body is the\r\n  message string, e.g. `\"File with reference 'missing.pdf' not found\"`\r\n- **500 Internal Server Error**: Technical error (e.g. database unavailable)\r\n            \r\nUnlike `DELETE /images({reference})` this operation has no 409 conflict: only images are queued\r\nfor background processing, so a file is never locked by an in-flight process.",
        "parameters": [
          {
            "name": "X-API-KEY",
            "in": "header",
            "description": "Tenant's API key (required)",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "reference",
            "in": "path",
            "description": "File name (reference), unique per tenant",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "No Content"
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnauthorizedRepresentation"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForbiddenRepresentation"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorRepresentation"
                }
              }
            }
          }
        }
      }
    },
    "/images": {
      "get": {
        "tags": [
          "Images"
        ],
        "summary": "Filter Images",
        "description": "Returns a paginated collection of the tenant's images, filtered and sorted using OData query options (`$filter`, `$orderby`, `$top`, `$skip`).\r\n            \r\n## Authentication\r\n            \r\nRequires a valid API key in the `X-API-KEY` header.\r\n            \r\n## Response (200 OK)\r\n            \r\nThe page is wrapped in the house collection envelope — the images are under `value`, never at the\r\ntop level:\r\n            \r\n```json\r\n{\r\n  \"value\": [\r\n    {\r\n      \"id\": 42,\r\n      \"reference\": \"product-main-front.jpg\",\r\n      \"numLinks\": 3,\r\n      \"status\": \"Ok\",\r\n      \"modifiedOn\": \"2024-04-05T08:44:33\",\r\n      \"fileType\": \"jpg\",\r\n      \"createdOn\": \"2024-04-05T08:44:33\",\r\n      \"tags\": [\"animales\", \"mascotas\"],\r\n      \"originalUrl\": \"https://cdn.example.com/product-main-front.jpg\",\r\n      \"thumbnailUrl\": \"https://cdn.example.com/product-main-front_TH.jpg\",\r\n      \"thumbnailMediumUrl\": \"https://cdn.example.com/product-main-front_THM.jpg\",\r\n      \"thumbnailPreviewUrl\": \"https://cdn.example.com/product-main-front_THP.jpg\",\r\n      \"width\": 800,\r\n      \"height\": 600,\r\n      \"sizeInBytes\": 102400\r\n    }\r\n  ],\r\n  \"@count\": 137,\r\n  \"@readLink\": \"https://api2.saleslayer.com/dam/images?$top=50\",\r\n  \"@nextLink\": \"https://api2.saleslayer.com/dam/images?$top=50&$skip=50\"\r\n}\r\n```\r\n            \r\nNotes on the payload:\r\n            \r\n- `status` tells you whether the image is usable yet — see the status values documented on\r\n  `GET /images`. A freshly created or substituted image is `Up` until the worker finishes.\r\n- `width`, `height` and `sizeInBytes` come from the stored metadata and are `0` until processing\r\n  has recorded them; the thumbnail URLs are empty until the renditions exist.\r\n- `tags` is stored as a comma-separated string, so a tag that itself contains a comma comes back\r\n  split into several tags.\r\n- `@count` is the total number of matches, ignoring `$top`/`$skip`.\r\n- `@readLink` is the canonical URL of this collection — this request, echoed back.\r\n- `@nextLink` is the URL of the next page and is omitted on the last page.\r\n            \r\nThe `@`-prefixed hypermedia links (`@readLink`, `@nextLink`) are only emitted when hypermedia\r\nenrichment is configured for the deployment; `@count` is always present. Where the links are\r\nabsent, page forward by incrementing `$skip` yourself until fewer than `$top` items come back.\r\n            \r\n## Supported fields\r\n            \r\nOnly these fields can be used in `$filter` and `$orderby`:\r\n            \r\n`id`, `reference`, `numLinks`, `status`, `modifiedOn`, `fileType`, `createdOn`\r\n            \r\nField names are case-insensitive. Every other field of the response — including `tags`,\r\n`width`, `height`, `sizeInBytes` and the URL fields — is **not** queryable: naming one in\r\n`$filter` or `$orderby` returns 400. `$orderby` honours only the first sort expression.\r\n            \r\n`$select` and `$expand` are not supported and are ignored.\r\n            \r\n## Supported operators and functions\r\n            \r\n- Comparison: `eq`, `ne`, `gt`, `ge`, `lt`, `le`\r\n- Logical: `and`, `or`, `not`\r\n- Sets: `in`\r\n- Functions: `contains`, `startswith`, `endswith`\r\n            \r\n## Status field values\r\n            \r\n- `Vd` - Void (not yet started processing)\r\n- `Up` - Updating (being uploaded/updated)\r\n- `Ok` - Processed correctly (ready to use)\r\n- `Re` - Reprocessing\r\n- `Er` - Error (processing failed)\r\n- `Dv` - Deleted/void (no longer counts towards the tenant's image-library quota)\r\n            \r\nResults are not filtered by status, so images in any of these states can be returned. Filter on\r\n`status` explicitly if you only want usable images.\r\n            \r\n## Examples\r\n            \r\n```\r\nGET /images?$filter=fileType eq 'jpg'\r\nGET /images?$filter=status eq 'Ok'\r\nGET /images?$filter=fileType eq 'png' and status eq 'Ok'\r\nGET /images?$filter=status in ('Ok', 'Re')\r\nGET /images?$filter=contains(reference, 'product')\r\nGET /images?$filter=startswith(reference, 'product-') and numLinks gt 0\r\nGET /images?$orderby=createdOn desc\r\nGET /images?$filter=fileType ne 'gif'&$orderby=numLinks desc&$skip=0&$top=50\r\n```\r\n            \r\n## Error Responses\r\n            \r\n- **400 Bad Request**: Invalid `$filter` syntax, a field that is not queryable in `$filter` or\r\n  `$orderby`, a negative `$skip`, or a `$top` that is not between 1 and 100\r\n- **401 Unauthorized**: Missing or invalid API key. Returned by the API gateway as\r\n  `{ \"message\": \"Unauthorized\", \"request_id\": \"d8aafa5b8f3e400b60bea0123dd33317\" }`\r\n- **403 Forbidden**: The API key does not have read permissions for this operation. Same body\r\n  shape as the 401\r\n- **500 Internal Server Error**: Technical error (e.g. database unavailable)\r\n            \r\nA 400 uses the shared validation envelope, keyed by the offending parameter:\r\n            \r\n```json\r\n{\r\n  \"validationFailures\": {\r\n    \"$orderby\": [\r\n      {\r\n        \"PropertyName\": \"$orderby\",\r\n        \"ErrorMessage\": \"Ordering by 'tags' is not supported. Supported fields: id, reference, numlinks, status, modifiedon, filetype, createdon (Parameter 'propertyName')\",\r\n        \"AttemptedValue\": null\r\n      }\r\n    ]\r\n  }\r\n}\r\n```",
        "parameters": [
          {
            "name": "X-API-KEY",
            "in": "header",
            "description": "Tenant's API key (required)",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "$filter",
            "in": "query",
            "description": "OData $filter expression — see the supported fields, operators and functions below",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "$orderby",
            "in": "query",
            "description": "OData $orderby expression — only the fields listed below are supported",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "$top",
            "in": "query",
            "description": "Maximum number of records to return (default: 100, max: 100)",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "$skip",
            "in": "query",
            "description": "Number of records to skip for pagination (default: 0)",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImageCollectionRepresentation"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationFailureRepresentation"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnauthorizedRepresentation"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForbiddenRepresentation"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorRepresentation"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Images"
        ],
        "summary": "Create Image",
        "description": "Registers a new image from a publicly accessible URL, optionally tagging it. The image is downloaded, validated against the supported formats and size limits, and processed asynchronously in the background.\r\n            \r\n## Authentication\r\n            \r\nRequires a valid API key in the `X-API-KEY` header.\r\n            \r\n## Request Body\r\n            \r\n```json\r\n{\r\n  \"url\": \"https://example.com/images/product.jpg\",\r\n  \"tags\": [\"ecommerce\", \"catalog\", \"summer-2024\"]\r\n}\r\n```\r\n            \r\n### URL Field (Required)\r\n            \r\nThe URL must meet the following requirements:\r\n            \r\n**Valid URL formats:**\r\n- Must be a valid HTTP or HTTPS URL\r\n- Must be absolute (e.g., `https://example.com/image.jpg`)\r\n- Relative URLs are NOT supported (e.g., `/images/photo.jpg`)\r\n- The filename will be automatically extracted from the URL path\r\n            \r\n**Examples of valid URLs:**\r\n```\r\nhttps://example.com/product.jpg\r\nhttps://cdn.example.com/images/2024/photo.png\r\nhttp://assets.example.com/catalog/item_123.gif\r\n```\r\n            \r\n**Examples of invalid URLs:**\r\n```\r\n/images/product.jpg                    (relative URL)\r\nexample.com/image.jpg                   (missing protocol)\r\nftp://server.com/file.jpg              (FTP not supported)\r\nhttps://example.com/page.html          (not a supported image extension)\r\nhttps://example.com/product            (no extension to derive the file type from)\r\n```\r\n            \r\n**URL validation rules:**\r\n- Maximum length: 2048 characters\r\n- The URL must be publicly accessible (no authentication supported)\r\n- The host must resolve to a public address. URLs pointing at loopback, private ranges or the\r\n  cloud metadata endpoint are rejected\r\n- The URL is probed with a `HEAD` request; anything other than a 2xx is rejected\r\n            \r\n### Tags Field (Optional)\r\n            \r\nOptional array of strings to categorize and organize your images.\r\n            \r\n**Rules:**\r\n- Each tag cannot be empty or contain only whitespace\r\n- Tags are case-sensitive\r\n- Tags are stored as a single comma-separated string, so a tag containing a comma is read back as\r\n  several tags. Avoid commas inside a tag\r\n- There is no explicit limit on the number of tags, but the whole comma-separated list must fit in\r\n  64 KB of storage\r\n            \r\n**Example:**\r\n```json\r\n{\r\n  \"url\": \"https://example.com/logo.png\",\r\n  \"tags\": [\"branding\", \"corporate\", \"logo\", \"2024\"]\r\n}\r\n```\r\n            \r\n## Supported Image Formats\r\n            \r\nThe file type is taken from the URL's file extension, and the `Content-Type` header returned by the\r\nURL is validated independently against the list of accepted image media types:\r\n            \r\n### Fully Supported Formats\r\n            \r\n| Format | Extension | Accepted Content-Type | Notes |\r\n|--------|-----------|-----------------------|-------|\r\n| JPEG | .jpg, .jpeg | `image/jpeg` | Standard and progressive JPEG |\r\n| PNG | .png | `image/png` | PNG-8, PNG-24, PNG-32 with transparency |\r\n| GIF | .gif | `image/gif` | Static and animated GIF |\r\n| TIFF | .tif, .tiff | `image/tiff`, `image/x-tiff` | Single and multi-page TIFF |\r\n| WebP | .webp | `image/webp` | Lossy and lossless compression |\r\n| PSD | .psd | `image/vnd.adobe.photoshop`, `image/photoshop`, `image/psd`, `application/octet-stream` | Adobe Photoshop files |\r\n            \r\n### Content-Type Validation\r\n            \r\n1. The URL **must return** a `Content-Type` header\r\n2. The Content-Type **must be** one of the accepted media types listed above\r\n3. `application/octet-stream` is **only accepted for .psd files** — many servers are misconfigured\r\n   and return this generic type for Photoshop files\r\n4. Apart from that `application/octet-stream` rule, the Content-Type is **not** cross-checked\r\n   against the file extension: a URL ending in `.jpg` that serves `image/png` is accepted, and the\r\n   image is registered with file type `jpg`\r\n            \r\n**Examples of valid Content-Type responses:**\r\n```\r\nContent-Type: image/jpeg          → ✅ Accepted\r\nContent-Type: image/png           → ✅ Accepted\r\nContent-Type: image/gif           → ✅ Accepted\r\nContent-Type: application/octet-stream with .psd extension → ✅ Accepted\r\n```\r\n            \r\n**Examples of invalid Content-Type responses:**\r\n```\r\nContent-Type: text/html           → ❌ Rejected (not an image)\r\nContent-Type: application/pdf     → ❌ Rejected (not supported)\r\nContent-Type: application/octet-stream with .jpg extension → ❌ Rejected\r\n(no Content-Type header)          → ❌ Rejected (missing header)\r\n```\r\n            \r\n## Image Size and Weight Limits\r\n            \r\nThe system automatically validates downloaded images:\r\n            \r\n### File Size Limits\r\n- **Maximum file size:** 250 MB (262,144,000 bytes)\r\n- Files larger than this limit will be rejected with a validation error\r\n            \r\n### Dimension Limits\r\n- **Maximum width:** 30,000 pixels\r\n- **Maximum height:** 30,000 pixels\r\n- Images exceeding these dimensions will be rejected\r\n            \r\n## Response\r\n            \r\n### Success Response (201 Created)\r\n            \r\nReturns the reference (filename) of the created image, wrapped in the house entity envelope. The\r\nreference is the public, tenant-unique key.\r\n            \r\n```json\r\n{\r\n  \"value\": {\r\n    \"reference\": \"product-123.jpg\"\r\n  },\r\n  \"@readLink\": \"https://api2.saleslayer.com/dam/images(product-123.jpg)\",\r\n  \"@editLink\": \"https://api2.saleslayer.com/dam/images(product-123.jpg)\"\r\n}\r\n```\r\n            \r\nA `Location` header points at the new resource, as an absolute URL —\r\n`https://api2.saleslayer.com/dam/images(product-123.jpg)`.\r\n            \r\nThe `@readLink`/`@editLink` annotations address the created image, matching the `Location` header,\r\nso either can be followed to read or modify it. They are only emitted when hypermedia enrichment\r\nis configured for the deployment; the `Location` header is always present.\r\n            \r\nThe image will be processed asynchronously in the background: it is created with status `Up` and\r\nthe worker moves it to `Ok` or `Er`, filling in the dimensions and thumbnail URLs.\r\n            \r\nTo check the processing status and retrieve full image details:\r\n```\r\nGET /images(product-123.jpg)\r\n```\r\n            \r\n## Error Responses\r\n            \r\n### 400 Bad Request\r\n            \r\nValidation failures are returned in the shared envelope. Service-level messages carry no field\r\ncontext, so they are grouped under a generic `error` key:\r\n            \r\n```json\r\n{\r\n  \"validationFailures\": {\r\n    \"error\": [\"URL is required\"]\r\n  }\r\n}\r\n```\r\n            \r\nThe messages you can receive, verbatim:\r\n            \r\n| Cause | Message |\r\n|-------|---------|\r\n| Missing or empty URL | `URL is required` |\r\n| URL over 2048 characters | `URL exceeds maximum length of 2048 characters` |\r\n| Not an absolute URL | `URL is not valid` |\r\n| Scheme other than HTTP/HTTPS | `URL must use HTTP or HTTPS protocol` |\r\n| No extension in the URL path | `Could not determine file type from URL. Please ensure the URL contains a filename with an extension.` |\r\n| Unsupported extension | `File extension 'pdf' is not allowed. Allowed extensions: jpg, jpeg, png, gif, tif, tiff, psd, webp` |\r\n| Host is not publicly routable | `URL host 'localhost' is not allowed.` |\r\n| URL not reachable (non-2xx `HEAD`) | `URL is not accessible. HTTP status: 404 Not Found` |\r\n| No `Content-Type` header | `URL does not return a valid Content-Type header` |\r\n| `application/octet-stream` for a non-PSD file | `URL returns generic Content-Type (application/octet-stream) for non-PSD file. Extension detected: jpg` |\r\n| Unsupported Content-Type | `URL does not return an image Content-Type. Received: text/html. Allowed types: image/jpeg, image/png, ...` |\r\n| Empty file | `The uploaded file is empty` |\r\n| File too large | `Image file size (300.00 MB) exceeds maximum allowed size of 250 MB` |\r\n| Dimensions unreadable | `Could not read image dimensions from the uploaded file. Ensure it is a valid image in a supported format.` |\r\n| Image too wide | `Image width (35000px) exceeds maximum allowed width of 30000px` |\r\n| Image too tall | `Image height (35000px) exceeds maximum allowed height of 30000px` |\r\n| Empty or whitespace-only tag | `Tags cannot contain empty values` |\r\n            \r\n### 401 Unauthorized\r\n            \r\nMissing or invalid API key in the `X-API-KEY` header. Emitted by the API gateway:\r\n            \r\n```json\r\n{\r\n  \"message\": \"Unauthorized\",\r\n  \"request_id\": \"d8aafa5b8f3e400b60bea0123dd33317\"\r\n}\r\n```\r\n            \r\nQuote `request_id` when contacting support about a rejected request.\r\n            \r\n### 403 Forbidden\r\n            \r\nThe API key does not have write permissions for this operation. Same body shape as the 401.\r\n            \r\n### 409 Conflict\r\n            \r\nTwo distinct causes, both using the conflict envelope:\r\n            \r\n**An image with the same filename (`reference`) already exists for this tenant:**\r\n            \r\n```json\r\n{\r\n  \"error\": \"An image with the filename 'product-123.jpg' already exists\"\r\n}\r\n```\r\n            \r\n**The tenant has reached its image-library quota:**\r\n            \r\n```json\r\n{\r\n  \"error\": \"Image library limit reached. Current: 5000, Maximum: 5000\"\r\n}\r\n```\r\n            \r\nImages in status `Dv` (deleted) do not count towards the quota. Delete unused images to free\r\ncapacity, or contact Sales Layer to raise the limit.\r\n            \r\n### 500 Internal Server Error\r\n            \r\nTechnical errors such as:\r\n- Network connectivity issues reaching the source URL, or a download timeout (30s)\r\n- Storage service unavailable\r\n- Database errors\r\n            \r\nNote that a source URL that responds with a non-2xx status is a 400, not a 500 — only transport\r\nfailures and timeouts land here. The body identifies the request for support:\r\n            \r\n```json\r\n{\r\n  \"error\": \"Please contact the administrator of this application by supplying the following code: 0HNF1A2B3C4D5\"\r\n}\r\n```\r\n            \r\n## Complete Example\r\n            \r\n**Request:**\r\n```http\r\nPOST /images HTTP/1.1\r\nHost: api2.saleslayer.com\r\nX-API-KEY: your-api-key-here\r\nContent-Type: application/json\r\n            \r\n{\r\n  \"url\": \"https://cdn.example.com/products/summer-2024/product-123.jpg\",\r\n  \"tags\": [\"ecommerce\", \"summer-collection\", \"featured\", \"new-arrival\"]\r\n}\r\n```\r\n            \r\n**Success Response:**\r\n```http\r\nHTTP/1.1 201 Created\r\nContent-Type: application/json\r\nLocation: https://api2.saleslayer.com/dam/images(product-123.jpg)\r\n            \r\n{\r\n  \"value\": {\r\n    \"reference\": \"product-123.jpg\"\r\n  }\r\n}\r\n```\r\n            \r\n**Checking the result:**\r\n```http\r\nGET /images(product-123.jpg) HTTP/1.1\r\nHost: api2.saleslayer.com\r\nX-API-KEY: your-api-key-here\r\n```\r\n            \r\n**Result after processing:**\r\n```json\r\n{\r\n  \"value\": {\r\n    \"id\": 42,\r\n    \"reference\": \"product-123.jpg\",\r\n    \"status\": \"Ok\",\r\n    \"fileType\": \"jpg\",\r\n    \"width\": 1920,\r\n    \"height\": 1080,\r\n    \"sizeInBytes\": 245760,\r\n    \"numLinks\": 5,\r\n    \"tags\": [\"ecommerce\", \"summer-collection\", \"featured\", \"new-arrival\"],\r\n    \"originalUrl\": \"https://cdn.example.com/product-123.jpg\",\r\n    \"thumbnailUrl\": \"https://cdn.example.com/product-123_TH.jpg\",\r\n    \"thumbnailMediumUrl\": \"https://cdn.example.com/product-123_THM.jpg\",\r\n    \"thumbnailPreviewUrl\": \"https://cdn.example.com/product-123_THP.jpg\",\r\n    \"createdOn\": \"2024-04-02T10:30:00\",\r\n    \"modifiedOn\": \"2024-04-02T10:30:15\"\r\n  }\r\n}\r\n```",
        "parameters": [
          {
            "name": "X-API-KEY",
            "in": "header",
            "description": "Tenant's API key (required)",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "Request containing the image URL and optional tags",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateImageRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateImageRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CreateImageRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedImageRepresentation"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationFailureRepresentation"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnauthorizedRepresentation"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForbiddenRepresentation"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConflictRepresentation"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorRepresentation"
                }
              }
            }
          }
        }
      }
    },
    "/images({reference})": {
      "get": {
        "tags": [
          "Images"
        ],
        "summary": "Get Image",
        "description": "In DAM, images are identified by their filename (`reference`), which is unique per tenant.\r\nUse this endpoint to retrieve a specific image when you already know its filename.\r\n            \r\n## Authentication\r\n            \r\nRequires a valid API key in the `X-API-KEY` header.\r\n            \r\n## Example\r\n            \r\n```\r\nGET /images(product-main-front.jpg)\r\nX-API-KEY: your-api-key-here\r\n```\r\n            \r\n## Response (200 OK)\r\n            \r\nThe image is wrapped in the house entity envelope — its fields are under `value`, never at the\r\ntop level:\r\n            \r\n```json\r\n{\r\n  \"value\": {\r\n    \"id\": 42,\r\n    \"reference\": \"product-main-front.jpg\",\r\n    \"numLinks\": 3,\r\n    \"status\": \"Ok\",\r\n    \"modifiedOn\": \"2024-04-05T08:44:33\",\r\n    \"fileType\": \"jpg\",\r\n    \"createdOn\": \"2024-04-05T08:44:33\",\r\n    \"tags\": [\"animales\", \"mascotas\"],\r\n    \"originalUrl\": \"https://cdn.example.com/product-main-front.jpg\",\r\n    \"thumbnailUrl\": \"https://cdn.example.com/product-main-front_TH.jpg\",\r\n    \"thumbnailMediumUrl\": \"https://cdn.example.com/product-main-front_THM.jpg\",\r\n    \"thumbnailPreviewUrl\": \"https://cdn.example.com/product-main-front_THP.jpg\",\r\n    \"width\": 800,\r\n    \"height\": 600,\r\n    \"sizeInBytes\": 102400\r\n  },\r\n  \"@readLink\": \"https://api2.saleslayer.com/dam/images(product-main-front.jpg)\",\r\n  \"@editLink\": \"https://api2.saleslayer.com/dam/images(product-main-front.jpg)\"\r\n}\r\n```\r\n            \r\nNotes on the payload:\r\n            \r\n- `status` tells you whether the image is usable yet — see the status values documented on\r\n  `GET /images`. A freshly created or substituted image is `Up` until the worker finishes.\r\n- `width`, `height` and `sizeInBytes` come from the stored metadata and are `0` until processing\r\n  has recorded them; the thumbnail URLs are empty until the renditions exist.\r\n- `tags` is stored as a comma-separated string, so a tag that itself contains a comma comes back\r\n  split into several tags.\r\n- `@readLink` and `@editLink` are the canonical URL of this image (this request, echoed back), and\r\n  are only emitted when hypermedia enrichment is configured for the deployment.\r\n            \r\n## Error Responses\r\n            \r\n- **401 Unauthorized**: Missing or invalid API key. Returned by the API gateway as\r\n  `{ \"message\": \"Unauthorized\", \"request_id\": \"d8aafa5b8f3e400b60bea0123dd33317\" }`\r\n- **403 Forbidden**: The API key does not have read permissions for this operation. Same body\r\n  shape as the 401\r\n- **404 Not Found**: No image with the given reference exists for this tenant. The body is the\r\n  message string, e.g. `\"Image with reference 'missing.jpg' not found\"`\r\n- **500 Internal Server Error**: Technical error (e.g. database unavailable)",
        "parameters": [
          {
            "name": "X-API-KEY",
            "in": "header",
            "description": "Tenant's API key (required)",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "reference",
            "in": "path",
            "description": "Image filename, unique per tenant (e.g. `product-main-front.jpg`)",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImageEntityRepresentation"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnauthorizedRepresentation"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForbiddenRepresentation"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorRepresentation"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Images"
        ],
        "summary": "Patch Image",
        "description": "Partially updates an existing image identified by its filename (`reference`), which is unique per tenant — substitute its file via a new `url` and/or replace its `tags`. At least one of these fields must be supplied.\r\n            \r\nThe replacement `url` can point at a file with any name — the image is **always stored under its\r\ncurrent `reference`**, because that filename is how the rest of the platform points at the image and\r\na substitution cannot rename it. What the replacement *cannot* change is the image's **file type**,\r\nor the image would end up served under a name that misdescribes it.\r\n            \r\nThat type is decided by **the content of the file**, not by the extension in the `url`: the extension\r\nis yours to write and any file can be published under any name, so it proves nothing. The extension\r\nstill has to be one `POST /images` accepts, but the bytes are what settle the type.\r\n            \r\n```\r\nPATCH /images(product-123.jpg)\r\n{ \"url\": \"https://cdn.example.com/products/v2/product-123.jpg\" }   OK   same name, JPEG content\r\n{ \"url\": \"https://cdn.example.com/uploads/a7f3c9e2-render.jpg\" }   OK   any name, JPEG content\r\n{ \"url\": \"https://cdn.example.com/products/product-123.jpeg\" }    OK   jpg and jpeg are one type\r\n{ \"url\": \"https://cdn.example.com/products/product-123.png\" }     400  content is a PNG\r\n{ \"url\": \"https://cdn.example.com/products/product-123.jpg\" }     400  named .jpg, but not a JPEG\r\n```\r\n            \r\n### Changing an image's file type\r\n            \r\nNot possible through a substitution, because the filename — and so the extension — is fixed. Do it\r\nclient-side in three steps:\r\n            \r\n1. **Create the new image** — `POST /images` with the new file's `url`.\r\n2. **Repoint the references** — use the Catalog API to find every entity (product, variant, category, …)\r\n   that uses the old filename and update it to the new one.\r\n3. **Delete the old image** — `DELETE /images({old-reference})` once nothing references it any more.\r\n            \r\n## Fields\r\n            \r\n- **url** (optional): public URL of the new image. Triggers a full image substitution. May be called\r\n  anything, but must serve an image of the current file type — see above.\r\n- **tags** (optional): array of strings, validated exactly as on `POST /images`.\r\n  - Absent → tags are not modified, **including when `url` is supplied**.\r\n  - `[]` → all tags are cleared.\r\n  - `[\"a\",\"b\"]` → tags are replaced with these values.\r\n            \r\n## Valid combinations\r\n            \r\n| Body | Operation |\r\n|------|-----------|\r\n| `{ \"url\": \"https://.../{any-filename}\" }` | Substitute image — **tags are kept** |\r\n| `{ \"url\": \"https://.../{any-filename}\", \"tags\": [\"a\"] }` | Substitute image and set tags |\r\n| `{ \"tags\": [\"a\"] }` | Update tags only, image file unchanged |\r\n            \r\n## Invalid combinations\r\n            \r\n- Empty body `{}` → 400.\r\n- `url` serving content that is not an image of the current file type → 400 (see above).\r\n            \r\n## Response (204 No Content)\r\n            \r\nSuccess returns no body.\r\n            \r\n## Notes\r\n            \r\n- If the image is currently being processed, returns 409.\r\n- **A substitution resets the image's `status` to `Up`** and re-enqueues it for processing, so the\r\n  thumbnails are regenerated. Until the worker finishes, `GET /images({reference})` reports `Up`\r\n  and the rendition URLs still point at the previous file. Poll `status` for `Ok` (or `Er`) before\r\n  treating the new file as live.\r\n- A tags-only update does not touch `status` and takes effect immediately.\r\n- Either way `modifiedOn` is refreshed.\r\n- A substitution does not change the image's `reference` or `fileType`, so entities referencing it\r\n  keep working.\r\n            \r\n## Error Responses\r\n            \r\n- **400 Bad Request**: Invalid request body (e.g., both fields missing), a replacement whose content\r\n  is not an image of the current `fileType`, or any of the `url`/`tags` validation failures\r\n  documented on `POST /images` (format, accessibility, Content-Type, size, dimensions). Same\r\n  `{ \"validationFailures\": { \"error\": [...] } }` envelope\r\n- **401 Unauthorized**: Missing or invalid API key. Returned by the API gateway as\r\n  `{ \"message\": \"Unauthorized\", \"request_id\": \"d8aafa5b8f3e400b60bea0123dd33317\" }`\r\n- **403 Forbidden**: The API key does not have write permissions for this operation. Same body\r\n  shape as the 401\r\n- **404 Not Found**: No image with the given reference exists for this tenant. The body is the\r\n  message string, e.g. `\"Image with reference 'missing.jpg' not found\"`\r\n- **409 Conflict**: The image is currently being processed and cannot be modified —\r\n  `{ \"error\": \"Image 'product-123.jpg' is currently being processed and cannot be modified\" }`\r\n- **500 Internal Server Error**: Technical error (network failure fetching the replacement file,\r\n  storage or database unavailable). The already-uploaded replacement object is cleaned up, so the\r\n  image is left on its previous file",
        "parameters": [
          {
            "name": "X-API-KEY",
            "in": "header",
            "description": "Tenant's API key (required)",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "reference",
            "in": "path",
            "description": "Image filename (reference), unique per tenant (e.g. `product-main-front.jpg`)",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "Fields to update. At least one of 'url' or 'tags' must be present.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PatchImageRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PatchImageRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PatchImageRequest"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "No Content"
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationFailureRepresentation"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnauthorizedRepresentation"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForbiddenRepresentation"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConflictRepresentation"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorRepresentation"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Images"
        ],
        "summary": "Delete Image",
        "description": "Deletes an image by its filename (`reference`), which is unique per tenant.\r\n            \r\n## Authentication\r\n            \r\nRequires a valid API key in the `X-API-KEY` header.\r\n            \r\n## Example\r\n            \r\n```\r\nDELETE /images(product-main-front.jpg)\r\nX-API-KEY: your-api-key-here\r\n```\r\n            \r\n## Response (204 No Content)\r\n            \r\nSuccess returns no body.\r\n            \r\n## Important: deletion does not cascade\r\n            \r\nThe image record is removed outright. The deletion is **not** propagated to the entities that use\r\nthe image — check `numLinks` on the image first (`GET /images({reference})`): if it is greater than\r\nzero, the products, variants and categories still referencing the filename will be left pointing at\r\nan image that no longer exists. Repoint them via the Catalog API before deleting.\r\n            \r\n## Error Responses\r\n            \r\n- **401 Unauthorized**: Missing or invalid API key. Returned by the API gateway as\r\n  `{ \"message\": \"Unauthorized\", \"request_id\": \"d8aafa5b8f3e400b60bea0123dd33317\" }`\r\n- **403 Forbidden**: The API key does not have write permissions for this operation. Same body\r\n  shape as the 401\r\n- **404 Not Found**: No image with the given reference exists for this tenant. The body is the\r\n  message string, e.g. `\"Image with reference 'missing.jpg' not found\"`\r\n- **409 Conflict**: Image is currently being processed and cannot be deleted —\r\n  `{ \"error\": \"Image 'product-123.jpg' is currently being processed and cannot be deleted\" }`\r\n- **500 Internal Server Error**: Technical error (e.g. database unavailable)",
        "parameters": [
          {
            "name": "X-API-KEY",
            "in": "header",
            "description": "Tenant's API key (required)",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "reference",
            "in": "path",
            "description": "Image filename (reference), unique per tenant",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "No Content"
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnauthorizedRepresentation"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForbiddenRepresentation"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConflictRepresentation"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorRepresentation"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "FileDto": {
        "type": "object",
        "properties": {
          "reference": {
            "type": "string",
            "description": "The file's filename, unique per tenant. This is the public key used to address the file and the\r\nvalue entities use to reference it.",
            "nullable": true
          },
          "numLinks": {
            "type": "integer",
            "description": "Number of entities (products, variants, categories, …) currently using this file.",
            "format": "int32"
          },
          "status": {
            "type": "string",
            "description": "Processing status: `Vd` (void), `Up` (uploading/updating), `Ok` (ready to use),\r\n`Re` (reprocessing), `Er` (processing failed) or `Dv` (deleted). Only `Ok`\r\nguarantees the stored file is usable.",
            "nullable": true
          },
          "modifiedOn": {
            "type": "string",
            "description": "When the file was last modified.",
            "format": "date-time"
          },
          "fileType": {
            "type": "string",
            "description": "File extension of the file, without the dot (e.g. `pdf`, `csv`). Derived from the\r\nfilename, not from the served Content-Type.",
            "nullable": true
          },
          "createdOn": {
            "type": "string",
            "description": "When the file was created. Null for files predating creation-date tracking.",
            "format": "date-time",
            "nullable": true
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tags assigned to the file. Stored as a comma-separated string, so a tag containing a comma is\r\nread back as several tags. Not usable in `$filter` or `$orderby`.",
            "nullable": true
          },
          "url": {
            "type": "string",
            "description": "URL of the stored file. Empty until processing has run.",
            "nullable": true
          },
          "sizeInBytes": {
            "type": "integer",
            "description": "Size of the file in bytes; `0` until processing has recorded it. Not usable in\r\n`$filter` or `$orderby`.",
            "format": "int64"
          }
        },
        "additionalProperties": false,
        "description": "A file asset."
      },
      "FileCollectionRepresentation": {
        "type": "object",
        "properties": {
          "value": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FileDto"
            },
            "description": "The files in this page, at most `$top` (default and maximum 100).",
            "nullable": true
          },
          "@count": {
            "type": "integer",
            "description": "Total number of files matching the filter, ignoring `$top`/`$skip`.",
            "format": "int64",
            "nullable": true
          },
          "@readLink": {
            "type": "string",
            "description": "Canonical URL of this collection — this request, echoed back. Present only when hypermedia\r\nenrichment is enabled.",
            "nullable": true
          },
          "@nextLink": {
            "type": "string",
            "description": "URL of the next page, omitted on the last page. Present only when hypermedia enrichment is\r\nenabled; otherwise page forward by incrementing `$skip` yourself.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A page of files wrapped in the house collection envelope: `{ value, @count }`."
      },
      "ValidationFailureRepresentation": {
        "type": "object",
        "properties": {
          "validationFailures": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {}
            },
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "UnauthorizedRepresentation": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "description": "Error description, e.g. `Unauthorized`.",
            "nullable": true
          },
          "request_id": {
            "type": "string",
            "description": "Gateway request identifier, to quote when contacting support.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Represents a 401 Unauthorized response returned when the API key is missing or invalid.\r\n            \r\nNOTE: this body is produced by the API gateway, which owns API-key authentication — DAM itself never\r\nvalidates the key, so a request that reaches this service has already been authenticated. Documented\r\nhere because it is what callers of the public API actually receive."
      },
      "ForbiddenRepresentation": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "nullable": true
          },
          "request_id": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Represents a 403 Forbidden response returned when the API key does not have write permissions for\r\nthis operation.\r\n            \r\nNOTE: this body is produced by the API gateway, which owns API-key authentication — DAM itself never\r\nvalidates the key, and the 403 it can emit for an unauthorised `AppResponse` has an empty body\r\n(see `Sl.Common.App.Adapters.Api.Rest.ActionResults.Forbidden`). Kept as the documented shape\r\nfor the gateway-generated response."
      },
      "ErrorRepresentation": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "nullable": true,
            "readOnly": true
          },
          "details": {
            "nullable": true,
            "readOnly": true
          }
        },
        "additionalProperties": false
      },
      "CreateFileRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "Publicly accessible HTTP or HTTPS URL of the file to import. Required. Must be absolute and end\r\nin a supported extension (pdf, doc, docx, txt, xls, xlsx, csv, rtf, ppt, pptx, zip, rar, tar,\r\nmpeg, mpg, avi, mov, tif, tiff, ai, eps, psd, x3d, sup, ldt, ies, dwg, obj, webp); the file's\r\n`reference` is the filename taken from the URL path. Maximum 2048 characters.",
            "nullable": true
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Optional tags to associate with the file, e.g. `[\"datasheets\", \"catalog\", \"2024\"]`. Tags are\r\ncase-sensitive and cannot be empty or whitespace. Avoid commas inside a tag — tags are stored as a\r\ncomma-separated string and would be read back split.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Request to create a new File entity."
      },
      "CreateFileResponse": {
        "type": "object",
        "properties": {
          "reference": {
            "type": "string",
            "description": "The file's filename, unique per tenant. This is the public key used to address the file.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "The reference of a newly created file. Retrieve the full file with\r\n`GET /files({reference})`. Unlike an image, a file is usable as soon as it is created — nothing\r\nprocesses it afterwards — so it starts, and stays, in status `Ok`."
      },
      "CreatedFileRepresentation": {
        "type": "object",
        "properties": {
          "value": {
            "$ref": "#/components/schemas/CreateFileResponse"
          },
          "@readLink": {
            "type": "string",
            "description": "Canonical URL of the created file, the same resource the `Location` header names.\r\nPresent only when hypermedia enrichment is enabled.",
            "nullable": true
          },
          "@editLink": {
            "type": "string",
            "description": "URL to modify the created file, identical to `@readLink`. Present only when hypermedia\r\nenrichment is enabled.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "The reference of a newly created file, wrapped in the house entity envelope: `{ value }`."
      },
      "ConflictRepresentation": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "FileEntityRepresentation": {
        "type": "object",
        "properties": {
          "value": {
            "$ref": "#/components/schemas/FileDto"
          },
          "@readLink": {
            "type": "string",
            "description": "Canonical URL of this file — this request, echoed back. Present only when hypermedia enrichment\r\nis enabled.",
            "nullable": true
          },
          "@editLink": {
            "type": "string",
            "description": "URL to modify this file, identical to `@readLink`. Present only when hypermedia enrichment\r\nis enabled.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A single file wrapped in the house entity envelope: `{ value }`."
      },
      "PatchFileRequest": {
        "type": "object",
        "properties": {
          "reference": {
            "type": "string",
            "description": "Taken from the URL path, not from the request body — any value sent in the body is ignored.",
            "nullable": true
          },
          "url": {
            "type": "string",
            "description": "Publicly accessible URL of the replacement file, which triggers a full substitution. Optional,\r\nbut at least one of `url` or `tags` must be supplied. Validated exactly as on create.\r\nThe replacement may be called anything at its own address — it is stored under the file's current\r\nSl.Apps.Dam.Application.Files.PatchFile.PatchFileRequest.Reference — but it must be a file of the same file type.",
            "nullable": true
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tags to set on the file. Optional. Omit to leave the tags untouched, send `[]` to clear them,\r\nor send values to replace them.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Request to partially update a File entity, identified by its reference (filename).\r\n            \r\nThe file is located by Sl.Apps.Dam.Application.Files.PatchFile.PatchFileRequest.Reference, which is the public, tenant-unique key\r\n(the numeric id is internal only). Supported body fields:\r\n  - Sl.Apps.Dam.Application.Files.PatchFile.PatchFileRequest.Url  → full file substitution.\r\n  - Sl.Apps.Dam.Application.Files.PatchFile.PatchFileRequest.Tags → update tags.\r\nAt least one body field must be present.\r\n            \r\nTags semantics:\r\n  - null  → field absent: tags are NOT modified, whatever else the request asks for.\r\n  - []    → clear: tags are set to empty.\r\n  - [...] → replace: tags are replaced with these values."
      },
      "ImageDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Internal numeric identifier. Not stable as a public key — address images by `reference`.",
            "format": "int32"
          },
          "reference": {
            "type": "string",
            "description": "The image's filename, unique per tenant. This is the public key used to address the image and the\r\nvalue entities use to reference it.",
            "nullable": true
          },
          "numLinks": {
            "type": "integer",
            "description": "Number of entities (products, variants, categories, …) currently using this image.",
            "format": "int32"
          },
          "status": {
            "type": "string",
            "description": "Processing status: `Vd` (void), `Up` (uploading/updating), `Ok` (ready to use),\r\n`Re` (reprocessing), `Er` (processing failed) or `Dv` (deleted). Only `Ok`\r\nguarantees the renditions exist.",
            "nullable": true
          },
          "modifiedOn": {
            "type": "string",
            "description": "When the image was last modified.",
            "format": "date-time"
          },
          "fileType": {
            "type": "string",
            "description": "File extension of the image, without the dot (e.g. `jpg`, `png`). Derived from the\r\nfilename, not from the served Content-Type.",
            "nullable": true
          },
          "createdOn": {
            "type": "string",
            "description": "When the image was created. Null for images predating creation-date tracking.",
            "format": "date-time",
            "nullable": true
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tags assigned to the image. Stored as a comma-separated string, so a tag containing a comma is\r\nread back as several tags. Not usable in `$filter` or `$orderby`.",
            "nullable": true
          },
          "originalUrl": {
            "type": "string",
            "description": "URL of the original, full-size image. Empty until processing has run.",
            "nullable": true
          },
          "thumbnailUrl": {
            "type": "string",
            "description": "URL of the small thumbnail rendition. Empty until processing has generated it.",
            "nullable": true
          },
          "thumbnailMediumUrl": {
            "type": "string",
            "description": "URL of the medium thumbnail rendition. Empty until processing has generated it.",
            "nullable": true
          },
          "thumbnailPreviewUrl": {
            "type": "string",
            "description": "URL of the preview thumbnail rendition. Empty until processing has generated it.",
            "nullable": true
          },
          "width": {
            "type": "integer",
            "description": "Width of the image in pixels; `0` until processing has recorded it. Not usable in\r\n`$filter` or `$orderby`.",
            "format": "int32"
          },
          "height": {
            "type": "integer",
            "description": "Height of the image in pixels; `0` until processing has recorded it. Not usable in\r\n`$filter` or `$orderby`.",
            "format": "int32"
          },
          "sizeInBytes": {
            "type": "integer",
            "description": "Size of the image file in bytes; `0` until processing has recorded it. Not usable in\r\n`$filter` or `$orderby`.",
            "format": "int64"
          }
        },
        "additionalProperties": false,
        "description": "An image asset."
      },
      "ImageCollectionRepresentation": {
        "type": "object",
        "properties": {
          "value": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ImageDto"
            },
            "description": "The images in this page, at most `$top` (default and maximum 100).",
            "nullable": true
          },
          "@count": {
            "type": "integer",
            "description": "Total number of images matching the filter, ignoring `$top`/`$skip`.",
            "format": "int64",
            "nullable": true
          },
          "@readLink": {
            "type": "string",
            "description": "Canonical URL of this collection — this request, echoed back. Present only when hypermedia\r\nenrichment is enabled.",
            "nullable": true
          },
          "@nextLink": {
            "type": "string",
            "description": "URL of the next page, omitted on the last page. Present only when hypermedia enrichment is\r\nenabled; otherwise page forward by incrementing `$skip` yourself.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A page of images wrapped in the house collection envelope: `{ value, @count }`."
      },
      "CreateImageRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "Publicly accessible HTTP or HTTPS URL of the image to import. Required. Must be absolute and end\r\nin a supported extension (jpg, jpeg, png, gif, tif, tiff, psd, webp); the image's `reference`\r\nis the filename taken from the URL path. Maximum 2048 characters.",
            "nullable": true
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Optional tags to associate with the image, e.g. `[\"product\", \"catalog\", \"2024\"]`. Tags are\r\ncase-sensitive and cannot be empty or whitespace. Avoid commas inside a tag — tags are stored as a\r\ncomma-separated string and would be read back split.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Request to create a new Image entity."
      },
      "CreateImageResponse": {
        "type": "object",
        "properties": {
          "reference": {
            "type": "string",
            "description": "The image's filename, unique per tenant. This is the public key used to address the image.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "The reference of a newly created image. Retrieve the full image with\r\n`GET /images({reference})`; processing runs asynchronously, so the image starts in status\r\n`Up` and moves to `Ok` or `Er`."
      },
      "CreatedImageRepresentation": {
        "type": "object",
        "properties": {
          "value": {
            "$ref": "#/components/schemas/CreateImageResponse"
          },
          "@readLink": {
            "type": "string",
            "description": "Canonical URL of the created image, the same resource the `Location` header names.\r\nPresent only when hypermedia enrichment is enabled.",
            "nullable": true
          },
          "@editLink": {
            "type": "string",
            "description": "URL to modify the created image, identical to `@readLink`. Present only when hypermedia\r\nenrichment is enabled.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "The reference of a newly created image, wrapped in the house entity envelope: `{ value }`."
      },
      "ImageEntityRepresentation": {
        "type": "object",
        "properties": {
          "value": {
            "$ref": "#/components/schemas/ImageDto"
          },
          "@readLink": {
            "type": "string",
            "description": "Canonical URL of this image — this request, echoed back. Present only when hypermedia enrichment\r\nis enabled.",
            "nullable": true
          },
          "@editLink": {
            "type": "string",
            "description": "URL to modify this image, identical to `@readLink`. Present only when hypermedia enrichment\r\nis enabled.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A single image wrapped in the house entity envelope: `{ value }`."
      },
      "PatchImageRequest": {
        "type": "object",
        "properties": {
          "reference": {
            "type": "string",
            "description": "Taken from the URL path, not from the request body — any value sent in the body is ignored.",
            "nullable": true
          },
          "url": {
            "type": "string",
            "description": "Publicly accessible URL of the replacement image, which triggers a full substitution. Optional,\r\nbut at least one of `url` or `tags` must be supplied. Validated exactly as on create.\r\nThe replacement may be called anything at its own address — it is stored under the image's\r\ncurrent Sl.Apps.Dam.Application.Images.PatchImage.PatchImageRequest.Reference — but it must be an image of the same file type.",
            "nullable": true
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tags to set on the image. Optional. Omit to leave the tags untouched, send `[]` to clear them,\r\nor send values to replace them — including when the image itself is substituted via `url`,\r\nwhich never touches the tags on its own.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Request to partially update an Image entity, identified by its reference (filename).\r\n            \r\nThe image is located by Sl.Apps.Dam.Application.Images.PatchImage.PatchImageRequest.Reference, which is the public, tenant-unique key\r\n(images are not addressed by their numeric id). Supported body fields:\r\n  - Sl.Apps.Dam.Application.Images.PatchImage.PatchImageRequest.Url  → full image substitution.\r\n  - Sl.Apps.Dam.Application.Images.PatchImage.PatchImageRequest.Tags → update tags.\r\nAt least one body field must be present.\r\n            \r\nTags semantics:\r\n  - null  → field absent: tags are NOT modified.\r\n  - []    → clear: tags are set to empty.\r\n  - [...] → replace: tags are replaced with these values."
      }
    },
    "securitySchemes": {
      "ApiKey": {
        "type": "apiKey",
        "description": "Tenant API key. Required on every request.",
        "name": "X-API-KEY",
        "in": "header"
      }
    }
  }
}