{
  "openapi": "3.1.0",
  "info": {
    "title": "Lynkarr API",
    "version": "1.0.0",
    "summary": "Create, repoint and read QR codes, short links and the events they produce.",
    "description": "A REST API over the same data the dashboard shows.\n\n**Authentication.** Every request carries `Authorization: Bearer lyn1_…`. Tokens are created in Settings, carry only the scopes you tick, and are shown once.\n\n**Rate limits.** Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`, so a client can pace itself rather than discovering the limit by exceeding it.\n\n**Reading what has happened.** `/events` and `/submissions` return newest first with a stable `id` on every row, which is what an automation platform deduplicates against.",
    "contact": {
      "name": "Lynkarr support",
      "url": "https://support.lynkarr.com"
    }
  },
  "servers": [
    {
      "url": "https://lynkarr.com/api/v1"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API token from Settings. It starts `lyn1_`. Note this is the only accepted form: `X-API-Key` works on the MCP endpoint and not here."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "unauthorized",
                  "forbidden",
                  "not_found",
                  "invalid_request",
                  "rate_limited",
                  "quota_exceeded",
                  "plan_required"
                ]
              },
              "message": {
                "type": "string"
              },
              "docs": {
                "type": "string"
              }
            }
          }
        }
      },
      "Code": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "qr_9Vd2rTn4"
          },
          "name": {
            "type": "string",
            "example": "Autumn poster"
          },
          "mode": {
            "type": "string",
            "enum": [
              "dynamic",
              "static"
            ]
          },
          "shortCode": {
            "type": [
              "string",
              "null"
            ],
            "example": "aB3xY"
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "description": "The address the symbol encodes, on your own domain if you have one."
          },
          "destination": {
            "type": [
              "string",
              "null"
            ]
          },
          "scans": {
            "type": "integer"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Event": {
        "type": "object",
        "description": "One scan, click or view. The unit a polling integration reads.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable. Use it to recognise a repeat."
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time"
          },
          "type": {
            "type": "string",
            "enum": [
              "scan",
              "link_click",
              "card_view",
              "page_view",
              "form_view",
              "form_submit",
              "gs1_resolve"
            ]
          },
          "codeId": {
            "type": [
              "string",
              "null"
            ]
          },
          "placement": {
            "type": [
              "string",
              "null"
            ]
          },
          "country": {
            "type": [
              "string",
              "null"
            ],
            "description": "Two letters, from the network."
          },
          "region": {
            "type": [
              "string",
              "null"
            ]
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "os": {
            "type": [
              "string",
              "null"
            ]
          },
          "device": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "mobile",
              "tablet",
              "desktop",
              "unknown",
              null
            ]
          },
          "browser": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      }
    },
    "parameters": {
      "limit": {
        "name": "limit",
        "in": "query",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 25
        }
      },
      "offset": {
        "name": "offset",
        "in": "query",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "default": 0
        }
      }
    }
  },
  "x-errorCodes": {
    "unauthorized": "No token, or one that is not ours. Check the Authorization header.",
    "forbidden": "The token is valid but lacks the scope this endpoint needs.",
    "not_found": "No such thing, or it belongs to another account.",
    "invalid_request": "Something in the request is wrong; the message says what.",
    "rate_limited": "Too many requests this minute. Wait, and see the RateLimit headers.",
    "quota_exceeded": "The monthly allowance is spent. Distinct from rate_limited on purpose: a burst is fixed by waiting seconds, a quota by waiting for the month or changing plan.",
    "plan_required": "This account's plan does not include the endpoint."
  },
  "paths": {
    "/me": {
      "get": {
        "summary": "Who this token belongs to",
        "description": "The account, the token's scopes and its remaining allowance. Needs no scope, so it is the right call for checking a connection works.",
        "responses": {
          "200": {
            "description": "The account and the token."
          },
          "401": {
            "description": "No token, or not one of ours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. The RateLimit headers say when to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/codes": {
      "get": {
        "summary": "List QR codes",
        "x-scope": "codes:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          },
          {
            "name": "name",
            "in": "query",
            "description": "Matches on the name only, never the destination — a search for 'example' that returned every code pointing at a domain would surprise whoever ran it.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Code"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not one of ours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. The RateLimit headers say when to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a QR code",
        "x-scope": "codes:write",
        "responses": {
          "201": {
            "description": "The whole code, including the address the symbol should encode.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Code"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not one of ours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. The RateLimit headers say when to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/codes/{id}": {
      "get": {
        "summary": "Read one code",
        "x-scope": "codes:read",
        "responses": {
          "200": {
            "description": "The code."
          },
          "401": {
            "description": "No token, or not one of ours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. The RateLimit headers say when to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Change a code",
        "description": "Including where it points. The printed symbol never changes, which is the whole reason a dynamic code exists.",
        "x-scope": "codes:write",
        "responses": {
          "200": {
            "description": "The code as it now is."
          },
          "401": {
            "description": "No token, or not one of ours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. The RateLimit headers say when to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/codes/{id}/clone": {
      "post": {
        "summary": "Duplicate a code",
        "description": "A second code with the same destination, design, routing rules and gate, and none of the same history. The copy gets its own short address — the original's is what makes it that code — and starts on zero scans.\n\nNamed `<original> (copy)`, numbered if that name is taken.\n\nIt counts against the plan's allowance, because it is a code.",
        "x-scope": "codes:write",
        "responses": {
          "201": {
            "description": "The copy, with its own id and short address."
          },
          "401": {
            "description": "No token, or not one of ours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. The RateLimit headers say when to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/codes/{id}/clear-scans": {
      "post": {
        "summary": "Delete a code's scan history",
        "description": "For the last moment before a launch, when the proofs have been scanned a dozen times in the office and the campaign should not open on twelve scans from one postcode.\n\nIt destroys data: the events, the daily totals, any alerts raised from them, and the lifetime counter. There is no undo. The code itself and every printed copy of it are untouched.\n\nThe response says how many events were removed.",
        "x-scope": "codes:write",
        "responses": {
          "200": {
            "description": "Cleared, with a count of what went."
          },
          "401": {
            "description": "No token, or not one of ours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. The RateLimit headers say when to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/codes/{id}/archive": {
      "post": {
        "summary": "Take a code out of use",
        "description": "Anybody scanning it afterwards is told it is no longer active rather than being sent anywhere.",
        "x-scope": "codes:write",
        "responses": {
          "200": {
            "description": "Archived."
          },
          "401": {
            "description": "No token, or not one of ours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. The RateLimit headers say when to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/links": {
      "get": {
        "summary": "List short links",
        "x-scope": "links:read",
        "responses": {
          "200": {
            "description": "Newest first."
          },
          "401": {
            "description": "No token, or not one of ours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. The RateLimit headers say when to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a short link",
        "x-scope": "links:write",
        "responses": {
          "201": {
            "description": "The link, with its address."
          },
          "401": {
            "description": "No token, or not one of ours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. The RateLimit headers say when to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/events": {
      "get": {
        "summary": "Recent scans, clicks and views",
        "description": "Newest first, always, with a stable `id` on every row.\n\nAutomated traffic is excluded: a link-preview robot arriving in somebody's workflow as a scan is worse than useless, because they act on it.\n\nThe default page is deliberately large. Automation platforms do not fetch a second page for a polling trigger, so anything pushed off the end between two polls is simply lost.",
        "x-scope": "analytics:read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "codeId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "type",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Event"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not one of ours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. The RateLimit headers say when to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/submissions": {
      "get": {
        "summary": "Recent form replies",
        "description": "With the answers. Newest first, with a stable `id`.",
        "x-scope": "forms:read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "formId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Newest first."
          },
          "401": {
            "description": "No token, or not one of ours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. The RateLimit headers say when to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/hooks": {
      "post": {
        "summary": "Subscribe to events as they happen",
        "description": "For automation platforms. Needs the plan that includes webhooks; `/events` is the path for everybody else.\n\nReturns an id to unsubscribe with. Delivery is at least once, so the same event can arrive twice — recognise it by its id.",
        "x-scope": "analytics:read",
        "responses": {
          "201": {
            "description": "Subscribed."
          },
          "401": {
            "description": "No token, or not one of ours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The plan does not include webhooks. Use /events instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. The RateLimit headers say when to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/hooks/{id}": {
      "delete": {
        "summary": "Unsubscribe",
        "x-scope": "analytics:read",
        "responses": {
          "200": {
            "description": "Gone."
          },
          "401": {
            "description": "No token, or not one of ours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. The RateLimit headers say when to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/analytics/codes/{id}": {
      "get": {
        "summary": "How one code has performed",
        "description": "Aggregated, because an integration wants totals rather than every row.",
        "x-scope": "analytics:read",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 3650,
              "default": 30
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Totals and breakdowns."
          },
          "401": {
            "description": "No token, or not one of ours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this endpoint needs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. The RateLimit headers say when to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  }
}