{
  "openapi": "3.1.0",
  "info": {
    "title": "CozyToolkit Links API",
    "version": "1.1.0",
    "description": "Focused beta API for hosted short links, tracked QR SVG exports and private analytics. Verified-account bearer keys, shared monthly link allowances, scoped permissions and atomic per-key monthly spending limits. No X, AI writing, downloads or file transformation endpoints are part of this public API. Rate limits: 20 requests/minute/IP and 30/minute/account per Cloudflare location. Current shared tracking safety ceiling: 5,000 recorded visits/day. No service availability guarantee; public checkout availability is separate from API access. No-key trials: 3 links per network per UTC day, expiring in 7 days; shared cap 100/day. Per-link receipts authorize trial read/QR/stats only."
  },
  "servers": [
    {
      "url": "https://cozytoolkit.com/api/v1"
    }
  ],
  "security": [
    {
      "ApiKey": []
    }
  ],
  "paths": {
    "/account/usage": {
      "get": {
        "operationId": "getUsage",
        "summary": "Read shared allowances, wallet and key budget",
        "description": "Required scope: links:read. ",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "LinkSummary plus keyBudget, keySpent and pricing. Purchased balance excludes active reservations. Monthly allowances/budgets reset in UTC."
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/RequestError"
          },
          "401": {
            "$ref": "#/components/responses/RequestError"
          },
          "402": {
            "$ref": "#/components/responses/RequestError"
          },
          "403": {
            "$ref": "#/components/responses/RequestError"
          },
          "404": {
            "$ref": "#/components/responses/RequestError"
          },
          "409": {
            "$ref": "#/components/responses/RequestError"
          },
          "429": {
            "$ref": "#/components/responses/RequestError"
          },
          "503": {
            "$ref": "#/components/responses/RequestError"
          }
        }
      }
    },
    "/quotes": {
      "post": {
        "operationId": "quoteAction",
        "summary": "Estimate the cost without reserving credits",
        "description": "Required scope: links:read. A quote can change as shared free allowance is used. Final spend must stay within maxCredits, the key monthly budget and account balance.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "action": {
                      "type": "string"
                    },
                    "credits": {
                      "type": "integer"
                    },
                    "maxCredits": {
                      "type": "integer"
                    },
                    "budgetRemaining": {
                      "type": "integer"
                    },
                    "purchasedAvailable": {
                      "type": "integer"
                    },
                    "canSpend": {
                      "type": "boolean"
                    },
                    "freeLinks": {
                      "type": "integer"
                    },
                    "quoteIsEstimate": {
                      "const": true
                    },
                    "trackingDailyServiceLimit": {
                      "type": "integer"
                    }
                  },
                  "required": [],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/RequestError"
          },
          "401": {
            "$ref": "#/components/responses/RequestError"
          },
          "402": {
            "$ref": "#/components/responses/RequestError"
          },
          "403": {
            "$ref": "#/components/responses/RequestError"
          },
          "404": {
            "$ref": "#/components/responses/RequestError"
          },
          "409": {
            "$ref": "#/components/responses/RequestError"
          },
          "429": {
            "$ref": "#/components/responses/RequestError"
          },
          "503": {
            "$ref": "#/components/responses/RequestError"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "action": {
                    "enum": [
                      "link.create",
                      "tracking.purchase"
                    ]
                  }
                },
                "required": [
                  "action"
                ],
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/links": {
      "get": {
        "operationId": "listLinks",
        "summary": "List owned links, up to 50 per page",
        "description": "Required scope: links:read. ",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "links": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Link"
                      }
                    },
                    "hasMore": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "links",
                    "hasMore"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/RequestError"
          },
          "401": {
            "$ref": "#/components/responses/RequestError"
          },
          "402": {
            "$ref": "#/components/responses/RequestError"
          },
          "403": {
            "$ref": "#/components/responses/RequestError"
          },
          "404": {
            "$ref": "#/components/responses/RequestError"
          },
          "409": {
            "$ref": "#/components/responses/RequestError"
          },
          "429": {
            "$ref": "#/components/responses/RequestError"
          },
          "503": {
            "$ref": "#/components/responses/RequestError"
          }
        },
        "parameters": [
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 100000
            }
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          }
        ]
      },
      "post": {
        "operationId": "createLink",
        "summary": "Create one short link and tracked QR URL",
        "description": "Required scope: links:write. 25 new links/month shared with website. Extra links cost 10 purchased credits and also require credits:spend. maxCredits defaults to 0; exceeding free allowance then returns 402. One saved link includes both /s/ and /q/ URLs. Without Authorization, creates a seven-day trial link. Only target, title and maxCredits:0 are accepted. No aliases, tags, account access or spending. UUID Idempotency-Key is required; replay from the same network and UTC day. Never resend with a new ID after an uncertain result. Invalid supplied keys do not fall back to guest access.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Link"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "trialToken": {
                          "type": "string",
                          "description": "Only present on trial creation/replay. Save privately; not returned by reads. Send as X-Cozy-Trial-Token for that link’s QR and stats."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/RequestError"
          },
          "401": {
            "$ref": "#/components/responses/RequestError"
          },
          "402": {
            "$ref": "#/components/responses/RequestError"
          },
          "403": {
            "$ref": "#/components/responses/RequestError"
          },
          "404": {
            "$ref": "#/components/responses/RequestError"
          },
          "409": {
            "$ref": "#/components/responses/RequestError"
          },
          "429": {
            "$ref": "#/components/responses/RequestError"
          },
          "503": {
            "$ref": "#/components/responses/RequestError"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "target": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 2048
                  },
                  "alias": {
                    "type": "string",
                    "maxLength": 40
                  },
                  "title": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "tags": {
                    "type": "string",
                    "maxLength": 160
                  },
                  "maxCredits": {
                    "type": "integer",
                    "enum": [
                      0,
                      10
                    ],
                    "default": 0
                  }
                },
                "required": [
                  "target"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Reuse this UUID and identical payload with the same key after an uncertain response. Never retry an uncertain purchase with a new UUID."
          }
        ],
        "security": [
          {
            "ApiKey": []
          },
          {}
        ]
      }
    },
    "/links/{id}": {
      "get": {
        "operationId": "getLink",
        "summary": "Read an owned link",
        "description": "Required scope: links:read.  Trial links accept their per-link X-Cozy-Trial-Token instead of an account key. Expired trials return 410.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Link"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/RequestError"
          },
          "401": {
            "$ref": "#/components/responses/RequestError"
          },
          "402": {
            "$ref": "#/components/responses/RequestError"
          },
          "403": {
            "$ref": "#/components/responses/RequestError"
          },
          "404": {
            "$ref": "#/components/responses/RequestError"
          },
          "409": {
            "$ref": "#/components/responses/RequestError"
          },
          "429": {
            "$ref": "#/components/responses/RequestError"
          },
          "503": {
            "$ref": "#/components/responses/RequestError"
          },
          "410": {
            "$ref": "#/components/responses/RequestError"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "security": [
          {
            "ApiKey": []
          },
          {
            "TrialReceipt": []
          }
        ]
      },
      "patch": {
        "operationId": "updateLink",
        "summary": "Edit destination or pause/resume without changing URLs",
        "description": "Required scope: links:write. No credit charge. Omitted fields are preserved. Use the version from GET; a stale version returns 409. No API deletion endpoint.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Link"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/RequestError"
          },
          "401": {
            "$ref": "#/components/responses/RequestError"
          },
          "402": {
            "$ref": "#/components/responses/RequestError"
          },
          "403": {
            "$ref": "#/components/responses/RequestError"
          },
          "404": {
            "$ref": "#/components/responses/RequestError"
          },
          "409": {
            "$ref": "#/components/responses/RequestError"
          },
          "429": {
            "$ref": "#/components/responses/RequestError"
          },
          "503": {
            "$ref": "#/components/responses/RequestError"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "version": {
                    "type": "integer",
                    "minimum": 1
                  },
                  "target": {
                    "type": "string",
                    "format": "uri"
                  },
                  "title": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "tags": {
                    "type": "string",
                    "maxLength": 160
                  },
                  "status": {
                    "enum": [
                      "active",
                      "paused"
                    ]
                  }
                },
                "required": [
                  "version"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ]
      }
    },
    "/links/{id}/stats": {
      "get": {
        "operationId": "getLinkAnalytics",
        "summary": "Read aggregated private traffic and recording gaps",
        "description": "Required scope: analytics:read. No credit charge. CSV contains aggregated rows and gap entries. The response includes missing recording periods; unrecorded traffic cannot be recovered. Trial links accept their per-link X-Cozy-Trial-Token instead of an account key. Expired trials return 410.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "from, to, refreshedAt (Unix milliseconds), total, daily, countries, devices, referrers and gaps. Counts are redirect requests, not unique people or confirmed scans."
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/RequestError"
          },
          "401": {
            "$ref": "#/components/responses/RequestError"
          },
          "402": {
            "$ref": "#/components/responses/RequestError"
          },
          "403": {
            "$ref": "#/components/responses/RequestError"
          },
          "404": {
            "$ref": "#/components/responses/RequestError"
          },
          "409": {
            "$ref": "#/components/responses/RequestError"
          },
          "429": {
            "$ref": "#/components/responses/RequestError"
          },
          "503": {
            "$ref": "#/components/responses/RequestError"
          },
          "410": {
            "$ref": "#/components/responses/RequestError"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "days",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 90,
              "default": 30
            }
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "enum": [
                "json",
                "csv"
              ],
              "default": "json"
            }
          }
        ],
        "security": [
          {
            "ApiKey": []
          },
          {
            "TrialReceipt": []
          }
        ]
      }
    },
    "/links/{id}/qr": {
      "post": {
        "operationId": "exportQrSvg",
        "summary": "Render a styled SVG for an active tracked link",
        "description": "Required scope: qr:read. No credit charge or second link creation. Light background and contrast of at least 4.5 are required. Quiet zone and error correction are handled by the renderer. PNG export is available in the browser only. Trial links accept their per-link X-Cozy-Trial-Token instead of an account key. Expired trials return 410.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/RequestError"
          },
          "401": {
            "$ref": "#/components/responses/RequestError"
          },
          "402": {
            "$ref": "#/components/responses/RequestError"
          },
          "403": {
            "$ref": "#/components/responses/RequestError"
          },
          "404": {
            "$ref": "#/components/responses/RequestError"
          },
          "409": {
            "$ref": "#/components/responses/RequestError"
          },
          "429": {
            "$ref": "#/components/responses/RequestError"
          },
          "503": {
            "$ref": "#/components/responses/RequestError"
          },
          "410": {
            "$ref": "#/components/responses/RequestError"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "preset": {
                    "enum": [
                      "classic",
                      "sage",
                      "ocean",
                      "rose",
                      "clay",
                      "ink"
                    ],
                    "default": "classic"
                  },
                  "size": {
                    "type": "integer",
                    "minimum": 256,
                    "maximum": 2048,
                    "default": 768
                  },
                  "foreground": {
                    "type": "string",
                    "pattern": "^#[a-fA-F0-9]{6}$"
                  },
                  "background": {
                    "type": "string",
                    "pattern": "^#[a-fA-F0-9]{6}$"
                  },
                  "dots": {
                    "enum": [
                      "square",
                      "rounded",
                      "dots"
                    ]
                  },
                  "corners": {
                    "enum": [
                      "square",
                      "rounded"
                    ]
                  },
                  "logoPng": {
                    "type": "string",
                    "maxLength": 700000,
                    "description": "PNG data URL, up to 512x512 pixels. No remote image fetching or persistent logo storage."
                  }
                },
                "required": [],
                "additionalProperties": false
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "security": [
          {
            "ApiKey": []
          },
          {
            "TrialReceipt": []
          }
        ]
      }
    },
    "/links/capacity": {
      "post": {
        "operationId": "buyTrackingCapacity",
        "summary": "Buy 10,000 additional recorded visits for 50 credits",
        "description": "Required scope: credits:spend. Purchased credits only. No automatic purchases. Capacity carries forward; recording is subject to shared capacity. Existing capacity and original-price refunds are preserved. Manage unused pack returns on the website.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "status": {
                      "enum": [
                        "active",
                        "refunded"
                      ]
                    },
                    "credits": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "id",
                    "status",
                    "credits"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/RequestError"
          },
          "401": {
            "$ref": "#/components/responses/RequestError"
          },
          "402": {
            "$ref": "#/components/responses/RequestError"
          },
          "403": {
            "$ref": "#/components/responses/RequestError"
          },
          "404": {
            "$ref": "#/components/responses/RequestError"
          },
          "409": {
            "$ref": "#/components/responses/RequestError"
          },
          "429": {
            "$ref": "#/components/responses/RequestError"
          },
          "503": {
            "$ref": "#/components/responses/RequestError"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "maxCredits": {
                    "const": 50
                  }
                },
                "required": [
                  "maxCredits"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Reuse this UUID and identical payload with the same key after an uncertain response. Never retry an uncertain purchase with a new UUID."
          }
        ]
      }
    },
    "/trial": {
      "get": {
        "operationId": "getTrialUsage",
        "summary": "Read remaining no-key network allowance",
        "security": [],
        "responses": {
          "200": {
            "description": "Daily allowance; shared capacity may still be exhausted. Resets at midnight UTC.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "trial": {
                      "const": true
                    },
                    "remaining": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 3
                    },
                    "dailyLinks": {
                      "const": 3
                    },
                    "expiresAfterDays": {
                      "const": 7
                    },
                    "resetAt": {
                      "type": "integer",
                      "description": "Unix milliseconds"
                    },
                    "sharedCapacity": {
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RequestError"
          },
          "503": {
            "$ref": "#/components/responses/RequestError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "A scoped cozy_live_ key created at /api-keys. Keep out of client-side code, URLs and prompts."
      },
      "TrialReceipt": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Cozy-Trial-Token",
        "description": "Private token returned on no-key link creation; valid for that link until its seven-day expiry."
      }
    },
    "schemas": {
      "Link": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "slug": {
            "type": "string"
          },
          "target": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "tags": {
            "type": "string"
          },
          "status": {
            "enum": [
              "active",
              "paused",
              "suspended",
              "deleted"
            ]
          },
          "version": {
            "type": "integer"
          },
          "environment": {
            "const": "live"
          },
          "created_at": {
            "type": "integer"
          },
          "updated_at": {
            "type": "integer"
          },
          "shortUrl": {
            "type": "string",
            "format": "uri"
          },
          "qrUrl": {
            "type": "string",
            "format": "uri"
          },
          "visits": {
            "type": "integer"
          },
          "trial": {
            "type": "boolean"
          },
          "expiresAt": {
            "type": "integer",
            "description": "Trial expiry, Unix milliseconds. Absent for account links."
          }
        },
        "required": [
          "id",
          "slug",
          "target",
          "title",
          "tags",
          "status",
          "version",
          "environment",
          "created_at",
          "updated_at",
          "shortUrl",
          "qrUrl"
        ],
        "additionalProperties": false
      }
    },
    "responses": {
      "RequestError": {
        "description": "Request rejected. Errors include api_key_invalid, api_scope_required, api_budget_exceeded, credits_required, link_allowance_required, link_changed, request_conflict, rate_limited and links_unavailable. Failed writes do not spend credits.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "string"
                }
              },
              "required": [
                "error"
              ],
              "additionalProperties": false
            }
          }
        }
      }
    }
  }
}
