{
  "openapi": "3.1.0",
  "info": {
    "title": "AriaQR API",
    "version": "1.0.0",
    "summary": "Make verified picture QR codes, read scan analytics, change destinations and read landing-page leads.",
    "description": "The AriaQR REST API. One call turns an image and a destination into a verified, activated QR code. Authenticate with an API key sent as `Authorization: Bearer aq_…`. Keys come with an active quarter plan (read only: codes, stats, pages, leads) or Business plan (create and change too) and are created on the account page. Limit: 60 requests a minute per key. Every error is JSON, `{ \"error\": \"…\" }`, with the matching HTTP status. Human-readable docs with cURL, Node.js and Python examples: https://ariaqr.com/developers",
    "termsOfService": "https://ariaqr.com/terms",
    "contact": { "name": "AriaQR", "url": "https://ariaqr.com/developers" }
  },
  "servers": [{ "url": "https://ariaqr.com", "description": "Production" }],
  "externalDocs": { "description": "Developer docs and changelog", "url": "https://ariaqr.com/developers" },
  "security": [{ "bearerAuth": [] }],
  "tags": [
    { "name": "Codes", "description": "Create and list codes, read their analytics, change where they go." },
    { "name": "Landing pages", "description": "Read what a landing page's forms collected. Pages themselves are built over the MCP server." }
  ],
  "paths": {
    "/api/v1/codes": {
      "post": {
        "operationId": "createCode",
        "tags": ["Codes"],
        "summary": "Create a code",
        "description": "Generates a code from an image through the same engine and verification as the site and, unless `activate` is false, activates it on your plan. Send `image_url` or `image_base64`. The image must be square (1:1, within 2%), PNG, JPEG or WebP, up to 10 MB. A website code needs `url`; other kinds take `kind` and `fields`. The call can take up to a few minutes.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateCodeRequest" },
              "example": { "image_url": "https://example.com/artwork.png", "url": "https://example.com/menu", "edge": "soft" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The code was made (and activated, unless `activate` was false).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateCodeResponse" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" },
          "503": { "$ref": "#/components/responses/ServerError" }
        }
      },
      "get": {
        "operationId": "listCodes",
        "tags": ["Codes"],
        "summary": "List your codes",
        "description": "Every code on the account (and on teams you are still a member of), newest first.",
        "responses": {
          "200": {
            "description": "The account's codes.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CodeList" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/v1/codes/{code}": {
      "parameters": [
        { "name": "code", "in": "path", "required": true, "description": "The code, for example `a7Bx9Q`.", "schema": { "type": "string" }, "example": "a7Bx9Q" }
      ],
      "get": {
        "operationId": "getCodeStats",
        "tags": ["Codes"],
        "summary": "Get a code with its analytics",
        "description": "The code's summary plus scan analytics: totals, unique visitors, 90 days of daily counts and breakdowns by place, device and browser.",
        "responses": {
          "200": {
            "description": "The code and its analytics.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CodeWithStats" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      },
      "patch": {
        "operationId": "setDestination",
        "tags": ["Codes"],
        "summary": "Change where a code goes",
        "description": "Points the printed code at a new address without reprinting. The address is safety-checked first.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SetDestinationRequest" },
              "example": { "destination": "https://example.com/spring-menu" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated code.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CodeSummary" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/v1/pages/{id}/leads": {
      "parameters": [
        { "name": "id", "in": "path", "required": true, "description": "The landing page's id.", "schema": { "type": "string" } }
      ],
      "get": {
        "operationId": "listPageLeads",
        "tags": ["Landing pages"],
        "summary": "List a landing page's leads",
        "description": "What the page's forms collected, newest first, with the 30-day conversion and leads per printed code. Only the page's owner can read it. With `format=csv` the response is every lead as a CSV file instead.",
        "parameters": [
          { "name": "limit", "in": "query", "description": "How many leads to return (1 to 500).", "schema": { "type": "integer", "minimum": 1, "maximum": 500, "default": 50 } },
          { "name": "since_days", "in": "query", "description": "Only leads from the last this many days (1 to 365).", "schema": { "type": "integer", "minimum": 1, "maximum": 365 } },
          { "name": "format", "in": "query", "description": "`csv` returns the whole list as a CSV download.", "schema": { "type": "string", "enum": ["csv"] } }
        ],
        "responses": {
          "200": {
            "description": "The page's leads and conversion report (JSON), or a CSV file when `format=csv`.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/LeadReport" } },
              "text/csv": { "schema": { "type": "string" } }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "aq_…",
        "description": "An AriaQR API key, `Authorization: Bearer aq_…`. Create one on the account page (API & MCP). Needs an active quarter plan or Business plan."
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Bad input: a missing or unreadable image, a non-square or oversized image, a missing url, a destination that is not a full URL or fails the safety check.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Image must be square (1:1). Yours is 1200×800." } } }
      },
      "Unauthorized": {
        "description": "No key, a wrong key or a revoked key.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Missing or invalid API key. Send `Authorization: Bearer aq_…`." } } }
      },
      "PaymentRequired": {
        "description": "The account has no active quarter or Business plan, or (when creating) no activations left, or a create or change call came from a quarter plan, which can only read.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "API access needs an active quarter plan or Business plan. https://ariaqr.com/pricing" } } }
      },
      "Forbidden": {
        "description": "The `team_id` is a team you are not a member of.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "You are not a member of that team." } } }
      },
      "NotFound": {
        "description": "That code or landing page is not on this account.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "No such code on this account." } } }
      },
      "RateLimited": {
        "description": "More than 60 requests in a minute on one key. The message says how many seconds to wait.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Rate limit: 60 requests a minute. Retry in 12s." } } }
      },
      "ServerError": {
        "description": "Something failed on our side. Safe to retry; a failed build is never charged.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Something went wrong." } } }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": { "error": { "type": "string", "description": "A readable message." } }
      },
      "CreateCodeRequest": {
        "type": "object",
        "description": "Send `image_url` or `image_base64`. A website code (the default kind) also needs `url`.",
        "properties": {
          "image_url": { "type": "string", "format": "uri", "description": "Public URL of a square PNG, JPEG or WebP, up to 10 MB." },
          "image_base64": { "type": "string", "description": "The image as base64 (a data URL is fine), when it is not online." },
          "url": { "type": "string", "format": "uri", "description": "Where the code goes. Required for a website code." },
          "kind": { "type": "string", "default": "website", "description": "The code's kind.", "examples": ["website", "whatsapp", "vcard", "wifi", "upi", "email", "phone", "sms", "maps", "event", "text"] },
          "fields": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Fields for the chosen kind, with the same names as the generator on the site." },
          "edge": { "type": "string", "enum": ["soft", "seamless"], "description": "How the code meets the picture." },
          "activate": { "type": "boolean", "default": true, "description": "False generates without spending an activation; the response then carries `pay_url`." },
          "team_id": { "type": "string", "description": "Make the code in a team workspace you belong to." }
        }
      },
      "CreateCodeResponse": {
        "type": "object",
        "required": ["job_id", "verified", "activated", "preview_url"],
        "properties": {
          "job_id": { "type": "string" },
          "verified": { "type": "boolean", "description": "Whether the code passed verification." },
          "activated": { "type": "boolean" },
          "code": { "type": ["string", "null"], "description": "The code's short identifier." },
          "short_url": { "type": ["string", "null"], "description": "What the printed code points at, without the scheme.", "examples": ["aqr.li/a7Bx9Q"] },
          "preview_url": { "type": "string", "format": "uri" },
          "download_url": { "type": ["string", "null"], "format": "uri", "description": "The 2048px download. Null until the code is activated." },
          "stats_url": { "type": ["string", "null"], "format": "uri", "description": "A link to the analytics page." },
          "pay_url": { "type": ["string", "null"], "format": "uri", "description": "Where to activate, when `activate` was false." }
        }
      },
      "CodeSummary": {
        "type": "object",
        "required": ["code", "short_url", "destination", "kind", "active", "created_at"],
        "properties": {
          "code": { "type": "string" },
          "short_url": { "type": "string" },
          "destination": { "type": "string", "description": "Where the code goes now." },
          "kind": { "type": "string", "examples": ["website"] },
          "summary": { "type": "string", "description": "A one-line description of what the code does." },
          "active": { "type": "boolean" },
          "tier": { "type": "string", "enum": ["paid", "free"] },
          "activation": { "type": ["string", "null"], "enum": ["single", "plan", "free", null] },
          "created_at": { "type": "string", "format": "date-time" },
          "expires_at": { "type": ["string", "null"], "format": "date-time" },
          "stats_url": { "type": "string", "format": "uri" }
        }
      },
      "CodeList": {
        "type": "object",
        "required": ["codes"],
        "properties": { "codes": { "type": "array", "items": { "$ref": "#/components/schemas/CodeSummary" } } }
      },
      "CodeWithStats": {
        "allOf": [
          { "$ref": "#/components/schemas/CodeSummary" },
          { "type": "object", "required": ["stats"], "properties": { "stats": { "$ref": "#/components/schemas/Stats" } } }
        ]
      },
      "SetDestinationRequest": {
        "type": "object",
        "required": ["destination"],
        "properties": { "destination": { "type": "string", "format": "uri", "description": "A full URL, including https://." } }
      },
      "Bucket": {
        "type": "object",
        "required": ["key", "label", "count"],
        "properties": {
          "key": { "type": "string" },
          "label": { "type": "string" },
          "count": { "type": "integer" },
          "pct": { "type": "number", "description": "Share of the total, in percent." }
        }
      },
      "DayPoint": {
        "type": "object",
        "required": ["day", "scans", "clicks", "unique"],
        "properties": {
          "day": { "type": "string", "format": "date" },
          "scans": { "type": "integer" },
          "clicks": { "type": "integer" },
          "unique": { "type": "integer" }
        }
      },
      "MapPoint": {
        "type": "object",
        "required": ["lat", "lon", "count"],
        "properties": {
          "lat": { "type": "number" },
          "lon": { "type": "number" },
          "count": { "type": "integer" },
          "city": { "type": "string" },
          "country": { "type": "string" }
        }
      },
      "Stats": {
        "type": "object",
        "description": "Scan analytics for one code. Pseudonymous: there is no per-person data.",
        "properties": {
          "total": { "type": "integer", "description": "Scans plus clicks (previews excluded)." },
          "scans": { "type": "integer" },
          "clicks": { "type": "integer" },
          "previews": { "type": "integer" },
          "unique": { "type": "integer", "description": "Sum of per-day unique visitors." },
          "repeatRate": { "type": "number", "minimum": 0, "maximum": 1, "description": "Share of scans that were a same-day repeat." },
          "firstTs": { "type": ["integer", "null"], "description": "Milliseconds since the epoch." },
          "lastTs": { "type": ["integer", "null"], "description": "Milliseconds since the epoch." },
          "last7": { "type": "integer" },
          "prev7": { "type": "integer" },
          "days": { "type": "array", "items": { "$ref": "#/components/schemas/DayPoint" }, "description": "The last 90 days, zero-filled." },
          "heat": { "type": "array", "items": { "type": "array", "items": { "type": "integer" } }, "description": "Scans by weekday (0 to 6) and hour (0 to 23), in the scanner's local time." },
          "bestHour": { "type": ["object", "null"], "properties": { "dow": { "type": "integer" }, "hour": { "type": "integer" }, "count": { "type": "integer" } } },
          "countries": { "type": "array", "items": { "$ref": "#/components/schemas/Bucket" } },
          "regions": { "type": "array", "items": { "$ref": "#/components/schemas/Bucket" } },
          "cities": { "type": "array", "items": { "$ref": "#/components/schemas/Bucket" } },
          "postals": { "type": "array", "items": { "$ref": "#/components/schemas/Bucket" } },
          "os": { "type": "array", "items": { "$ref": "#/components/schemas/Bucket" } },
          "browsers": { "type": "array", "items": { "$ref": "#/components/schemas/Bucket" } },
          "inApp": { "type": "array", "items": { "$ref": "#/components/schemas/Bucket" } },
          "langs": { "type": "array", "items": { "$ref": "#/components/schemas/Bucket" } },
          "kinds": { "type": "array", "items": { "$ref": "#/components/schemas/Bucket" } },
          "points": { "type": "array", "items": { "$ref": "#/components/schemas/MapPoint" } },
          "countryPoints": { "type": "array", "items": { "$ref": "#/components/schemas/MapPoint" } },
          "localShare": { "type": ["number", "null"], "description": "Share of scans near where the code is placed, when that is known." },
          "countryOnly": { "type": "integer", "description": "Scans located to a country only, not on the map." },
          "recent": { "type": "array", "items": { "type": "object", "additionalProperties": true }, "description": "The latest scan records." }
        }
      },
      "Lead": {
        "type": "object",
        "required": ["id", "received", "form", "source", "answers"],
        "properties": {
          "id": { "type": "string" },
          "received": { "type": "string", "format": "date-time" },
          "form": { "type": "string", "description": "The form's title when the lead was sent." },
          "source": { "type": "string", "enum": ["qr", "link"], "description": "Whether the visitor came from a QR scan or a link." },
          "code": { "type": ["string", "null"], "description": "The printed code they scanned, when `source` is `qr`." },
          "city": { "type": ["string", "null"] },
          "country": { "type": ["string", "null"] },
          "answers": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Each question and the visitor's answer." }
        }
      },
      "LeadReport": {
        "type": "object",
        "required": ["page", "total_kept", "last_30d", "by_code", "leads", "retention_days"],
        "properties": {
          "page": {
            "type": "object",
            "required": ["id", "title", "live"],
            "properties": { "id": { "type": "string" }, "title": { "type": "string" }, "live": { "type": "boolean" } }
          },
          "total_kept": { "type": "integer", "description": "How many leads are stored for the page." },
          "last_30d": {
            "type": "object",
            "properties": {
              "leads": { "type": "integer" },
              "views": { "type": "integer" },
              "conversion_pct": { "type": ["number", "null"], "description": "Leads divided by views, in percent." },
              "from_qr": { "type": "integer" }
            }
          },
          "by_code": {
            "type": "array",
            "description": "Printed codes that sent visitors to the page (top 20).",
            "items": {
              "type": "object",
              "properties": {
                "code": { "type": "string" },
                "scans": { "type": "integer" },
                "leads": { "type": "integer" },
                "conversion_pct": { "type": ["number", "null"] }
              }
            }
          },
          "leads": { "type": "array", "items": { "$ref": "#/components/schemas/Lead" } },
          "retention_days": { "type": "integer", "description": "How long a lead is kept." }
        }
      }
    }
  }
}
