{
  "openapi": "3.1.0",
  "info": {
    "title": "UpDash Mail API",
    "version": "0.1.0",
    "summary": "Order automotive direct mail (postcards and letters) for a dealer.",
    "description": "Phase 1 pilot. Submitted live orders go to Xcel's ops queue and are produced by hand.\n\nFlow: products -> dealer (create or update its profile) -> dealer brand -> create campaign -> audience upload (signed URL) -> complete -> creative -> proof -> validate -> quote -> approval -> a human decides on the review page -> submit (Idempotency-Key) -> status/results.\n\nApproval: live review links are emailed by Xcel to one of the dealer's registered approvers (set by Xcel, never through the API) and are never returned to the caller. Sandbox returns `review_url` (with `sandbox_only: true`) so a developer can try the page.\n\nAuth: `Authorization: Bearer xm_test_...` (sandbox: nothing prints, ops never sees it) or `Bearer xm_live_...`. Every request is scoped to the key's partner and mode; other partners' records answer 404.\n\nEvery `/v1` error has the shape `{\"error\":{\"code\",\"message\",\"fix\",\"field\"}}`, and every `/v1` response carries an `X-Request-Id` header.\n\nCustomer rows never pass through this API: audiences are uploaded straight to storage, and responses carry ids and counts only. Pilot prices are placeholders (`placeholder_pricing: true`). USPS-derived statuses are processing scans, not proof of delivery.\n\nSeventeen MCP tools at `/api/mcp` (Streamable HTTP) run the same code; MCP clients authenticate with the same Bearer key or by OAuth 2.1 (discovery at `/.well-known/oauth-protected-resource`), and OAuth tokens are not accepted on `/v1`. Listing campaigns, webhooks, the dealer import, profile versions and PUT, and the sandbox advance are REST only.\n\nDealers: partners create dealers and own their profiles (versioned; the version is the profile's ETag, sent back as If-Match). Approvers, disclosures, the license number, spend caps and live ordering are Xcel's (`xcel_managed_field`). A creative prints the profile version it was made with, so a profile change never alters an approved order."
  },
  "servers": [
    {
      "url": "https://api.updash.com",
      "description": "Production: sandbox (xm_test_) and live (xm_live_) keys"
    },
    {
      "url": "http://localhost:3000",
      "description": "Local development"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "Catalog",
      "description": "The four products, their print specs and their templates."
    },
    {
      "name": "Dealers",
      "description": "Create your dealers, keep their profiles current (versioned, with ETag and If-Match), upload logos and import dealers from a CSV. Approvers, disclosures, the license number and spend caps are Xcel's."
    },
    {
      "name": "Campaigns",
      "description": "Create and list campaigns."
    },
    {
      "name": "Audience",
      "description": "Upload an audience CSV through a signed URL and process it. Rows never pass through the API."
    },
    {
      "name": "Creative",
      "description": "Fill a template with copy (or a brief), render and check the proof."
    },
    {
      "name": "Quote and approval",
      "description": "Quote the campaign, ask a human to approve, and submit."
    },
    {
      "name": "Status",
      "description": "Campaign status, next step, the timeline and results."
    },
    {
      "name": "Sandbox",
      "description": "Test-key only: simulate production statuses."
    },
    {
      "name": "Webhooks",
      "description": "Register and list signed event endpoints."
    }
  ],
  "paths": {
    "/v1/products": {
      "get": {
        "tags": [
          "Catalog"
        ],
        "operationId": "listMailProducts",
        "summary": "List mail products, specs and templates",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/dealers": {
      "get": {
        "tags": [
          "Dealers"
        ],
        "operationId": "listDealers",
        "summary": "List your dealers, a page at a time",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealerList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        },
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The previous page's next_after."
          }
        ]
      },
      "post": {
        "tags": [
          "Dealers"
        ],
        "operationId": "createDealer",
        "summary": "Create a dealer with its profile version 1",
        "description": "A new dealer has no registered approver: it can be quoted in test and live, but live request_approval and submit answer no_registered_approver until Xcel registers one. With a test (xm_test_) key it only validates: 200 with validated_only true, and nothing is created.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateDealerRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Validated only (test key): nothing was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealerCreateValidated"
                }
              }
            },
            "headers": {
              "X-Xcel-Validated-Only": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                },
                "description": "Present on a dealer write made with a test key: everything was checked and nothing was saved."
              }
            }
          },
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealerCreated"
                }
              }
            },
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                },
                "description": "The profile version, e.g. \"3\"."
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/v1/dealers/import": {
      "post": {
        "tags": [
          "Dealers"
        ],
        "operationId": "importDealers",
        "summary": "Upsert dealers from a CSV by external_id",
        "description": "A row for a new external_id creates the dealer (it needs name, address_line1, city, state and zip); a row for an existing one merges its non-empty cells. At most 500 rows and 1 MB. Cells a spreadsheet would read as a formula are refused per row (formula_value). Xcel-managed, unknown or repeated columns refuse the whole file. Columns: see /docs/dealers.md. With a test (xm_test_) key it only validates: validated_only is true, the X-Xcel-Validated-Only header is set, and nothing is saved.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "pattern": "^[\\x21-\\x7e]{1,255}$"
            },
            "description": "Optional. The same file with the same key replays the stored result for 24 hours (Idempotent-Replayed: true)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "text/csv": {
              "schema": {
                "type": "string"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealerImportResult"
                }
              }
            },
            "headers": {
              "X-Xcel-Validated-Only": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                },
                "description": "Present on a dealer write made with a test key: everything was checked and nothing was saved."
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          }
        }
      }
    },
    "/v1/dealers/{id}": {
      "get": {
        "tags": [
          "Dealers"
        ],
        "operationId": "getDealer",
        "summary": "Get one dealer",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 100
            },
            "description": "Xcel dealer id or your own external dealer id."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealerResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/dealers/{id}/profile": {
      "get": {
        "tags": [
          "Dealers"
        ],
        "operationId": "getDealerProfile",
        "summary": "Get the dealer's profile",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 100
            },
            "description": "Xcel dealer id or your own external dealer id."
          },
          {
            "name": "version",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "An older version; omit for the current one."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealerProfileResponse"
                }
              }
            },
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                },
                "description": "The profile version, e.g. \"3\"."
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "Dealers"
        ],
        "operationId": "replaceDealerProfile",
        "summary": "Replace the profile",
        "description": "Fields left out are cleared. Logos and Xcel-managed fields are kept. Writes a new version unless nothing changed. With a test (xm_test_) key it only validates: validated_only is true, the X-Xcel-Validated-Only header is set, and nothing is saved.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 100
            },
            "description": "Xcel dealer id or your own external dealer id."
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The profile version you read (its ETag, e.g. \"3\"). A stale one is 412 version_conflict. Omit (or *) to write over whatever is current."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DealerProfile"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealerProfileWritten"
                }
              }
            },
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                },
                "description": "The profile version, e.g. \"3\"."
              },
              "X-Xcel-Validated-Only": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                },
                "description": "Present on a dealer write made with a test key: everything was checked and nothing was saved."
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          }
        }
      },
      "patch": {
        "tags": [
          "Dealers"
        ],
        "operationId": "patchDealerProfile",
        "summary": "Merge-patch the profile",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 100
            },
            "description": "Xcel dealer id or your own external dealer id."
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The profile version you read (its ETag, e.g. \"3\"). A stale one is 412 version_conflict. Omit (or *) to write over whatever is current."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProfilePatch"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DealerProfileWritten"
                }
              }
            },
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                },
                "description": "The profile version, e.g. \"3\"."
              },
              "X-Xcel-Validated-Only": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                },
                "description": "Present on a dealer write made with a test key: everything was checked and nothing was saved."
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          }
        },
        "description": "With a test (xm_test_) key it only validates: validated_only is true, the X-Xcel-Validated-Only header is set, and nothing is saved."
      }
    },
    "/v1/dealers/{id}/profile/versions": {
      "get": {
        "tags": [
          "Dealers"
        ],
        "operationId": "listDealerProfileVersions",
        "summary": "List profile versions, newest first",
        "description": "Who changed which fields and when; never values.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 100
            },
            "description": "Xcel dealer id or your own external dealer id."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "before",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "The previous page's next_before."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProfileVersionList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/dealers/{id}/logo": {
      "post": {
        "tags": [
          "Dealers"
        ],
        "operationId": "uploadDealerLogo",
        "summary": "Upload the primary or reversed logo",
        "description": "PNG, JPEG, SVG or PDF judged by content, at most 3 MB. Raster: at least 600 px on the long side and 100 px on the short side. Animated files are refused. SVG is rebuilt from allowed parts and drawn to a PNG; proofs print only the PNG. PDF needs a PDF rasterizer on the server (415 logo_pdf_unsupported otherwise). Writes a new profile version unless it is the same file. With a test (xm_test_) key it only validates: validated_only is true, the X-Xcel-Validated-Only header is set, and nothing is saved.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 100
            },
            "description": "Xcel dealer id or your own external dealer id."
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The profile version you read (its ETag, e.g. \"3\"). A stale one is 412 version_conflict. Omit (or *) to write over whatever is current."
          },
          {
            "name": "variant",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "primary",
                "reversed"
              ],
              "default": "primary"
            },
            "description": "For a raw file body."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "image/png": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/jpeg": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/svg+xml": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "application/pdf": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LogoUploadJson"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LogoUploaded"
                }
              }
            },
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                },
                "description": "The profile version, e.g. \"3\"."
              },
              "X-Xcel-Validated-Only": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                },
                "description": "Present on a dealer write made with a test key: everything was checked and nothing was saved."
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/v1/dealers/{id}/brand": {
      "get": {
        "tags": [
          "Dealers"
        ],
        "operationId": "getDealerBrand",
        "summary": "What a new creative prints for the dealer",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Xcel dealer id or your own external dealer id."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BrandKit"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/campaigns": {
      "get": {
        "tags": [
          "Campaigns"
        ],
        "operationId": "listCampaigns",
        "summary": "List campaigns (this key's partner and mode)",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/CampaignStatus"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "tags": [
          "Campaigns"
        ],
        "operationId": "createCampaign",
        "summary": "Create a draft campaign",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCampaignRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/campaigns/{id}": {
      "get": {
        "tags": [
          "Status"
        ],
        "operationId": "getCampaignStatus",
        "summary": "Campaign status, counts, approval state and next step",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Campaign id."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignStatusResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/campaigns/{id}/audience": {
      "post": {
        "tags": [
          "Audience"
        ],
        "operationId": "startAudienceUpload",
        "summary": "Start an audience upload (signed URL)",
        "description": "Returns a signed upload URL valid for 2 hours. PUT the CSV there (`Content-Type: text/csv`), then call `/audience/complete`. Rows are never accepted in a request body. One PUT per URL: the URL cannot replace a file that exists, so a corrected file needs a new upload.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Campaign id."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StartAudienceRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AudienceUpload"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/v1/campaigns/{id}/audience/complete": {
      "post": {
        "tags": [
          "Audience"
        ],
        "operationId": "completeAudienceUpload",
        "summary": "Process an uploaded audience",
        "description": "Parses, normalizes, dedupes (name + normalized address) and applies the optional suppression list. Returns counts and a reason code per rejected spreadsheet row number. Live audiences then wait for Xcel's CASS/NCOA run (`pending_address_processing`); sandbox audiences pass immediately. Completing twice returns the stored result. A file that cannot be processed is refused with 422 and that version becomes `rejected`. The response has no `next_step`; the next step is the creative.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Campaign id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompleteAudienceRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AudienceCompleted"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          }
        }
      }
    },
    "/v1/campaigns/{id}/creative": {
      "post": {
        "tags": [
          "Creative"
        ],
        "operationId": "generateCreative",
        "summary": "Fill a template with copy, or supply HTML",
        "description": "Brand values (logo, colors, phone, dealer name/address, disclosures) come from the locked brand kit and the FCRA notices are fixed text; sending `brand`, `dealer`, brand-named copy keys or copy keys starting with fcra is refused with `locked_field`. The creative is stored even when preflight finds errors. With a brief, Claude writes the open copy slots (see Creative docs, AI-written copy). Caller html is parsed and cleaned before storage: scripts, embedding elements, on* handlers, javascript: and remote URLs, @import, @media print, @page and :has() are removed, and each is a preflight error (script_not_allowed, active_content_not_allowed, remote_resource_not_allowed, print_css_not_allowed, css_invalid).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Campaign id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GenerateCreativeRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreativeGenerated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "description": "The partner's daily AI copy cap is used (`ai_copy_rate_limited`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/v1/campaigns/{id}/proof": {
      "post": {
        "tags": [
          "Creative"
        ],
        "operationId": "renderProof",
        "summary": "Render and check the proof",
        "description": "Renders three sample rows plus the worst case (longest name/address), checks the brand-kit logo (warning brand_logo_unreachable; the proof then shows a placeholder box with the dealer name), and fits the worst case into the address area (warnings address_abbreviated, address_line_wrapped, address_block_overflow). Where PDF rendering is on, it prints the print-ready PDF once at trim size plus bleed (one page per panel, else 422 proof_layout_invalid), draws the review proof's guides and PROOF footer over those same pages, and stores both (`renderer.format: \"pdf\"`, `pdf_available: true`); a print failure answers 502 `proof_render_failed` and changes nothing. Elsewhere the proof is an HTML preview (`pdf_available: false`). The proof shows customer records, so it is not returned; the approver sees it on the review page. Limited per partner per UTC hour (429 proof_render_rate_limited).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Campaign id."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProofResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "429": {
            "description": "proof_render_rate_limited: the partner's render_proof calls this UTC hour reached PROOF_RENDER_LIMIT_PER_HOUR (30 by default, sandbox and live together)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/campaigns/{id}/validate": {
      "get": {
        "tags": [
          "Creative"
        ],
        "operationId": "validateCampaignGet",
        "summary": "Readiness checklist and preflight issues (read-only; same as POST)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Campaign id."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidateResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "Creative"
        ],
        "operationId": "validateCampaign",
        "summary": "Readiness checklist and preflight issues (read-only; GET also works)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Campaign id."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidateResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/campaigns/{id}/quote": {
      "post": {
        "tags": [
          "Quote and approval"
        ],
        "operationId": "getQuote",
        "summary": "Quote the current creative and audience",
        "description": "Refuses with 500 `price_below_cost` if a configured price is below Xcel's cost floor (a configuration error at Xcel; nothing in the request needs to change).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Campaign id."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuoteRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuoteResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/v1/campaigns/{id}/approval": {
      "post": {
        "tags": [
          "Quote and approval"
        ],
        "operationId": "requestApproval",
        "summary": "Request human approval",
        "description": "Creates an approval bound to the campaign's current creative version, audience version, the quote and `max_spend_cents`. Live: `approver_email` must be one of the dealer's `registered_approvers` (omit it when exactly one is registered); Xcel emails the review link to that person and the response holds only the approval id, status, masked approver email, total and expiry. Refusals: `no_registered_approver` (409), `approver_not_registered` and `approver_email_required` (400), `approval_delivery_unavailable` (503, nothing created), `approval_delivery_failed` (502, approval expired). Sandbox: no registry check, no email; the response adds `review_url` and `sandbox_only: true`. Only a human decision on the review page approves the order. Where PDF rendering is on, the approval records the print-ready PDF's sha256 and is refused with 409 proof_pdf_missing when no PDF exists for the current versions; approving, submitting and the printer packet then refuse a changed file (proof_pdf_changed).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Campaign id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApprovalRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created. Live responses match ApprovalRequestedLive; sandbox responses add review_url and sandbox_only (ApprovalRequestedSandbox).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ApprovalRequestedLive"
                    },
                    {
                      "$ref": "#/components/schemas/ApprovalRequestedSandbox"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/v1/campaigns/{id}/submit": {
      "post": {
        "tags": [
          "Quote and approval"
        ],
        "operationId": "submitCampaign",
        "summary": "Submit an approved campaign",
        "description": "Requires `Idempotency-Key`, scoped to the partner and the key's mode. The dealer's spend cap is checked again inside the submit transaction under a per-dealer lock. Refused unless the approval is approved by a human, unexpired, bound to the current creative/audience versions and quote, the quote is unexpired, its total is within `max_spend_cents` and the dealer's monthly spend cap, and (live) the partner has ordering enabled. Refusal codes: proof_pdf_changed, approval_not_found, approval_pending, approval_rejected, approval_expired, approval_stale_creative, approval_stale_audience, approval_quote_mismatch, quote_expired, max_spend_exceeded, campaign_not_approved, ordering_disabled, dealer_spend_cap_exceeded, already_submitted, idempotency_key_required, invalid_idempotency_key, idempotency_key_reused, idempotency_in_progress, campaign_changed, not_found. The body is validated before the key. A repeat with the same key, campaign and approval_id within 24 hours returns the stored response with `Idempotent-Replayed: true`; refusals are not stored. While a request with the key is running, a repeat gets idempotency_in_progress; the running request holds the key for at most 60 seconds, so a crashed request frees it within a minute. The submission, the status change, its event and webhook deliveries are written in one transaction.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Campaign id."
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "pattern": "^[\\x21-\\x7e]{1,255}$"
            },
            "description": "One key per order (a UUID): 1 to 255 printable ASCII characters, no spaces. Scoped to the partner and the key's mode. Reuse it only to retry the same request."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubmitRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Submitted (or replayed)",
            "headers": {
              "Idempotent-Replayed": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubmitResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/v1/campaigns/{id}/results": {
      "get": {
        "tags": [
          "Status"
        ],
        "operationId": "getCampaignResults",
        "summary": "Timeline, quantities and results",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Campaign id."
          },
          {
            "name": "leads_after",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^xm[a-z2-7]{16}$"
            },
            "description": "Cursor: the leads.next_after of the previous page. Omit for the first page."
          },
          {
            "name": "leads_limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            },
            "description": "Leads per page."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResultsResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "description": "The production timeline and quantities, and results once Xcel's tracking reports them: aggregates and a page of per-lead results keyed by your external_id and the row key, never names or addresses. Live orders are refreshed every 6 hours until 90 days after completion; sandbox orders get simulated results from the test advance (in_postal_network onward). Per-lead results are deleted 365 days (by default) after the order completes or is cancelled; the aggregates stay."
      }
    },
    "/v1/test/campaigns/{id}/advance": {
      "post": {
        "tags": [
          "Sandbox"
        ],
        "operationId": "advanceTestCampaign",
        "summary": "Sandbox only: simulate the next production status",
        "description": "Test-mode keys only (`test_mode_only` otherwise). Moves a submitted sandbox campaign to the next status, or to `to`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Campaign id."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AdvanceRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdvanceResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/v1/webhooks": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "listWebhooks",
        "summary": "List webhook endpoints with their latest delivery (secrets never shown)",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "description": "Your endpoints in the key's mode (a sandbox key sees sandbox endpoints only), active and deactivated, each with its latest delivery, and the active-endpoint limit. The signing secret is never returned here: only at creation and rotation."
      },
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "createWebhook",
        "summary": "Register a webhook endpoint (secret shown once)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateWebhookRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        },
        "description": "Live endpoints must be https; sandbox endpoints may be http. URLs with credentials, local host names, hosts that do not resolve, and hosts that resolve to any non-public address are refused with `invalid_webhook_url`. The host is checked again before every delivery. The secret is returned once. At most WEBHOOKS_MAX_PER_PARTNER_MODE (5 by default) active endpoints per partner and mode: 409 webhook_limit_reached."
      }
    },
    "/v1/webhooks/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "Webhook endpoint id."
        }
      ],
      "delete": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "deleteWebhook",
        "summary": "Deactivate a webhook endpoint (idempotent)",
        "description": "Deactivates one of your endpoints in the key's mode and cancels its pending deliveries, so nothing more is sent to it. The endpoint stays in the list (with deactivated_at and its last delivery) and no longer counts toward the active-endpoint limit. Calling it again answers 200 with pending_deliveries_cancelled 0. Another partner's endpoint, or one in the other mode, answers 404 webhook_not_found.",
        "responses": {
          "200": {
            "description": "Deactivated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDeactivated"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/webhooks/{id}/rotate-secret": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "Webhook endpoint id."
        }
      ],
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "rotateWebhookSecret",
        "summary": "Rotate an endpoint's signing secret (new secret shown once)",
        "description": "Gives an active endpoint a new `whsec_` secret and returns it once. It applies at once: every delivery sent from now on, retries included, is signed with it, so update your receiver first or accept both secrets for a short overlap. A deactivated endpoint answers 409 webhook_inactive; another partner's or mode's answers 404 webhook_not_found. No request body.",
        "responses": {
          "200": {
            "description": "Rotated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookCreated"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    }
  },
  "webhooks": {
    "campaignEvent": {
      "post": {
        "operationId": "receiveCampaignEvent",
        "summary": "Campaign event delivery",
        "description": "POSTed to each active endpoint of the partner and mode that existed when the event happened (https for live endpoints; sandbox endpoints may be http). Verify `X-Xcel-Signature: t=<unix seconds>,v1=<hex>` where v1 = HMAC-SHA256(secret, `${t}.${raw body}`); reject if t is more than 5 minutes old. Return a 2xx within 5 seconds; redirects are not followed. Failed deliveries are retried no sooner than 1m, 5m, 30m, 2h, 6h and 24h later (seven attempts over about 33 hours; the delivery job runs every 5 minutes), then marked failed. Live deliveries are claimed and sent first, then partners take turns, up to 4 at a time. Each attempt is claimed before it is sent, so concurrent delivery runs never send the same delivery, but delivery is still at least once and unordered: de-duplicate on `X-Xcel-Event-Id` (the body's `id`). Event types: campaign.status_changed, audience.completed, audience.address_processed, audience.purged, creative.generated, quote.created, approval.requested, approval.decided, campaign.results_updated, campaign.lead_results_updated, dealer.updated. dealer.updated goes to the partner's active endpoints in both modes, since test and live keys share dealers; a test key's dealer write only validates and sends nothing. The two results events are sent only when the results changed.",
        "parameters": [
          {
            "name": "X-Xcel-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Xcel-Event-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Xcel-Event-Type",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Return any 2xx to acknowledge."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "xm_test_... | xm_live_...",
        "description": "Partner API key. Keys are stored only as sha256 hashes."
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid request",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid API key",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "headers": {
          "WWW-Authenticate": {
            "schema": {
              "type": "string",
              "enum": [
                "Bearer realm=\"xcel-mail-api\""
              ]
            }
          }
        }
      },
      "Forbidden": {
        "description": "Not allowed for this key or partner",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Not found for this partner and mode",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Conflict": {
        "description": "Refused in the current state; `fix` says what to do",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unprocessable": {
        "description": "The file or creative failed checks; `fix` says what to change",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ServerError": {
        "description": "Server or Xcel configuration error (e.g. price_below_cost)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "BadGateway": {
        "description": "An upstream service (email, AI copy) or the PDF renderer failed",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ServiceUnavailable": {
        "description": "Not configured or temporarily unavailable",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PreconditionFailed": {
        "description": "`If-Match` named a profile version that is no longer current (`version_conflict`). Read the profile again and reapply your change.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "The body is over the size limit (`payload_too_large`, `logo_too_large`, `import_too_large`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "UnsupportedMediaType": {
        "description": "The body's type is not one this endpoint takes (`unsupported_media_type`, `logo_unsupported_type`, `logo_pdf_unsupported`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message",
              "fix",
              "field"
            ],
            "additionalProperties": false,
            "properties": {
              "code": {
                "type": "string",
                "examples": [
                  "approval_pending"
                ]
              },
              "message": {
                "type": "string"
              },
              "fix": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "field": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          }
        }
      },
      "CampaignStatus": {
        "type": "string",
        "enum": [
          "draft",
          "proof_ready",
          "awaiting_approval",
          "approved",
          "submitted",
          "in_production",
          "at_usps",
          "in_postal_network",
          "delivery_estimated",
          "completed",
          "cancelled"
        ]
      },
      "MailClass": {
        "type": "string",
        "enum": [
          "marketing_mail",
          "first_class"
        ]
      },
      "Address": {
        "type": "object",
        "properties": {
          "line1": {
            "type": "string"
          },
          "line2": {
            "type": [
              "string",
              "null"
            ]
          },
          "city": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "description": "Two-letter US state code."
          },
          "zip": {
            "type": "string",
            "description": "12345 or 12345-6789."
          }
        }
      },
      "Product": {
        "type": "object",
        "description": "Print spec in inches (snake_case of src/catalog/products.ts ProductSpec) plus its templates.",
        "additionalProperties": true,
        "properties": {
          "code": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "postcard",
              "letter"
            ]
          },
          "usps_category": {
            "type": "string",
            "enum": [
              "card",
              "letter",
              "flat"
            ]
          },
          "finished_size": {
            "type": "object",
            "properties": {
              "width": {
                "type": "number"
              },
              "height": {
                "type": "number"
              }
            },
            "description": "Trimmed size in inches."
          },
          "bleed": {
            "type": "number",
            "description": "Inches the artwork extends past the trim on every side."
          },
          "safe_margin": {
            "type": "number",
            "description": "Inches inside the trim where text and logos must stay."
          },
          "sides": {
            "type": "integer"
          },
          "mail_classes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MailClass"
            }
          },
          "min_presort_pieces": {
            "type": "object",
            "properties": {
              "marketing_mail": {
                "type": "integer"
              },
              "first_class": {
                "type": "integer"
              }
            }
          },
          "confirmed_by_printer": {
            "type": "boolean"
          },
          "fold": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "type": {
                "type": "string",
                "examples": [
                  "tri",
                  "double_parallel"
                ]
              },
              "panels": {
                "type": "integer"
              },
              "folded_size": {
                "type": "object",
                "properties": {
                  "width": {
                    "type": "number"
                  },
                  "height": {
                    "type": "number"
                  }
                }
              }
            }
          },
          "address_block": {
            "type": "object",
            "properties": {
              "side": {
                "type": "string",
                "enum": [
                  "front",
                  "back",
                  "page1"
                ]
              },
              "x": {
                "type": "number"
              },
              "y": {
                "type": "number"
              },
              "width": {
                "type": "number"
              },
              "height": {
                "type": "number"
              },
              "note": {
                "type": "string"
              }
            }
          },
          "barcode_clear_zone": {
            "type": "object",
            "properties": {
              "side": {
                "type": "string",
                "enum": [
                  "front",
                  "back",
                  "envelope"
                ]
              },
              "from_right": {
                "type": "number"
              },
              "from_bottom": {
                "type": "number"
              },
              "width": {
                "type": "number"
              },
              "height": {
                "type": "number"
              }
            }
          },
          "envelope": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "code": {
                "type": "string",
                "examples": [
                  "#10"
                ]
              },
              "size": {
                "type": "object",
                "properties": {
                  "width": {
                    "type": "number"
                  },
                  "height": {
                    "type": "number"
                  }
                }
              },
              "windows": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "purpose": {
                      "type": "string",
                      "enum": [
                        "recipient",
                        "return"
                      ]
                    },
                    "x": {
                      "type": "number"
                    },
                    "y": {
                      "type": "number"
                    },
                    "width": {
                      "type": "number"
                    },
                    "height": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          },
          "notes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Research notes behind the spec."
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Source URLs."
          },
          "templates": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "code": {
                  "type": "string",
                  "examples": [
                    "postcard_6x9_service"
                  ]
                },
                "version": {
                  "type": "integer"
                },
                "name": {
                  "type": "string",
                  "examples": [
                    "Service reminder"
                  ]
                },
                "description": {
                  "type": "string"
                },
                "compliance": {
                  "type": "object",
                  "description": "Rules preflight applies. offer_disclosure_slot: the slot that must carry the full terms when copy states a Reg Z or Reg M trigger term. prescreen: always for a template built as a prescreened credit offer (FCRA notices, firm_offer and prescreen_cra required); when_mentioned when only pre-approval language in the copy makes it one. These are pattern checks, not legal review.",
                  "properties": {
                    "offer_disclosure_slot": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "examples": [
                        "offer_disclosure"
                      ]
                    },
                    "prescreen": {
                      "type": "string",
                      "enum": [
                        "always",
                        "when_mentioned"
                      ]
                    }
                  }
                },
                "slots": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "required": [
                      "name",
                      "label",
                      "kind",
                      "max_length",
                      "required",
                      "sample"
                    ],
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "label": {
                        "type": "string"
                      },
                      "kind": {
                        "type": "string",
                        "enum": [
                          "text",
                          "multiline",
                          "offer",
                          "disclosure",
                          "phone",
                          "date"
                        ],
                        "description": "What the slot holds. Informational: preflight checks length and compliance rules, not the kind."
                      },
                      "max_length": {
                        "type": "integer"
                      },
                      "required": {
                        "type": "boolean"
                      },
                      "description": {
                        "type": "string"
                      },
                      "sample": {
                        "type": "string",
                        "description": "Example copy. The samples of a template, sent as copy, pass preflight."
                      },
                      "default": {
                        "type": "string",
                        "description": "Optional slots only. generate_creative fills it in when the caller leaves the slot out; send the slot to replace it."
                      }
                    }
                  }
                }
              }
            },
            "description": "The product's active templates, one (the highest active version) per code. Retired templates are not listed. Layout regions are not returned."
          }
        }
      },
      "ProductList": {
        "type": "object",
        "properties": {
          "products": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Product"
            }
          },
          "placeholder_pricing": {
            "type": "boolean"
          },
          "pricing_note": {
            "type": "string"
          },
          "specs_note": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Dealer": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": "string"
          },
          "address": {
            "$ref": "#/components/schemas/Address"
          },
          "spend_cap_cents": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Cap on submitted spend per calendar month (UTC), counted per mode. Null means no cap."
          },
          "registered_approvers": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Masked emails (j***@dealer.com) of the people Xcel registered to approve live orders. Read-only."
          },
          "profile_version": {
            "type": [
              "integer",
              "null"
            ]
          },
          "live_ready": {
            "type": "boolean",
            "description": "True once Xcel has registered an approver; live request_approval and submit need it (no_registered_approver)."
          },
          "updash_linked": {
            "type": "boolean"
          },
          "created_by": {
            "type": "string",
            "enum": [
              "xcel",
              "partner",
              "updash_seed"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DealerList": {
        "type": "object",
        "properties": {
          "dealers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Dealer"
            }
          },
          "next_after": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Pass as after for the next page; null after the last."
          },
          "limit": {
            "type": "integer"
          }
        }
      },
      "BrandKit": {
        "type": "object",
        "properties": {
          "dealer_id": {
            "type": "string",
            "format": "uuid"
          },
          "dealer_name": {
            "type": "string"
          },
          "address": {
            "$ref": "#/components/schemas/Address"
          },
          "logo_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "A logo URL Xcel set; null for an uploaded logo (see logo)."
          },
          "colors": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "disclaimers": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "phone": {
            "type": [
              "string",
              "null"
            ],
            "description": "The profile's phones.main, formatted (555) 010-0199. Printed as {{brand.phone}} and as the fallback when a campaign sends no copy.cta_phone."
          },
          "locked": {
            "type": "boolean"
          },
          "registered_approvers": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Masked; set by Xcel only."
          },
          "note": {
            "type": "string"
          },
          "logo": {
            "$ref": "#/components/schemas/Logo"
          },
          "profile_version": {
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "description": "What a new creative prints: name, address, logo, colors and phone from the dealer's current profile; disclaimers, locked and the masked approvers from Xcel."
      },
      "Campaign": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "mode": {
            "type": "string",
            "enum": [
              "test",
              "live"
            ]
          },
          "name": {
            "type": "string"
          },
          "dealer_id": {
            "type": "string"
          },
          "product_code": {
            "type": "string"
          },
          "requested_mail_date": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "$ref": "#/components/schemas/CampaignStatus"
          },
          "status_label": {
            "type": "string"
          },
          "status_evidence": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "usps_processing_scan",
              null
            ]
          },
          "creative_version": {
            "type": [
              "integer",
              "null"
            ]
          },
          "audience_version": {
            "type": [
              "integer",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CampaignList": {
        "type": "object",
        "properties": {
          "campaigns": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Campaign"
            }
          }
        }
      },
      "CreateCampaignRequest": {
        "type": "object",
        "required": [
          "dealer_id",
          "product_code",
          "name"
        ],
        "properties": {
          "dealer_id": {
            "type": "string",
            "description": "Xcel dealer id or your external dealer id.",
            "minLength": 1,
            "maxLength": 100
          },
          "product_code": {
            "type": "string",
            "examples": [
              "postcard_6x9",
              "postcard_6x11",
              "letter_8_5x11_10win",
              "letter_8_5x14"
            ],
            "minLength": 1,
            "maxLength": 64,
            "description": "A product code from `GET /v1/products`: postcard_6x9, postcard_6x11, letter_8_5x11_10win or letter_8_5x14. Any other code is refused with 400 `unknown_product`."
          },
          "name": {
            "type": "string",
            "maxLength": 120,
            "minLength": 1,
            "description": "1 to 120 characters after trimming."
          },
          "requested_mail_date": {
            "type": "string",
            "format": "date",
            "description": "YYYY-MM-DD, not in the past (UTC). Optional."
          }
        }
      },
      "CampaignCreated": {
        "type": "object",
        "properties": {
          "campaign": {
            "$ref": "#/components/schemas/Campaign"
          },
          "next_step": {
            "type": "string"
          }
        }
      },
      "Reconciliation": {
        "type": "object",
        "description": "Counts, reason codes and spreadsheet row numbers only. Never row values.",
        "properties": {
          "submitted": {
            "type": "integer"
          },
          "mailable": {
            "type": "integer"
          },
          "rejected": {
            "type": "integer"
          },
          "rejected_by_reason": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "rejected_rows": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "row": {
                  "type": "integer",
                  "description": "Spreadsheet row; the header is row 1."
                },
                "reason": {
                  "type": "string",
                  "enum": [
                    "missing_first_name",
                    "missing_last_name",
                    "missing_address1",
                    "missing_city",
                    "missing_state",
                    "missing_zip",
                    "invalid_state",
                    "invalid_zip",
                    "field_too_long",
                    "wrong_column_count",
                    "duplicate",
                    "suppressed"
                  ]
                }
              }
            }
          },
          "rejected_rows_truncated": {
            "type": "boolean"
          },
          "warnings": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Counts per warning code: zip_leading_zero_restored, invalid_vin_blanked, invalid_vehicle_year_blanked, duplicate_external_id, external_id_blank, external_id_contact_info_blanked (an external_id that looked like an email address or phone number was blanked; the row still mails)."
          },
          "columns": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "blank_counts": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "suppression_list_rows": {
            "type": "integer"
          },
          "address_processing": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "pending",
                  "processed"
                ]
              },
              "processor": {
                "type": "string",
                "examples": [
                  "manual_accutrace",
                  "sandbox_pass_all"
                ]
              },
              "note": {
                "type": "string"
              },
              "processed_at": {
                "type": "string",
                "format": "date-time"
              },
              "cass_failed": {
                "type": "integer"
              },
              "ncoa_moved": {
                "type": "integer"
              }
            }
          }
        }
      },
      "Audience": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "campaign_id": {
            "type": "string"
          },
          "version": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "awaiting_upload",
              "rejected",
              "pending_address_processing",
              "ready"
            ]
          },
          "submitted_count": {
            "type": [
              "integer",
              "null"
            ]
          },
          "mailable_count": {
            "type": [
              "integer",
              "null"
            ]
          },
          "rejected_count": {
            "type": [
              "integer",
              "null"
            ]
          },
          "reconciliation": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Reconciliation"
              },
              {
                "type": "null"
              }
            ]
          },
          "retention_expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "purged_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the retention job deleted this audience's files; null until then. Counts and the reconciliation report remain."
          }
        }
      },
      "StartAudienceRequest": {
        "type": "object",
        "properties": {
          "include_suppression_list": {
            "type": "boolean",
            "description": "Also return suppression_upload_url for a do-not-mail CSV (address1, zip, optional first_name/last_name)."
          }
        }
      },
      "AudienceUpload": {
        "type": "object",
        "properties": {
          "audience_id": {
            "type": "string"
          },
          "version": {
            "type": "integer"
          },
          "upload_url": {
            "type": "string"
          },
          "upload_method": {
            "type": "string",
            "enum": [
              "PUT"
            ]
          },
          "upload_content_type": {
            "type": "string"
          },
          "upload_expires_in_seconds": {
            "type": "integer"
          },
          "suppression_upload_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "required_columns": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "optional_columns": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "next_step": {
            "type": "string"
          }
        }
      },
      "CompleteAudienceRequest": {
        "type": "object",
        "required": [
          "audience_id"
        ],
        "properties": {
          "audience_id": {
            "type": "string",
            "maxLength": 64,
            "minLength": 1,
            "description": "The audience_id from POST /v1/campaigns/{id}/audience."
          }
        }
      },
      "AudienceCompleted": {
        "type": "object",
        "properties": {
          "audience": {
            "$ref": "#/components/schemas/Audience"
          },
          "campaign_status": {
            "$ref": "#/components/schemas/CampaignStatus"
          }
        }
      },
      "PreflightIssue": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "One of: missing_copy_slot, slot_too_long, unused_copy_slot, unknown_placeholder, nested_copy_placeholder, brand_value_missing, missing_merge_field, merge_field_blank, audience_not_checked, locked_disclosure_missing, brand_logo_unused, script_not_allowed, active_content_not_allowed, remote_resource_not_allowed, print_css_not_allowed, css_invalid, address_block_missing, offer_disclosure_slot_missing, offer_disclosure_required, offer_disclosure_incomplete, prescreen_notice_missing, firm_offer_missing, prescreen_cra_missing, prescreen_guarantee_claim; and, from render_proof only, brand_logo_unreachable (field brand.logo: the logo URL failed its check and the proof shows a placeholder box with the dealer name), address_abbreviated, address_line_wrapped, address_block_overflow, and proof_font_substituted (PDF proofs: text set in a fallback font)."
          },
          "severity": {
            "type": "string",
            "enum": [
              "error",
              "warning"
            ]
          },
          "message": {
            "type": "string"
          },
          "fix": {
            "type": "string"
          },
          "field": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Preflight": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "checked_at": {
            "type": "string"
          },
          "audience_version": {
            "type": [
              "integer",
              "null"
            ]
          },
          "issues": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PreflightIssue"
            }
          }
        }
      },
      "GenerateCreativeRequest": {
        "type": "object",
        "description": "Send exactly one of template_code or html (`invalid_request` otherwise). A `brand` or `dealer` property is refused with `locked_field`, and so is a copy key that contains a dot, starts with brand, dealer, recipient or fcra, or is a brand field name (logo, logo_url, primary_color, secondary_color, accent_color, disclaimers, phone). Preflight then checks the copy, including pattern checks for Reg Z/Reg M offer disclosures and FCRA prescreen notices (not legal review). With template_code, optional slots the caller leaves out are filled with the template's default text where it has one. With template_code and a `brief`, Claude writes the copy slots `copy` leaves out (caller copy wins slot by slot); the response's `ai_copy` returns the draft with status `ready` or `needs_edit`. A brief without template_code is `invalid_request`; without AI copy configured it is `ai_copy_unavailable` (503); past the partner's daily cap, `ai_copy_rate_limited` (429).",
        "properties": {
          "template_code": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64,
            "description": "A template code for the campaign's product (see GET /v1/products)."
          },
          "html": {
            "type": "string",
            "maxLength": 200000,
            "minLength": 1,
            "description": "Your own creative HTML. It needs an element with class xm-address and, when the dealer's kit locks disclosures, {{brand.disclaimers}}."
          },
          "copy": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "maxLength": 5000
            },
            "description": "Copy slot values. May contain {{recipient.<column>}}, {{recipient.vehicle}} and {{field|fallback}} defaults; a fallback of @brand.<field>, @dealer.<field> or @recipient.<field> uses that field, e.g. {{copy.cta_phone|@brand.phone}} in a template. With a brief, these values win over generated copy, slot by slot.",
            "propertyNames": {
              "type": "string",
              "pattern": "^[a-z][a-z0-9_]{0,39}$"
            }
          },
          "brief": {
            "type": "object",
            "description": "Template creatives only. Claude writes the slots you leave out from this brief, in the dealer's voice and within each slot's limits. It never writes phones, disclosures, hours or signer names, uses only figures from the brief, and states credit or lease figures only with offer_terms. Sent to the model with the template's slot rules, the dealer's name and city, the brand kit's disclosures and merge-field names; never audience rows, recipient values or your copy. Never put customer data in a brief.",
            "required": [
              "goal",
              "offer"
            ],
            "properties": {
              "goal": {
                "type": "string",
                "minLength": 1,
                "maxLength": 300,
                "description": "What the piece should achieve."
              },
              "offer": {
                "type": "string",
                "minLength": 1,
                "maxLength": 400,
                "description": "The offer in plain words with its real figures and end date. Generated copy uses only figures given here or in offer_terms."
              },
              "tone": {
                "type": "string",
                "minLength": 1,
                "maxLength": 100,
                "description": "Voice of the copy. Default: warm, clear and direct."
              },
              "audience": {
                "type": "string",
                "minLength": 1,
                "maxLength": 300,
                "description": "Who receives it, in general terms. Never names or customer data."
              },
              "offer_terms": {
                "type": "object",
                "description": "Structured credit (finance) or lease terms. The offer disclosure is filled from them unless copy.offer_disclosure is sent. Finance: apr_percent required, down_payment_cents required with monthly_payment_cents; no lease fields. Lease: monthly_payment_cents and due_at_signing_cents required; no apr_percent, down_payment_cents or amount_financed_cents.",
                "required": [
                  "type",
                  "term_months"
                ],
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "finance",
                      "lease"
                    ]
                  },
                  "term_months": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 120,
                    "description": "Number of monthly payments."
                  },
                  "monthly_payment_cents": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100000000,
                    "description": "Monthly payment in cents."
                  },
                  "apr_percent": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 36,
                    "description": "Annual percentage rate, e.g. 6.49. Finance only."
                  },
                  "down_payment_cents": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100000000,
                    "description": "Down payment in cents. Finance only."
                  },
                  "amount_financed_cents": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100000000,
                    "description": "Amount financed in cents. Finance only."
                  },
                  "due_at_signing_cents": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100000000,
                    "description": "Total due at signing in cents. Lease only."
                  },
                  "miles_per_year": {
                    "type": "integer",
                    "minimum": 1000,
                    "maximum": 100000,
                    "description": "Mileage allowance per year. Lease only."
                  },
                  "excess_mileage_cents": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 500,
                    "description": "Charge per mile over the allowance, in cents. Lease only."
                  },
                  "conditions": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200,
                    "description": "Your own fine print, printed after the terms as written."
                  }
                }
              }
            }
          }
        }
      },
      "CreativeGenerated": {
        "type": "object",
        "properties": {
          "creative": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "campaign_id": {
                "type": "string"
              },
              "version": {
                "type": "integer"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "template",
                  "html",
                  "pdf"
                ]
              },
              "template_code": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "template_version": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "copy_slots": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "preflight": {
                "$ref": "#/components/schemas/Preflight"
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "campaign_status": {
            "$ref": "#/components/schemas/CampaignStatus"
          },
          "ai_copy": {
            "type": [
              "object",
              "null"
            ],
            "description": "The AI draft when the request had a brief; null otherwise.",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "ready",
                  "needs_edit"
                ],
                "description": "ready: preflight passes and no AI check is left. needs_edit: the draft is stored but needs your edits."
              },
              "model": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The model that answered; null when your copy covered every slot and no call was made."
              },
              "rounds": {
                "type": "integer",
                "minimum": 0,
                "maximum": 2,
                "description": "Model calls made: the draft and at most one repair."
              },
              "generated_slots": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "offer_terms_slots": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Slots filled from brief.offer_terms (the offer disclosure)."
              },
              "copy": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                },
                "description": "The generated slot text and the disclosure from the offer terms. Your copy is not repeated here."
              },
              "issues": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/PreflightIssue"
                },
                "description": "Warnings naming each slot removed from the draft (ai_* codes) or ai_output_invalid."
              }
            }
          },
          "next_step": {
            "type": "string"
          }
        }
      },
      "ProofResponse": {
        "type": "object",
        "properties": {
          "campaign_id": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/CampaignStatus"
          },
          "creative_version": {
            "type": "integer"
          },
          "audience_version": {
            "type": "integer"
          },
          "renderer": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "examples": [
                  "chromium_pdf",
                  "chrome_pdf",
                  "html_preview"
                ],
                "description": "chromium_pdf on the hosted service, chrome_pdf on a local stack with CHROME_PATH, html_preview where PDF rendering is off."
              },
              "format": {
                "type": "string",
                "enum": [
                  "html",
                  "pdf"
                ],
                "description": "pdf when the review and print-ready PDFs were printed and stored; html for an HTML preview only."
              },
              "pdf_available": {
                "type": "boolean",
                "description": "True when the PDFs exist. They are never returned by the API: the approver sees the review proof in the review page (served inline by /review/{token}/proof.pdf), Xcel ops get both PDFs from the dashboard."
              },
              "message": {
                "type": "string"
              }
            }
          },
          "pages": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "label": {
                  "type": "string"
                },
                "source_row": {
                  "type": [
                    "integer",
                    "null"
                  ]
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PreflightIssue"
            }
          },
          "preview": {
            "type": "string"
          },
          "next_step": {
            "type": "string"
          }
        }
      },
      "ValidateResponse": {
        "type": "object",
        "properties": {
          "campaign_id": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/CampaignStatus"
          },
          "ready_for_proof": {
            "type": "boolean"
          },
          "ready_for_quote": {
            "type": "boolean"
          },
          "ready_for_approval": {
            "type": "boolean"
          },
          "checks": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "enum": [
                    "audience",
                    "creative",
                    "preflight",
                    "quantity",
                    "mail_date"
                  ],
                  "description": "quantity appears only under the Marketing Mail presort minimum; mail_date only when a mail date was requested. mail_date does not affect the ready_for_* flags."
                },
                "ok": {
                  "type": "boolean"
                },
                "detail": {
                  "type": "string"
                }
              }
            }
          },
          "preflight": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Preflight"
              },
              {
                "type": "null"
              }
            ]
          },
          "next_step": {
            "type": "string"
          }
        }
      },
      "QuoteRequest": {
        "type": "object",
        "properties": {
          "mail_class": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MailClass"
              }
            ],
            "default": "marketing_mail"
          }
        }
      },
      "Quote": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "campaign_id": {
            "type": "string"
          },
          "creative_version": {
            "type": "integer"
          },
          "audience_version": {
            "type": "integer"
          },
          "quantity": {
            "type": "integer"
          },
          "mail_class": {
            "$ref": "#/components/schemas/MailClass"
          },
          "pooled_weekly": {
            "type": "boolean"
          },
          "unit_price_cents": {
            "type": "integer"
          },
          "total_cents": {
            "type": "integer"
          },
          "line_items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "code": {
                  "type": "string"
                },
                "description": {
                  "type": "string"
                },
                "quantity": {
                  "type": "integer"
                },
                "unit_price_cents": {
                  "type": "integer"
                },
                "total_cents": {
                  "type": "integer"
                }
              }
            }
          },
          "notes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "placeholder_pricing": {
            "type": "boolean"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "QuoteResponse": {
        "type": "object",
        "properties": {
          "quote": {
            "$ref": "#/components/schemas/Quote"
          },
          "next_step": {
            "type": "string"
          }
        }
      },
      "ApprovalRequest": {
        "type": "object",
        "required": [
          "quote_id",
          "max_spend_cents"
        ],
        "properties": {
          "quote_id": {
            "type": "string",
            "maxLength": 64,
            "minLength": 1,
            "description": "quote.id from POST /v1/campaigns/{id}/quote, for the current creative and audience versions and not expired."
          },
          "max_spend_cents": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10000000000,
            "description": "Ceiling the approver accepts, in cents. At least the quote total."
          },
          "approver_email": {
            "type": "string",
            "format": "email",
            "description": "Live: one of the dealer's registered approvers; optional when exactly one is registered. Sandbox: any address.",
            "maxLength": 254
          }
        }
      },
      "Approval": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "campaign_id": {
            "type": "string"
          },
          "quote_id": {
            "type": "string"
          },
          "creative_version": {
            "type": "integer"
          },
          "audience_version": {
            "type": "integer"
          },
          "max_spend_cents": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "rejected",
              "expired"
            ]
          },
          "approver_email": {
            "type": [
              "string",
              "null"
            ],
            "description": "Masked, e.g. j***@dealer.com."
          },
          "decided_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ApprovalRequestedLive": {
        "type": "object",
        "required": [
          "approval_id",
          "status",
          "approver_email",
          "total_cents",
          "expires_at"
        ],
        "description": "Live: the review link went by email to the approver and is not included.",
        "properties": {
          "approval_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending"
            ]
          },
          "approver_email": {
            "type": [
              "string",
              "null"
            ],
            "description": "Masked, e.g. j***@dealer.com. Always set for live; null in the sandbox when no approver_email was sent."
          },
          "total_cents": {
            "type": "integer"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "next_step": {
            "type": "string"
          }
        }
      },
      "ApprovalRequestedSandbox": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ApprovalRequestedLive"
          },
          {
            "type": "object",
            "required": [
              "review_url",
              "sandbox_only"
            ],
            "properties": {
              "review_url": {
                "type": "string",
                "format": "uri"
              },
              "sandbox_only": {
                "type": "boolean",
                "enum": [
                  true
                ]
              }
            }
          }
        ]
      },
      "SubmitRequest": {
        "type": "object",
        "required": [
          "approval_id"
        ],
        "properties": {
          "approval_id": {
            "type": "string",
            "maxLength": 64,
            "minLength": 1,
            "description": "The approval_id from request approval, after a human approved it."
          }
        }
      },
      "SubmitResponse": {
        "type": "object",
        "properties": {
          "submission_id": {
            "type": "string"
          },
          "campaign_id": {
            "type": "string"
          },
          "approval_id": {
            "type": "string"
          },
          "mode": {
            "type": "string",
            "enum": [
              "test",
              "live"
            ]
          },
          "status": {
            "$ref": "#/components/schemas/CampaignStatus"
          },
          "quantity": {
            "type": "integer"
          },
          "mail_class": {
            "$ref": "#/components/schemas/MailClass"
          },
          "total_cents": {
            "type": "integer"
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time"
          },
          "in_ops_queue": {
            "type": "boolean"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "CampaignStatusResponse": {
        "type": "object",
        "properties": {
          "campaign": {
            "$ref": "#/components/schemas/Campaign"
          },
          "status_description": {
            "type": "string"
          },
          "evidence_note": {
            "type": "string",
            "description": "Present only for statuses backed by USPS processing scans."
          },
          "audience": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Audience"
              },
              {
                "type": "null"
              }
            ]
          },
          "pending_audience_upload": {
            "type": [
              "object",
              "null"
            ],
            "description": "An upload that was started but not completed.",
            "properties": {
              "audience_id": {
                "type": "string"
              },
              "version": {
                "type": "integer"
              }
            }
          },
          "creative": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "version": {
                "type": "integer"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "template",
                  "html",
                  "pdf"
                ]
              },
              "preflight_ok": {
                "type": "boolean"
              }
            }
          },
          "latest_approval": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Approval"
              },
              {
                "type": "null"
              }
            ]
          },
          "submission": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "approval_id": {
                "type": "string"
              },
              "total_cents": {
                "type": "integer"
              },
              "submitted_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "in_ops_queue": {
            "type": "boolean"
          },
          "next_step": {
            "type": "string"
          }
        }
      },
      "ResultsResponse": {
        "type": "object",
        "properties": {
          "campaign_id": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/CampaignStatus"
          },
          "status_label": {
            "type": "string"
          },
          "quantities": {
            "type": "object",
            "properties": {
              "submitted_rows": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "mailable": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "rejected_rows": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "ordered": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            }
          },
          "submitted_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "timeline": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "at": {
                  "type": "string"
                },
                "status": {
                  "$ref": "#/components/schemas/CampaignStatus"
                },
                "label": {
                  "type": "string"
                },
                "evidence": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          },
          "evidence_note": {
            "type": "string"
          },
          "results": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ResultsAggregates"
              },
              {
                "type": "null"
              }
            ],
            "description": "Null until the first results arrive: a live order after Xcel links it to UpDash, a sandbox order once the test advance reaches in_postal_network."
          },
          "results_note": {
            "type": "string",
            "description": "Where the results come from, or why there are none yet."
          },
          "leads": {
            "type": "object",
            "description": "One page of per-lead results, ordered by row_key. Empty once the order's per-lead results are deleted, RESULTS_RETENTION_DAYS (365 by default) after it completes or is cancelled; results stays.",
            "properties": {
              "items": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/LeadResult"
                }
              },
              "next_after": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Pass as leads_after for the next page; null on the last page."
              },
              "limit": {
                "type": "integer"
              }
            }
          }
        }
      },
      "ResultsAggregates": {
        "type": "object",
        "description": "Campaign totals from Xcel's tracking (UpDash), or simulated in the sandbox.",
        "properties": {
          "source": {
            "type": "string",
            "enum": [
              "updash",
              "sandbox"
            ]
          },
          "simulated": {
            "type": "boolean",
            "description": "True for sandbox results: realistic shape, invented values."
          },
          "job": {
            "type": [
              "string",
              "null"
            ],
            "description": "The UpDash job number (`SBX-...` in the sandbox)."
          },
          "drops": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ResultsDrop"
            }
          },
          "delivery_percent": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100,
            "description": "Percent of pieces with USPS processing scans. Processing evidence, not proof of delivery."
          },
          "responses": {
            "type": "object",
            "description": "Response counts by channel: leads, sms, web, calls, appointments, showroom_visits, facebook, qr_codes, total, unique_responders. A channel UpDash does not report is absent.",
            "additionalProperties": {
              "type": "number",
              "minimum": 0
            }
          },
          "matched": {
            "type": "object",
            "description": "Responders matched to the dealer's sales and service records.",
            "properties": {
              "sales": {
                "type": "number",
                "minimum": 0
              },
              "repair_orders": {
                "type": "number",
                "minimum": 0
              }
            }
          },
          "leads_with_results": {
            "type": "integer",
            "minimum": 0,
            "description": "Mailed leads with at least one response or match."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ResultsDrop": {
        "type": "object",
        "properties": {
          "drop_number": {
            "type": [
              "integer",
              "null"
            ]
          },
          "home_date": {
            "type": [
              "string",
              "null"
            ],
            "description": "In-home date, YYYY-MM-DD."
          },
          "project_status": {
            "type": [
              "string",
              "null"
            ],
            "description": "UpDash's drop status, for example `At Vendor` or `Completed`."
          },
          "quantity": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "LeadResult": {
        "type": "object",
        "description": "One mailed lead's results. Never a name or an address.",
        "properties": {
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your id from the audience's external_id column; null when the row had none."
          },
          "row_key": {
            "type": "string",
            "pattern": "^xm[a-z2-7]{16}$",
            "description": "The row key the Mail API gave the row (the ResponseCode column of the mailable file)."
          },
          "responses": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LeadResponse"
            }
          },
          "matches": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LeadMatch"
            }
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "LeadResponse": {
        "type": "object",
        "properties": {
          "type": {
            "type": "integer",
            "description": "UpDash response type: 1 call, 2 web, 3 showroom visit, 4 appointment, 7 QR code, and others."
          },
          "type_label": {
            "type": [
              "string",
              "null"
            ],
            "description": "For example `call`, `web`, `qr_code`, `appointment`."
          },
          "at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the response happened, ISO 8601 UTC."
          }
        }
      },
      "LeadMatch": {
        "type": "object",
        "description": "A sale or service visit that Xcel's matcher tied to the lead. Figures come from the dealer's DMS: confidential dealer data, shared under the partner agreement.",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "sale",
              "service"
            ]
          },
          "date": {
            "type": [
              "string",
              "null"
            ]
          },
          "vehicle_type": {
            "type": [
              "string",
              "null"
            ],
            "description": "For example `New` or `Used`."
          },
          "gross_cents": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Matched gross profit in cents (sales); null when the DMS has none."
          },
          "revenue_cents": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Matched revenue in cents: the sale price or the repair order total."
          }
        }
      },
      "AdvanceRequest": {
        "type": "object",
        "properties": {
          "to": {
            "type": "string",
            "enum": [
              "submitted",
              "in_production",
              "at_usps",
              "in_postal_network",
              "delivery_estimated",
              "completed"
            ]
          }
        }
      },
      "AdvanceResponse": {
        "type": "object",
        "properties": {
          "campaign_id": {
            "type": "string"
          },
          "from": {
            "$ref": "#/components/schemas/CampaignStatus"
          },
          "status": {
            "$ref": "#/components/schemas/CampaignStatus"
          },
          "status_label": {
            "type": "string"
          },
          "status_evidence": {
            "type": [
              "string",
              "null"
            ]
          },
          "simulated": {
            "type": "boolean"
          },
          "results_simulated": {
            "type": "boolean",
            "description": "True when this step stored simulated results (from in_postal_network on)."
          }
        }
      },
      "CreateWebhookRequest": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2000,
            "description": "Public URL without credentials. Live keys: https only. Sandbox keys: https or http. The host must resolve, and only to public addresses; it is checked again before every delivery."
          },
          "event_types": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "campaign.status_changed",
                "audience.completed",
                "audience.address_processed",
                "audience.purged",
                "creative.generated",
                "quote.created",
                "approval.requested",
                "approval.decided",
                "campaign.results_updated",
                "campaign.lead_results_updated",
                "dealer.updated"
              ]
            },
            "description": "Empty or omitted means all."
          }
        }
      },
      "Webhook": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "mode": {
            "type": "string",
            "enum": [
              "test",
              "live"
            ]
          },
          "url": {
            "type": "string"
          },
          "event_types": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "campaign.status_changed",
                "audience.completed",
                "audience.address_processed",
                "audience.purged",
                "creative.generated",
                "quote.created",
                "approval.requested",
                "approval.decided",
                "campaign.results_updated",
                "campaign.lead_results_updated"
              ]
            },
            "description": "Empty means every event type."
          },
          "active": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "deactivated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When you deactivated it with DELETE /v1/webhooks/{id}; null while active."
          },
          "secret_rotated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the secret was last rotated; null if never."
          }
        }
      },
      "WebhookList": {
        "type": "object",
        "properties": {
          "webhooks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookListItem"
            },
            "description": "Your endpoints in the key's mode, active and deactivated, oldest first. Never a secret."
          },
          "limit": {
            "type": "object",
            "properties": {
              "active_max": {
                "type": "integer"
              },
              "active": {
                "type": "integer"
              }
            },
            "description": "Active endpoints allowed per mode, and how many you have."
          }
        }
      },
      "WebhookCreated": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Webhook"
          },
          {
            "type": "object",
            "properties": {
              "secret": {
                "type": "string",
                "description": "Signing secret, `whsec_` plus 32 base64url characters. Shown once; sign with the whole string, prefix included."
              },
              "signature": {
                "type": "object",
                "properties": {
                  "header": {
                    "type": "string",
                    "enum": [
                      "X-Xcel-Signature"
                    ]
                  },
                  "format": {
                    "type": "string"
                  },
                  "note": {
                    "type": "string"
                  }
                }
              }
            },
            "required": [
              "secret",
              "signature"
            ]
          }
        ]
      },
      "WebhookEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "campaign.status_changed",
              "audience.completed",
              "audience.address_processed",
              "audience.purged",
              "creative.generated",
              "quote.created",
              "approval.requested",
              "approval.decided",
              "campaign.results_updated",
              "campaign.lead_results_updated",
              "dealer.updated"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "description": "campaign_id, mode and event fields (ids, statuses, counts). USPS-derived statuses carry evidence: usps_processing_scan. campaign.results_updated carries the results aggregates; campaign.lead_results_updated names the changed leads (external_id and row_key, up to 500 per event, numbered part of parts) without their responses or matches. dealer.updated has no campaign_id or mode: dealer_id, external_id, version, changed_fields (names, never values) and actor_type; it goes to the partner's endpoints in both modes.",
            "additionalProperties": true
          }
        }
      },
      "WebhookDelivery": {
        "type": "object",
        "description": "The endpoint's most recent delivery.",
        "properties": {
          "event_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "delivered",
              "failed",
              "cancelled"
            ],
            "description": "cancelled: the endpoint was deactivated before it was sent."
          },
          "attempts": {
            "type": "integer"
          },
          "http_status": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Your endpoint's HTTP status on the last attempt."
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "A short code for the last failure, e.g. timeout or webhook_deactivated."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "delivered_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "next_attempt_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "The next retry, while pending."
          }
        }
      },
      "WebhookListItem": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Webhook"
          },
          {
            "type": "object",
            "properties": {
              "last_delivery": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/WebhookDelivery"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          }
        ]
      },
      "WebhookDeactivated": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Webhook"
          },
          {
            "type": "object",
            "properties": {
              "pending_deliveries_cancelled": {
                "type": "integer",
                "description": "Deliveries cancelled by this call; 0 when the endpoint was already deactivated."
              }
            },
            "required": [
              "pending_deliveries_cancelled"
            ]
          }
        ]
      },
      "DealerProfile": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name",
          "address"
        ],
        "description": "The partner-owned profile. Every key is present in responses (null or empty when unset). Unknown fields are refused (invalid_profile); Xcel-managed fields (approvers, approver_emails, registered_approvers, disclaimers, disclosures, license_number, dealer_license_number, spend_cap_cents, ordering_enabled, brand.disclaimers) are refused with xcel_managed_field.",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "legal_name": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          },
          "alias": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 60,
            "description": "Short name for scripts and agents."
          },
          "pronunciation": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          },
          "dealer_group": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          },
          "oem_dealer_codes": {
            "type": "array",
            "maxItems": 20,
            "items": {
              "type": "object",
              "required": [
                "make",
                "code"
              ],
              "additionalProperties": false,
              "properties": {
                "make": {
                  "type": "string",
                  "maxLength": 40
                },
                "code": {
                  "type": "string",
                  "maxLength": 40
                }
              }
            }
          },
          "makes": {
            "type": [
              "array",
              "null"
            ],
            "maxItems": 30,
            "items": {
              "type": "string",
              "maxLength": 40
            },
            "description": "Franchise makes. [] means used vehicles only; null means not stated."
          },
          "address": {
            "$ref": "#/components/schemas/Address"
          },
          "return_address": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Address"
              },
              {
                "type": "null"
              }
            ]
          },
          "service_area_zips": {
            "type": "array",
            "maxItems": 1000,
            "items": {
              "type": "string",
              "pattern": "^\\d{5}$"
            }
          },
          "phones": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "main": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "A 10-digit US number. Punctuation, spaces and a leading +1 are removed on write; stored and returned as 10 digits."
              },
              "sales": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "A 10-digit US number. Punctuation, spaces and a leading +1 are removed on write; stored and returned as 10 digits."
              },
              "service": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "A 10-digit US number. Punctuation, spaces and a leading +1 are removed on write; stored and returned as 10 digits."
              },
              "parts": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "A 10-digit US number. Punctuation, spaces and a leading +1 are removed on write; stored and returned as 10 digits."
              }
            },
            "description": "main prints when a campaign sends no tracked cta_phone."
          },
          "contact": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": false,
            "properties": {
              "name": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 120
              },
              "title": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 120
              },
              "email": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "email",
                "maxLength": 254
              }
            }
          },
          "notification_emails": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "campaign": {
                "type": [
                  "array",
                  "null"
                ],
                "maxItems": 10,
                "items": {
                  "type": "string",
                  "format": "email",
                  "maxLength": 254
                }
              },
              "service": {
                "type": [
                  "array",
                  "null"
                ],
                "maxItems": 10,
                "items": {
                  "type": "string",
                  "format": "email",
                  "maxLength": 254
                }
              }
            },
            "description": "[] means nobody; null means not stated."
          },
          "time_zone": {
            "type": [
              "string",
              "null"
            ],
            "description": "IANA name, e.g. America/Chicago."
          },
          "hours": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "sales": {
                "type": [
                  "object",
                  "null"
                ],
                "additionalProperties": false,
                "properties": {
                  "mon": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        },
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        }
                      ],
                      "minItems": 2,
                      "maxItems": 2,
                      "description": "[open, close], 24-hour; close after open."
                    },
                    "maxItems": 4,
                    "description": "Up to four intervals, in order, not overlapping. Empty: closed."
                  },
                  "tue": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        },
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        }
                      ],
                      "minItems": 2,
                      "maxItems": 2,
                      "description": "[open, close], 24-hour; close after open."
                    },
                    "maxItems": 4,
                    "description": "Up to four intervals, in order, not overlapping. Empty: closed."
                  },
                  "wed": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        },
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        }
                      ],
                      "minItems": 2,
                      "maxItems": 2,
                      "description": "[open, close], 24-hour; close after open."
                    },
                    "maxItems": 4,
                    "description": "Up to four intervals, in order, not overlapping. Empty: closed."
                  },
                  "thu": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        },
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        }
                      ],
                      "minItems": 2,
                      "maxItems": 2,
                      "description": "[open, close], 24-hour; close after open."
                    },
                    "maxItems": 4,
                    "description": "Up to four intervals, in order, not overlapping. Empty: closed."
                  },
                  "fri": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        },
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        }
                      ],
                      "minItems": 2,
                      "maxItems": 2,
                      "description": "[open, close], 24-hour; close after open."
                    },
                    "maxItems": 4,
                    "description": "Up to four intervals, in order, not overlapping. Empty: closed."
                  },
                  "sat": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        },
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        }
                      ],
                      "minItems": 2,
                      "maxItems": 2,
                      "description": "[open, close], 24-hour; close after open."
                    },
                    "maxItems": 4,
                    "description": "Up to four intervals, in order, not overlapping. Empty: closed."
                  },
                  "sun": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        },
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        }
                      ],
                      "minItems": 2,
                      "maxItems": 2,
                      "description": "[open, close], 24-hour; close after open."
                    },
                    "maxItems": 4,
                    "description": "Up to four intervals, in order, not overlapping. Empty: closed."
                  }
                }
              },
              "service": {
                "type": [
                  "object",
                  "null"
                ],
                "additionalProperties": false,
                "properties": {
                  "mon": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        },
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        }
                      ],
                      "minItems": 2,
                      "maxItems": 2,
                      "description": "[open, close], 24-hour; close after open."
                    },
                    "maxItems": 4,
                    "description": "Up to four intervals, in order, not overlapping. Empty: closed."
                  },
                  "tue": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        },
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        }
                      ],
                      "minItems": 2,
                      "maxItems": 2,
                      "description": "[open, close], 24-hour; close after open."
                    },
                    "maxItems": 4,
                    "description": "Up to four intervals, in order, not overlapping. Empty: closed."
                  },
                  "wed": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        },
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        }
                      ],
                      "minItems": 2,
                      "maxItems": 2,
                      "description": "[open, close], 24-hour; close after open."
                    },
                    "maxItems": 4,
                    "description": "Up to four intervals, in order, not overlapping. Empty: closed."
                  },
                  "thu": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        },
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        }
                      ],
                      "minItems": 2,
                      "maxItems": 2,
                      "description": "[open, close], 24-hour; close after open."
                    },
                    "maxItems": 4,
                    "description": "Up to four intervals, in order, not overlapping. Empty: closed."
                  },
                  "fri": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        },
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        }
                      ],
                      "minItems": 2,
                      "maxItems": 2,
                      "description": "[open, close], 24-hour; close after open."
                    },
                    "maxItems": 4,
                    "description": "Up to four intervals, in order, not overlapping. Empty: closed."
                  },
                  "sat": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        },
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        }
                      ],
                      "minItems": 2,
                      "maxItems": 2,
                      "description": "[open, close], 24-hour; close after open."
                    },
                    "maxItems": 4,
                    "description": "Up to four intervals, in order, not overlapping. Empty: closed."
                  },
                  "sun": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        },
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        }
                      ],
                      "minItems": 2,
                      "maxItems": 2,
                      "description": "[open, close], 24-hour; close after open."
                    },
                    "maxItems": 4,
                    "description": "Up to four intervals, in order, not overlapping. Empty: closed."
                  }
                }
              },
              "parts": {
                "type": [
                  "object",
                  "null"
                ],
                "additionalProperties": false,
                "properties": {
                  "mon": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        },
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        }
                      ],
                      "minItems": 2,
                      "maxItems": 2,
                      "description": "[open, close], 24-hour; close after open."
                    },
                    "maxItems": 4,
                    "description": "Up to four intervals, in order, not overlapping. Empty: closed."
                  },
                  "tue": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        },
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        }
                      ],
                      "minItems": 2,
                      "maxItems": 2,
                      "description": "[open, close], 24-hour; close after open."
                    },
                    "maxItems": 4,
                    "description": "Up to four intervals, in order, not overlapping. Empty: closed."
                  },
                  "wed": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        },
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        }
                      ],
                      "minItems": 2,
                      "maxItems": 2,
                      "description": "[open, close], 24-hour; close after open."
                    },
                    "maxItems": 4,
                    "description": "Up to four intervals, in order, not overlapping. Empty: closed."
                  },
                  "thu": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        },
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        }
                      ],
                      "minItems": 2,
                      "maxItems": 2,
                      "description": "[open, close], 24-hour; close after open."
                    },
                    "maxItems": 4,
                    "description": "Up to four intervals, in order, not overlapping. Empty: closed."
                  },
                  "fri": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        },
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        }
                      ],
                      "minItems": 2,
                      "maxItems": 2,
                      "description": "[open, close], 24-hour; close after open."
                    },
                    "maxItems": 4,
                    "description": "Up to four intervals, in order, not overlapping. Empty: closed."
                  },
                  "sat": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        },
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        }
                      ],
                      "minItems": 2,
                      "maxItems": 2,
                      "description": "[open, close], 24-hour; close after open."
                    },
                    "maxItems": 4,
                    "description": "Up to four intervals, in order, not overlapping. Empty: closed."
                  },
                  "sun": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        },
                        {
                          "type": "string",
                          "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                        }
                      ],
                      "minItems": 2,
                      "maxItems": 2,
                      "description": "[open, close], 24-hour; close after open."
                    },
                    "maxItems": 4,
                    "description": "Up to four intervals, in order, not overlapping. Empty: closed."
                  }
                }
              }
            }
          },
          "hours_exceptions": {
            "type": "array",
            "maxItems": 100,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "date",
                "department"
              ],
              "properties": {
                "date": {
                  "type": "string",
                  "format": "date"
                },
                "department": {
                  "type": "string",
                  "enum": [
                    "sales",
                    "service",
                    "parts",
                    "all"
                  ]
                },
                "closed": {
                  "type": "boolean"
                },
                "intervals": {
                  "type": "array",
                  "items": {
                    "type": "array",
                    "prefixItems": [
                      {
                        "type": "string",
                        "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                      },
                      {
                        "type": "string",
                        "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$|^24:00$"
                      }
                    ],
                    "minItems": 2,
                    "maxItems": 2,
                    "description": "[open, close], 24-hour; close after open."
                  },
                  "maxItems": 4
                }
              },
              "description": "closed: true, or a non-empty intervals list. One per date and department."
            }
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "website": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uri",
                "maxLength": 2000,
                "description": "https only."
              },
              "new_inventory": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uri",
                "maxLength": 2000,
                "description": "https only."
              },
              "used_inventory": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uri",
                "maxLength": 2000,
                "description": "https only."
              },
              "service_scheduler": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uri",
                "maxLength": 2000,
                "description": "https only."
              },
              "credit_app": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uri",
                "maxLength": 2000,
                "description": "https only."
              },
              "trade_in": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uri",
                "maxLength": 2000,
                "description": "https only."
              }
            }
          },
          "brand": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "colors": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "primary": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "pattern": "^#[0-9a-fA-F]{6}$"
                  },
                  "secondary": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "pattern": "^#[0-9a-fA-F]{6}$"
                  },
                  "accent": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "pattern": "^#[0-9a-fA-F]{6}$"
                  }
                }
              }
            }
          },
          "lead_routing": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": false,
            "description": "Stored and shown only; never sent anywhere or pushed to UpDash.",
            "properties": {
              "crm_provider": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 60
              },
              "adf_emails": {
                "type": "array",
                "maxItems": 10,
                "items": {
                  "type": "string",
                  "format": "email",
                  "maxLength": 254
                }
              },
              "webhook_url": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uri",
                "maxLength": 2000,
                "description": "https only."
              }
            }
          }
        }
      },
      "ProfilePatch": {
        "type": "object",
        "description": "JSON merge patch (RFC 7396) of DealerProfile: only the fields to change; objects merge, null clears a field (name and address cannot be cleared), lists replace. Send Content-Type application/json.",
        "additionalProperties": true
      },
      "Logo": {
        "type": [
          "object",
          "null"
        ],
        "description": "An uploaded logo (storage paths are never shown), or a logo URL Xcel set before uploads existed.",
        "properties": {
          "source": {
            "type": "string",
            "enum": [
              "upload",
              "url_fetch",
              "updash_seed",
              "url"
            ]
          },
          "url": {
            "type": "string",
            "description": "source url only."
          },
          "format": {
            "type": "string",
            "enum": [
              "png",
              "jpeg",
              "svg",
              "pdf"
            ]
          },
          "content_type": {
            "type": "string"
          },
          "bytes": {
            "type": "integer"
          },
          "sha256": {
            "type": "string"
          },
          "width_px": {
            "type": "integer",
            "description": "Of the upload (raster) or of the PNG rendition (SVG)."
          },
          "height_px": {
            "type": "integer"
          },
          "png": {
            "type": "object",
            "properties": {
              "bytes": {
                "type": "integer"
              },
              "sha256": {
                "type": "string"
              }
            },
            "description": "The PNG rendition proofs print."
          },
          "uploaded_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DealerResponse": {
        "type": "object",
        "properties": {
          "dealer": {
            "$ref": "#/components/schemas/Dealer"
          },
          "live_ready_note": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "DealerProfileResponse": {
        "type": "object",
        "properties": {
          "dealer_id": {
            "type": "string",
            "format": "uuid"
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "version": {
            "type": "integer",
            "description": "0 only for a dealer that has no version yet."
          },
          "etag": {
            "type": "string",
            "description": "The version as an ETag, e.g. \"3\". Also the ETag header."
          },
          "current": {
            "type": "boolean"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_by": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "api_key",
                  "oauth_client",
                  "console_user",
                  "xcel_admin",
                  "updash_seed"
                ]
              },
              "id": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "profile": {
            "$ref": "#/components/schemas/DealerProfile"
          },
          "logos": {
            "type": "object",
            "properties": {
              "primary": {
                "$ref": "#/components/schemas/Logo"
              },
              "reversed": {
                "$ref": "#/components/schemas/Logo"
              }
            }
          },
          "xcel_managed": {
            "type": "object",
            "description": "Read-only.",
            "properties": {
              "registered_approvers": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "disclaimers": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "license_number": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "spend_cap_cents": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "ordering_enabled": {
                "type": "boolean"
              }
            }
          },
          "live_ready": {
            "type": "boolean"
          },
          "live_ready_note": {
            "type": [
              "string",
              "null"
            ]
          },
          "updash_linked": {
            "type": "boolean"
          }
        }
      },
      "DealerProfileWritten": {
        "allOf": [
          {
            "$ref": "#/components/schemas/DealerProfileResponse"
          },
          {
            "type": "object",
            "properties": {
              "changed": {
                "type": "boolean",
                "description": "False when the write changed nothing (no new version). In a validated-only answer: whether a live key's write would change anything."
              },
              "changed_fields": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Names such as phones.main, never values."
              },
              "validated_only": {
                "type": "boolean",
                "description": "True when the write was made with a test (xm_test_) key: every check ran and nothing was saved (no version, no logo file, no dealer.updated webhook, no UpDash push). False for a stored write."
              },
              "would_be_version": {
                "type": "integer",
                "description": "Validated-only answers only: the version a live key's write would create (the same as version when nothing would change)."
              },
              "note": {
                "type": "string",
                "description": "Validated-only answers only: what happened and how to save the change."
              }
            },
            "description": "After a write. In a validated-only answer (test key), version and etag are the version that is still current, and profile is what would be stored."
          }
        ]
      },
      "LogoUploaded": {
        "allOf": [
          {
            "$ref": "#/components/schemas/DealerProfileWritten"
          },
          {
            "type": "object",
            "properties": {
              "variant": {
                "type": "string",
                "enum": [
                  "primary",
                  "reversed"
                ]
              },
              "logo": {
                "$ref": "#/components/schemas/Logo"
              },
              "warnings": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "For SVG: what the cleaning removed."
              }
            }
          }
        ]
      },
      "CreateDealerRequest": {
        "type": "object",
        "required": [
          "external_id",
          "profile"
        ],
        "properties": {
          "external_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100,
            "description": "Your id for the dealer, unique among your dealers."
          },
          "profile": {
            "$ref": "#/components/schemas/DealerProfile"
          }
        }
      },
      "DealerCreated": {
        "type": "object",
        "properties": {
          "validated_only": {
            "type": "boolean",
            "enum": [
              false
            ],
            "description": "False: the dealer was created. A test key's create answers 200 with DealerCreateValidated instead."
          },
          "dealer": {
            "$ref": "#/components/schemas/Dealer"
          },
          "profile": {
            "$ref": "#/components/schemas/DealerProfileResponse"
          },
          "next_step": {
            "type": "string"
          }
        }
      },
      "ProfileVersionList": {
        "type": "object",
        "properties": {
          "dealer_id": {
            "type": "string",
            "format": "uuid"
          },
          "current_version": {
            "type": "integer"
          },
          "versions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "version": {
                  "type": "integer"
                },
                "changed_fields": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "actor": {
                  "type": "object",
                  "properties": {
                    "type": {
                      "type": "string"
                    },
                    "id": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                },
                "created_at": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          },
          "next_before": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "LogoUploadJson": {
        "type": "object",
        "properties": {
          "variant": {
            "type": "string",
            "enum": [
              "primary",
              "reversed"
            ],
            "default": "primary"
          },
          "content_base64": {
            "type": "string",
            "maxLength": 4194308,
            "description": "The file, base64. Send this or url."
          },
          "url": {
            "type": "string",
            "maxLength": 2000,
            "description": "An https URL Xcel fetches: public host, no redirects, 3 MB at most."
          }
        }
      },
      "DealerImportResult": {
        "type": "object",
        "properties": {
          "validated_only": {
            "type": "boolean",
            "description": "True when a test key sent the file: every row was checked as a live key's import would be, and nothing was created or changed. Rows that would create a dealer have dealer_id null."
          },
          "note": {
            "type": "string",
            "description": "Validated-only answers only."
          },
          "rows": {
            "type": "integer"
          },
          "created": {
            "type": "integer"
          },
          "updated": {
            "type": "integer"
          },
          "unchanged": {
            "type": "integer"
          },
          "failed": {
            "type": "integer"
          },
          "results": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "row": {
                  "type": "integer",
                  "description": "Spreadsheet row; the header is row 1."
                },
                "external_id": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "created",
                    "updated",
                    "unchanged",
                    "failed"
                  ]
                },
                "dealer_id": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "uuid"
                },
                "version": {
                  "type": [
                    "integer",
                    "null"
                  ]
                },
                "changed_fields": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "error": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "properties": {
                    "code": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    },
                    "field": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      },
      "DealerCreateValidated": {
        "type": "object",
        "description": "A create with a test (xm_test_) key: checked, nothing created. Make the dealer with a live key.",
        "properties": {
          "validated_only": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "note": {
            "type": "string"
          },
          "dealer": {
            "type": "object",
            "description": "The dealer as it would be created; id, profile_version, created_at and updated_at are null."
          },
          "profile": {
            "type": "object",
            "description": "The profile as version 1 would hold it, in the DealerProfileResponse shape, with dealer_id and etag null, version 0 and would_be_version 1."
          },
          "next_step": {
            "type": "string"
          }
        }
      }
    }
  }
}