{
  "openapi": "3.1.0",
  "x-service-info": {
    "categories": [
      "image-generation",
      "qr-codes"
    ]
  },
  "info": {
    "title": "qart.uk API",
    "version": "1.0.0",
    "summary": "Claim, retarget and generate scannable QR artwork.",
    "description": "The public surface of qart.uk. Three products share it: a free pool of pre-generated QR art that signed-in users can claim and point at any URL, a paid Studio that generates custom codes against your payload, priced in prepaid credits, and a pay-per-QR agent API under /v1 that sells one finished, scan-verified artwork outright over x402 with **no account and no bearer token at all**.\n\nPrograms acting on a person's account authenticate with OAuth 2.1 (public clients, PKCE S256, open dynamic registration); browsers use a session cookie from Discord or Google sign-in. The /v1 endpoints need neither — a wallet signature is the only identity involved. See https://qart.uk/.well-known/auth.md.",
    "contact": {
      "url": "https://qart.uk/"
    }
  },
  "servers": [
    {
      "url": "https://qart.uk/api",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "public",
      "description": "No authentication required."
    },
    {
      "name": "pool",
      "description": "The free claim pool. Metered by a daily allowance, never by money."
    },
    {
      "name": "studio",
      "description": "The paid generator. Spends prepaid credits."
    },
    {
      "name": "agent",
      "description": "The pay-per-QR agent API under /v1. No account, no bearer token — x402 from your own wallet."
    }
  ],
  "components": {
    "securitySchemes": {
      "cookieAuth": {
        "type": "apiKey",
        "in": "cookie",
        "name": "session_token",
        "description": "The browser session, set by the Discord/Google OAuth callback. HttpOnly, so it is not readable or settable by a script, and it cannot be obtained programmatically. Listed for completeness — an API client should use oauth2 below."
      },
      "oauth2": {
        "type": "oauth2",
        "description": "How a program authenticates. Public clients only: PKCE with S256 is mandatory and there is no client secret. Registration is open (RFC 7591) at https://qart.uk/api/oauth/register. Full walkthrough at https://qart.uk/.well-known/auth.md.",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://qart.uk/api/oauth/authorize",
            "tokenUrl": "https://qart.uk/api/oauth/token",
            "refreshUrl": "https://qart.uk/api/oauth/token",
            "scopes": {
              "qart:profile": "Which account you are acting for.",
              "qart:read": "Their codes, credit balance, and run status.",
              "qart:claim": "Claim, retarget and retire pool codes. Uses the daily allowance; costs no money.",
              "qart:studio": "SPENDS REAL MONEY. Starts Studio runs against their prepaid credits, with no per-application limit.",
              "qart:purchase": "SPENDS THE APPLICATION'S OWN MONEY. Buys credit packs from the agent's own wallet over x402 or MPP. The user's card is never charged; the credits land on their balance."
            }
          }
        }
      }
    }
  },
  "paths": {
    "/qart/resolve/{alias}": {
      "get": {
        "tags": [
          "public"
        ],
        "operationId": "resolveAlias",
        "summary": "Resolve a qart.uk code to its destination",
        "description": "Given the alias from a scanned code (the `_ab12cde` part of `qart.uk/_ab12cde`), return where it currently points. This is what a scan does. Public and unauthenticated on purpose: a scan is a stranger with a phone. Cached 10s; the visit counter it increments is honest but approximate.",
        "parameters": [
          {
            "name": "alias",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^_[a-z0-9]{7}$"
            },
            "example": "_ab12cde"
          }
        ],
        "responses": {
          "200": {
            "description": "The code resolved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "alias": {
                      "type": "string",
                      "description": "The alias, echoed back."
                    },
                    "targetUrl": {
                      "type": "string",
                      "description": "Where it points. Absent if disabled."
                    },
                    "disabledReason": {
                      "type": "string",
                      "description": "Set if the code has been retired or disabled; takes precedence over targetUrl."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not a valid alias shape, or no such code.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Resolution backend unavailable. Never treated as \"no such code\".",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/qart/pool/gallery": {
      "get": {
        "tags": [
          "public",
          "pool"
        ],
        "operationId": "browsePool",
        "summary": "The free claim pool storefront",
        "description": "The codes currently offered — one per style. Signed out you get a shared, stable guest selection; signed in you get your own, which `shuffle` re-rolls. Everything listed is claimable; claimed codes are simply absent.",
        "responses": {
          "200": {
            "description": "The storefront.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "code": {
                            "type": "string",
                            "description": "The claimable code, e.g. _ab12cde."
                          },
                          "style": {
                            "type": "string"
                          },
                          "imageUrl": {
                            "type": "string",
                            "description": "Site-relative preview image path."
                          },
                          "available": {
                            "type": "integer",
                            "description": "How many unclaimed codes share this style — NOT a boolean. Every code on the storefront is claimable; this says how much choice is left in that look."
                          }
                        }
                      }
                    },
                    "poolRemaining": {
                      "type": "integer",
                      "description": "Unclaimed codes left in the whole pool."
                    },
                    "shufflesLeft": {
                      "type": "integer"
                    },
                    "claimsLeft": {
                      "type": "integer"
                    },
                    "shufflesPerDay": {
                      "type": "integer",
                      "examples": [
                        5
                      ]
                    },
                    "claimsPerDay": {
                      "type": "integer",
                      "examples": [
                        3
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/qart/config": {
      "get": {
        "tags": [
          "public"
        ],
        "operationId": "getConfig",
        "summary": "Feature flags and Studio pricing inputs",
        "description": "What is currently switched on, and the numbers the client needs to quote a price before a run. Read this rather than assuming: the Studio has a kill switch and the edit feature has its own.",
        "responses": {
          "200": {
            "description": "Current configuration.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "studioEnabled": {
                      "type": "boolean"
                    },
                    "generationEnabled": {
                      "type": "boolean"
                    },
                    "editsEnabled": {
                      "type": "boolean"
                    },
                    "studioMinImages": {
                      "type": "integer",
                      "description": "Images charged if a launched run is stopped early.",
                      "examples": [
                        10
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/qart/pool/{code}/claim": {
      "post": {
        "tags": [
          "pool"
        ],
        "operationId": "claimCode",
        "summary": "Claim a pool code and point it somewhere",
        "description": "Permanent: the code becomes yours and can be retargeted forever. Costs one of 3 daily claims. The destination is screened — https only, no credentials in the URL, no IP literals, no non-public hosts, and no URL shorteners.",
        "security": [
          {
            "oauth2": [
              "qart:claim"
            ]
          },
          {
            "cookieAuth": []
          }
        ],
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^_[a-z0-9]{7}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "targetUrl"
                ],
                "properties": {
                  "targetUrl": {
                    "type": "string",
                    "description": "https:// destination, max 2000 chars."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Claimed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "code": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string",
                      "description": "The scannable qart.uk URL."
                    },
                    "targetUrl": {
                      "type": "string"
                    },
                    "imageUrl": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Destination rejected by screening.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "That code is not currently offered to you.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Already claimed by someone else.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Daily claim allowance exhausted. Resets at 00:00 UTC.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/qart/pool/{code}/destination": {
      "put": {
        "tags": [
          "pool"
        ],
        "operationId": "retargetCode",
        "summary": "Change where a code you own points",
        "description": "Free and unlimited — this is the feature the product is built around. The printed pixels never change.",
        "security": [
          {
            "oauth2": [
              "qart:claim"
            ]
          },
          {
            "cookieAuth": []
          }
        ],
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^_[a-z0-9]{7}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "targetUrl"
                ],
                "properties": {
                  "targetUrl": {
                    "type": "string",
                    "description": "https:// destination."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Retargeted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "code": {
                      "type": "string"
                    },
                    "targetUrl": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "You do not own that code.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/qart/my-codes": {
      "get": {
        "tags": [
          "pool"
        ],
        "operationId": "listMyCodes",
        "summary": "Every code you have claimed",
        "security": [
          {
            "oauth2": [
              "qart:read"
            ]
          },
          {
            "cookieAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Your codes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "codes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "code": {
                            "type": "string"
                          },
                          "targetUrl": {
                            "type": "string"
                          },
                          "scanUrl": {
                            "type": "string"
                          },
                          "imageUrl": {
                            "type": "string"
                          },
                          "hits": {
                            "type": "integer",
                            "description": "Approximate scan count."
                          },
                          "disabled": {
                            "type": "boolean"
                          }
                        }
                      }
                    },
                    "keptImages": {
                      "type": "array",
                      "description": "Studio images you kept that have no qart.uk alias behind them (every payload kind except qart_code bakes its content into the modules). Read-only: nothing to retarget, nothing to count.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "type": {
                            "type": "string",
                            "enum": [
                              "image",
                              "edit"
                            ]
                          },
                          "runId": {
                            "type": "string"
                          },
                          "idx": {
                            "type": "integer"
                          },
                          "editId": {
                            "type": "string"
                          },
                          "action": {
                            "type": "string"
                          },
                          "payloadKind": {
                            "type": "string"
                          },
                          "payloadSummary": {
                            "type": "string",
                            "description": "Display-safe description of what the code encodes (e.g. \"WiFi · \"Office\" (WPA)\", \"URL · example.com\"). The raw payload is never returned: for wifi, vcard, mecard, mailto, tel, sms, geo and whatsapp it is a credential or personal data, and a URL query string can carry a token."
                          },
                          "scannable": {
                            "type": "boolean"
                          },
                          "scanChecked": {
                            "type": "boolean"
                          },
                          "keptAt": {
                            "type": "string"
                          },
                          "imageUrl": {
                            "type": "string"
                          },
                          "runUrl": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/qart/pool/shuffle": {
      "post": {
        "tags": [
          "pool"
        ],
        "operationId": "shufflePool",
        "summary": "Re-roll your storefront selection",
        "description": "Costs one of 5 daily shuffles. Returns the same shape as the storefront.",
        "security": [
          {
            "oauth2": [
              "qart:claim"
            ]
          },
          {
            "cookieAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "A new selection.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Daily shuffle allowance exhausted. Resets at 00:00 UTC.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/qart/studio/bases": {
      "get": {
        "tags": [
          "studio"
        ],
        "operationId": "listStudioBases",
        "summary": "Base-model catalog with sample codes",
        "description": "Each Stable Diffusion checkpoint this account can use in Studio: look, approximate release month, license, optional trigger token, and a few sample QR images generated with that model (a look-book from the claim pool, not a storefront). NSFW checkpoints are omitted unless the account has the NSFW unlock.",
        "security": [
          {
            "oauth2": [
              "qart:read"
            ]
          },
          {
            "cookieAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The catalog.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "nsfw": {
                      "type": "boolean"
                    },
                    "bases": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Hugging Face checkpoint id."
                          },
                          "label": {
                            "type": "string"
                          },
                          "look": {
                            "type": "string"
                          },
                          "released": {
                            "type": "string",
                            "description": "Approximate first-publish month, YYYY-MM."
                          },
                          "kind": {
                            "type": "string"
                          },
                          "license": {
                            "type": "string"
                          },
                          "trigger": {
                            "type": "string",
                            "description": "Prompt prefix this checkpoint expects, if any."
                          },
                          "hfUrl": {
                            "type": "string"
                          },
                          "nsfw": {
                            "type": "boolean"
                          },
                          "styleCount": {
                            "type": "integer"
                          },
                          "samples": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "style": {
                                  "type": "string"
                                },
                                "imageUrl": {
                                  "type": "string",
                                  "description": "Site-relative preview image path."
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/qart/studio/preview": {
      "post": {
        "tags": [
          "studio"
        ],
        "operationId": "previewEncoding",
        "summary": "Check how a payload will encode — free",
        "description": "Assembles the payload and reports the QR parameters it needs: version, module count, capacity, and any warnings. **Costs nothing and mints nothing** — it encodes against a placeholder alias. Use it to answer \"will this URL fit at this error correction level\" before committing to a run.",
        "security": [
          {
            "oauth2": [
              "qart:read"
            ]
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "payloadKind": {
                    "type": "string",
                    "enum": [
                      "url",
                      "qart_code",
                      "text",
                      "wifi",
                      "vcard",
                      "mecard",
                      "mailto",
                      "tel",
                      "sms",
                      "geo",
                      "whatsapp",
                      "telegram",
                      "event",
                      "instagram",
                      "twitter",
                      "youtube",
                      "facebook",
                      "tiktok",
                      "linkedin",
                      "paypal"
                    ]
                  },
                  "fields": {
                    "type": "object",
                    "properties": {}
                  },
                  "ecc": {
                    "type": "string",
                    "enum": [
                      "L",
                      "M",
                      "Q",
                      "H"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Encoding parameters.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "payload": {
                      "type": "string",
                      "description": "The exact string the pixels will carry."
                    },
                    "byteLength": {
                      "type": "integer"
                    },
                    "version": {
                      "type": "integer"
                    },
                    "minVersion": {
                      "type": "integer"
                    },
                    "modules": {
                      "type": "integer"
                    },
                    "capacity": {
                      "type": "integer"
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/qart/credits": {
      "get": {
        "tags": [
          "studio"
        ],
        "operationId": "getCredits",
        "summary": "Credit balance, packs, and ledger",
        "security": [
          {
            "oauth2": [
              "qart:read"
            ]
          },
          {
            "cookieAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Your balance and recent transactions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "balance": {
                      "type": "number"
                    },
                    "packs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "key": {
                            "type": "string"
                          },
                          "label": {
                            "type": "string"
                          },
                          "usdCents": {
                            "type": "integer"
                          },
                          "credits": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "transactions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "amount": {
                            "type": "number"
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "purchase",
                              "reserve",
                              "refund",
                              "coupon",
                              "edit"
                            ]
                          },
                          "createdAt": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/qart/studio/run": {
      "post": {
        "tags": [
          "studio"
        ],
        "operationId": "startRun",
        "summary": "Start a generation run — SPENDS CREDITS",
        "description": "Reserves `budgetCredits` up front and generates until the budget is spent or you stop. One active run per account.\n\nTwo things that are not obvious and cost money if missed:\n- **Once the GPU has launched, stopping charges a 10-image floor.** A run that never launched refunds in full.\n- **You must poll `/qart/studio/run/{id}` about every 2 seconds until the run is terminal.** That poll is not a passive read: it imports finished images and acts as the GPU presence heartbeat. Roughly 120 seconds of silence soft-stops the run.",
        "security": [
          {
            "oauth2": [
              "qart:studio"
            ]
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "payloadKind": {
                    "type": "string",
                    "enum": [
                      "url",
                      "qart_code",
                      "text",
                      "wifi",
                      "vcard",
                      "mecard",
                      "mailto",
                      "tel",
                      "sms",
                      "geo",
                      "whatsapp",
                      "telegram",
                      "event",
                      "instagram",
                      "twitter",
                      "youtube",
                      "facebook",
                      "tiktok",
                      "linkedin",
                      "paypal"
                    ]
                  },
                  "fields": {
                    "type": "object",
                    "properties": {}
                  },
                  "encode": {
                    "type": "object",
                    "properties": {
                      "ecc": {
                        "type": "string"
                      },
                      "version": {
                        "type": "integer"
                      },
                      "quietZone": {
                        "type": "integer"
                      }
                    }
                  },
                  "look": {
                    "type": "object",
                    "properties": {
                      "styleKey": {
                        "type": "string"
                      },
                      "prompt": {
                        "type": "string"
                      },
                      "steps": {
                        "type": "integer"
                      },
                      "resolution": {
                        "type": "integer"
                      }
                    }
                  },
                  "budgetCredits": {
                    "type": "number",
                    "description": "Clamped to [perImage x 10, 500]. Below the floor is a hard 400, never silently rounded."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Run created and launching.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "run": {
                      "type": "object",
                      "properties": {}
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Budget below the 10-image floor, or an invalid recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Insufficient credits.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "You already have an active run.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/qart/studio/run/{id}": {
      "get": {
        "tags": [
          "studio"
        ],
        "operationId": "getRun",
        "summary": "Run state — and the import pump. Poll this.",
        "description": "Returns the run, its images so far, and any edits. **This call has side effects by design**: it imports up to 5 finished images per call and refreshes the GPU heartbeat. Poll it every ~2s until `run.status` is `done`, `failed`, or `cancelled`. Stopping early does not just delay images — it can soft-stop the run.",
        "security": [
          {
            "oauth2": [
              "qart:read"
            ]
          },
          {
            "cookieAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Current state.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "run": {
                      "type": "object",
                      "properties": {
                        "status": {
                          "type": "string",
                          "enum": [
                            "queued",
                            "running",
                            "stopping",
                            "done",
                            "failed",
                            "cancelled"
                          ]
                        }
                      }
                    },
                    "images": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "idx": {
                            "type": "integer"
                          },
                          "scanned": {
                            "type": "boolean",
                            "description": "Verified to decode back to your exact payload."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/qart/x402/info": {
      "get": {
        "tags": [
          "public",
          "studio"
        ],
        "operationId": "x402Info",
        "summary": "x402 capabilities, network and pack prices",
        "description": "What the x402 rail currently accepts: chain, asset, recipient, and the atomic amount for each credit pack. Reports `enabled: false` with a reason when the rail is off, so an agent can check before quoting a price to a user. Public and unauthenticated.",
        "responses": {
          "200": {
            "description": "Current x402 configuration.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "enabled": {
                      "type": "boolean"
                    },
                    "x402Version": {
                      "type": "integer"
                    },
                    "network": {
                      "type": "string",
                      "description": "CAIP-2 chain id, e.g. eip155:8453 (Base mainnet)."
                    },
                    "asset": {
                      "type": "string",
                      "description": "ERC-20 contract address of the settlement asset."
                    },
                    "payTo": {
                      "type": "string",
                      "description": "Recipient address."
                    },
                    "scheme": {
                      "type": "string",
                      "description": "Payment scheme; 'exact'."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/qart/x402/credits": {
      "post": {
        "tags": [
          "studio"
        ],
        "operationId": "buyCreditsX402",
        "summary": "Buy Studio credits over x402",
        "description": "Pay from your own wallet and receive Studio credits. Called without a `PAYMENT-SIGNATURE` header this answers **402** with the payment requirements in the `PAYMENT-REQUIRED` response header (x402 v2 — header, not body); sign and retry to complete.\n\nAn OAuth bearer token with `qart:purchase` is OPTIONAL. With one, the credits land on that account. Without one, the payment still settles and the response carries a single-use `redemptionCode` that any account can redeem at `/qart/redeem` — which is what lets an agent buy for a person who has no qart.uk account yet.",
        "parameters": [
          {
            "name": "pack",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "starter",
                "studio",
                "pro"
              ]
            },
            "description": "Credit pack; defaults to 'starter'."
          }
        ],
        "responses": {
          "200": {
            "description": "Paid. Credits added, or a redemption code issued.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "credits": {
                      "type": "number"
                    },
                    "redemptionCode": {
                      "type": "string",
                      "description": "Present only when no account was identified."
                    },
                    "transaction": {
                      "type": "string",
                      "description": "Settlement transaction hash."
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Payment required. Requirements are in the PAYMENT-REQUIRED header.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {}
                }
              }
            }
          },
          "503": {
            "description": "The x402 rail is not currently enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/qart/redeem": {
      "post": {
        "tags": [
          "studio"
        ],
        "operationId": "redeemCode",
        "summary": "Redeem a credit redemption code",
        "description": "Apply a single-use redemption code — issued by an account-less x402 purchase — to the authenticated account. Needs a session or an OAuth token with `qart:purchase`.",
        "security": [
          {
            "oauth2": [
              "qart:purchase"
            ]
          },
          {
            "cookieAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Redeemed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "creditsAdded": {
                      "type": "number"
                    },
                    "balance": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Already redeemed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/qart/mpp/credits": {
      "post": {
        "tags": [
          "studio"
        ],
        "operationId": "buyCreditsMpp",
        "summary": "Buy credits by machine payment (MPP)",
        "description": "Pay for a credit pack over Stripe's Machine Payable Protocol and have the credits added to your account. Answers 402 with a `WWW-Authenticate: Payment` challenge until a payment credential is presented.\n\nRequires an OAuth bearer with the `qart:purchase` scope BEFORE the challenge is issued — credits land on a specific account, so a token that could not be credited is refused rather than quoted a price it would waste money on.\n\nSee also /qart/x402/credits, which does the same job over the x402 protocol on Base.",
        "security": [
          {
            "oauth2": [
              "qart:purchase"
            ]
          }
        ],
        "parameters": [
          {
            "name": "pack",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "starter",
                "studio",
                "pro"
              ]
            },
            "description": "Which credit pack to buy. Defaults to starter."
          }
        ],
        "x-payment-info": {
          "intent": "charge",
          "method": "tempo",
          "amount": "500",
          "currency": "usd",
          "description": "500 qart.uk Studio credits (Starter pack)",
          "offers": [
            {
              "amount": "500",
              "intent": "charge",
              "method": "tempo",
              "currency": "usd"
            },
            {
              "amount": "1000",
              "intent": "charge",
              "method": "tempo",
              "currency": "usd"
            },
            {
              "amount": "2500",
              "intent": "charge",
              "method": "tempo",
              "currency": "usd"
            }
          ]
        },
        "responses": {
          "200": {
            "description": "Paid and credited.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "creditsAdded": {
                      "type": "integer"
                    },
                    "balance": {
                      "type": "number"
                    },
                    "reference": {
                      "type": "string",
                      "description": "Settlement reference, e.g. the transaction hash."
                    },
                    "replay": {
                      "type": "boolean",
                      "description": "True if this payment was already applied."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or insufficiently scoped bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Payment required. See the WWW-Authenticate header.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {}
                }
              }
            }
          },
          "503": {
            "description": "Machine payment is not currently enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1": {
      "get": {
        "tags": [
          "public",
          "agent"
        ],
        "operationId": "agentApiIndex",
        "summary": "The agent API index and live price — answers 402",
        "description": "The entry point for the pay-per-QR API. Always answers **402**: the x402 payment requirements are in the `PAYMENT-REQUIRED` response header (v2), and the JSON body documents what is on sale, the live price for each size, and the parameters `/v1/qr` accepts. Free to call — the 402 is the documentation, not a charge; nothing is settled unless you sign and retry against `/v1/qr`.\n\nRead this rather than caching a price: the price is an operator setting and can change without a redeploy of any document here.",
        "responses": {
          "402": {
            "description": "The live quote and the endpoint documentation. Requirements are in the PAYMENT-REQUIRED header.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "service": {
                      "type": "string"
                    },
                    "description": {
                      "type": "string"
                    },
                    "guarantee": {
                      "type": "string",
                      "description": "Restates that settlement follows the scan verdict."
                    },
                    "resources": {
                      "type": "object",
                      "properties": {}
                    },
                    "docs": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The paid agent API is disabled or unconfigured.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "service": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    },
                    "detail": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/qr": {
      "get": {
        "tags": [
          "public",
          "agent"
        ],
        "operationId": "buyQr",
        "summary": "Buy one scan-verified QR artwork (x402, no account)",
        "description": "Returns 402 with x402 payment requirements; retry with a `PAYMENT-SIGNATURE` header and the image comes back as image/png.\n\nNo account and no bearer token: the wallet is the only identity. You are charged ONLY if a real QR detector decodes the image we generate — settlement happens after the scan verdict, so a request that produces nothing scannable is never settled and costs you nothing.\n\nThe destination URL is encoded directly; no qart.uk redirect is minted, so nothing about the code depends on us staying up. Retrying with the same payment nonce returns the same image rather than generating (and charging) a second time.",
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "https destination to encode. Length-capped; see GET /api/v1."
          },
          {
            "name": "style",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "A key from GET /api/v1/styles. Free-text prompts are not accepted here."
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "enum": [
                768,
                1024
              ]
            },
            "description": "Pixel size. 1024 costs more; see GET /api/v1."
          }
        ],
        "responses": {
          "200": {
            "description": "Paid and delivered. Scan-verified PNG.",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "Bad or unsafe destination, unknown style, or a URL over the cap. Not charged.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Payment required. Requirements are in the PAYMENT-REQUIRED header.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {}
                }
              }
            }
          },
          "409": {
            "description": "That payment is already generating.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limited for this wallet.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "502": {
            "description": "Nothing scannable was produced. NOT charged.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The paid agent API is disabled or unconfigured.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/styles": {
      "get": {
        "tags": [
          "public",
          "agent"
        ],
        "operationId": "agentStyles",
        "summary": "Style keys accepted by /api/v1/qr",
        "description": "Free and unauthenticated: an agent should not have to trigger a payment to learn what it can ask for.",
        "responses": {
          "200": {
            "description": "The curated style list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "styles": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "key": {
                            "type": "string"
                          },
                          "description": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/result/{id}": {
      "get": {
        "tags": [
          "public",
          "agent"
        ],
        "operationId": "agentResult",
        "summary": "Re-fetch an image you already paid for",
        "description": "Free, for 7 days. The id comes back as X-Qart-Request-Id on the paid response.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The image.",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "No such delivered result.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "410": {
            "description": "Delivered, but the bytes have since been reclaimed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/qart/studio/run/{id}/stop": {
      "post": {
        "tags": [
          "studio"
        ],
        "operationId": "stopRun",
        "summary": "Stop a run and refund the unused reserve",
        "description": "Drains images already produced (they are real GPU spend and are charged), then refunds reserve minus consumed. Images that finish after the stop are delivered free.",
        "security": [
          {
            "oauth2": [
              "qart:read"
            ]
          },
          {
            "cookieAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Stopped and settled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "run": {
                      "type": "object",
                      "properties": {}
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}