# Hovercode API

> Create and manage QR codes, PDF codes, short links, landing pages, forms, and GS1 Digital Link codes programmatically.

Base URL: `https://hovercode.com`

Human-readable docs: https://hovercode.com/api/

**Prefer working from an AI assistant?** Hovercode also has an [MCP server](https://hovercode.com/mcp/docs) so tools like Claude and Cursor can create and manage all of this conversationally.

---

## Introduction

Hovercode's API lets you create and update [dynamic QR codes](https://hovercode.com/blog/static-vs-dynamic-qr-codes/), PDF codes, short links, hosted landing pages, forms, and GS1 Digital Link codes programmatically. It's ideal for creating codes in bulk or adding QR/link features to your own product.

You need access to the business plan to use the API, but you can test it for free — see [pricing](https://hovercode.com/pricing/).

**Note:** You can't call this API from the browser — that would expose your API key. Call it from a back-end (PHP, Node.js, Python, Ruby, etc.).

---

## Authentication

Every request must include an `Authorization` header with your API token:

```
Authorization: Token YOUR-TOKEN
```

Find your token in your account settings while logged in. Keep it private. Your workspace ID is also in your account settings — many endpoints require it as a field named `workspace`.

---

## QR codes

### Create a QR code

`POST https://hovercode.com/api/v2/hovercode/create/`

Generates a QR code. Returns the QR code as an SVG string by default; set `generate_png: true` to also get `.png` and `.svg` file URLs (slower). `qr_type` defaults to `Link`; use `Text` for plain text, or see the vCard and GS1 sections for those types.

**Parameters**

| Name | Required | Description |
|---|---|---|
| `workspace` | required | Your workspace ID |
| `qr_data` | required (Link/Text) | For `qr_type=Link`: a valid URL. For `qr_type=Text`: any plain text |
| `qr_type` | optional | `Link` (default), `Text`, `vCard`, or `GS1` |
| `dynamic` | optional | `false` by default. Set `true` for a dynamic QR code |
| `display_name` | optional | Internal name for organising codes |
| `domain` | optional | Custom domain for dynamic codes |
| `generate_png` | optional | `true` also returns `.png` and `.svg` file URLs |
| `gps_tracking` | optional | Enables GPS tracking for dynamic codes |
| `error_correction` | optional | `L`, `M`, `Q`, or `H` |
| `size` | optional | Width in pixels. Defaults to `220` |
| `logo_url` | optional | URL to an image to embed as a logo |
| `folder` | optional | ID or slug of a folder to file the code into (see [Folders](#folders)) |
| `template` | optional | ID of a design template saved in your workspace. Applies its full design (colors, pattern, eye style, frame, text, logo); fields passed explicitly override the template. Get IDs from List templates |
| `logo_round` | optional | Force logo into a circle shape |
| `primary_color` | optional | Hex color (with `#`). Defaults to `#111111`. Eyes and frame use this color too unless `eye_color`/`frame_color` are also given |
| `eye_color` | optional | Hex color for the corner eyes, if different from `primary_color` |
| `background_color` | optional | Hex color (with `#`). Transparent by default |
| `pattern` | optional | `Original` (default), `Circles`, `Squares`, `Diamonds`, `Triangles` |
| `eye_style` | optional | `Square` (default), `Rounded`, `Drop`, `Leaf` |
| `frame` | optional | `border`, `border-small`, `border-large`, `square`, `speech-bubble`, `speech-bubble-above`, `card`, `card-above`, `text-frame`, `round-frame`, `circle-viewfinder`, `solid-spin`, `burst`, `scattered-lines`, `polkadot`, `swirl`. If `text` is set and this is omitted (or the frame given doesn't render text), a frame that does (`card`) is used automatically |
| `frame_color` | optional | Hex color for the frame, if different from `primary_color` |
| `has_border` | optional | For frames with a border option |
| `text` | optional | For frames with a text option — see the automatic frame selection under `frame` above |

```python
import requests

data = {
    "workspace": "YOUR-WORKSPACE-ID",
    "qr_data": "https://twitter.com/hovercodeHQ",
    "primary_color": "#1DA1F2"
}

response = requests.post(
    "https://hovercode.com/api/v2/hovercode/create/",
    headers={"Authorization": "Token YOUR-TOKEN"},
    json=data,
    timeout=10,
)
```

### Create a vCard QR code

`POST https://hovercode.com/api/v2/hovercode/create/`

Set `qr_type` to `vCard` and pass a nested `vcard` object (instead of `qr_data`). A dynamic vCard (`dynamic: true`) is editable later; a static one (default) encodes the contact directly. `first_name` is required; all other vCard fields (`last_name`, `company_name`, `position`, `email`, `mobile_number`, `phone_number`, `website`, `street`, `city`, `state`, `country`, `post_code`, `description`) are optional.

```python
import requests

data = {
    "workspace": "YOUR-WORKSPACE-ID",
    "qr_type": "vCard",
    "dynamic": True,
    "vcard": {
        "first_name": "Ada",
        "last_name": "Lovelace",
        "email": "ada@example.com"
    }
}

response = requests.post(
    "https://hovercode.com/api/v2/hovercode/create/",
    headers={"Authorization": "Token YOUR-TOKEN"},
    json=data,
    timeout=10,
)
```

### List QR codes

`GET https://hovercode.com/api/v2/workspace/{WORKSPACE-ID}/hovercodes/`

Each result: `id`, `display_name`, `qr_data`, `qr_type`, `shortlink_url`, `dynamic`, `png`, `created`, and `folder` (an object with `id`, `name`, `slug` and `path` — the full breadcrumb — or `null` when the code isn't filed).

Paginated 50 per page; `?page=` walks the pages and `?page_size=` raises it to 200. Add `?q=` to search links, display names, shortlink URLs, and tags.

**Query parameters**

| Name | Description |
|---|---|
| `q` | Search links, display names, shortlink URLs, and tags |
| `folder` | Only codes in this folder — its `id` or its `slug`. Use `none` for codes that aren't in a folder |
| `subfolders` | With `folder`, also include codes in its subfolders. `true`/`false` |
| `include` | Comma-separated extras: `scans` adds `total_scans` and `unique_scans` to every code, `tags` adds each code's tags |
| `days` / `start` / `end` | With `include=scans`, count a window instead of all time (see below) |
| `page`, `page_size` | Pagination — 50 per page, up to 200 |

`include=scans` counts the whole page in one query, so you don't need a request per code. The counts are **lifetime** by default (the same numbers as the dashboard and the single-code endpoints); add `?days=30`, or `?start=2026-01-01&end=2026-03-31`, to count a window instead. Codes that can't be tracked at all (static codes have no short link) report `null` rather than `0`.

```
GET /api/v2/workspace/{WORKSPACE-ID}/hovercodes/?include=scans&page_size=200
```

```json
{
  "count": 128,
  "next": "https://hovercode.com/api/v2/workspace/.../hovercodes/?page=2&page_size=200",
  "results": [
    {
      "id": "2fbb014a-4b5a-4ecd-95a3-p914d4aa167b",
      "display_name": "Spring menu",
      "qr_data": "https://example.com/menu",
      "qr_type": "Link",
      "shortlink_url": "https://qrr.to/spring-menu",
      "dynamic": true,
      "created": "2026-03-04T09:12:44.221Z",
      "folder": {
        "id": "6b0c1f8e-1c2a-4a7f-9a0e-5d2b3c4e5f60",
        "name": "Spring 2026",
        "slug": "spring-2026",
        "path": "Campaigns / Spring 2026"
      },
      "total_scans": 1841,
      "unique_scans": 1502
    }
  ]
}
```

### Reporting: building a table of codes and scans

This is the shape most BI tools (Power BI, Looker Studio, a warehouse sync) want — every code with its folder and scan counts, without a request per code:

```
GET https://hovercode.com/api/v2/workspace/{WORKSPACE-ID}/hovercodes/?include=scans&page_size=200
```

Walk `next` until it's `null`. Each row already carries `folder name`, `id`, `created`, `display_name`, `qr_data`, `shortlink_url`, `qr_type`, `total_scans` and `unique_scans`.

In Power BI, `Web.Contents` with the `Authorization` header, then expand the `folder` column to get `folder.name` or `folder.path`:

```
let
    Source = Json.Document(Web.Contents(
        "https://hovercode.com/api/v2/workspace/YOUR-WORKSPACE-ID/hovercodes/?include=scans&page_size=200",
        [Headers=[Authorization="Token YOUR-TOKEN"]]
    )),
    Codes = Table.FromRecords(Source[results]),
    Expanded = Table.ExpandRecordColumn(Codes, "folder", {"name", "path"}, {"folder_name", "folder_path"})
in
    Expanded
```

Note that `/workspace/{WORKSPACE-ID}/analytics/` is a *different* thing: one aggregate row for the whole workspace, not a row per code. Use it for headline totals, not for a code-level table.

### Folders

`GET https://hovercode.com/api/v2/workspace/{WORKSPACE-ID}/folders/`

The workspace's folders, so a report can resolve or filter by them. Each result: `id`, `name`, `slug`, `path` (full breadcrumb), `parent` (a folder id, or `null` at the top level), `code_count`, and `created`.

`POST https://hovercode.com/api/v2/folder/create/`

Creates a folder. Fields: `workspace` (required), `name` (required), and `parent` (optional — a folder `id` or `slug` to nest under). Creating a folder that already exists with the same name under the same parent returns the existing one instead of a duplicate, so a sync job can call it repeatedly.

`PATCH https://hovercode.com/api/v2/folder/{FOLDER-ID}/update/`

Renames and/or moves a folder. Fields: `name` (new name; the slug is regenerated) and `parent` (a folder `id` or `slug` to nest under, or `null` for the top level). A folder can't be moved into itself or one of its subfolders.

`DELETE https://hovercode.com/api/v2/folder/{FOLDER-ID}/delete/`

Deletes a folder. Codes in it (and in its subfolders) become unfiled — they are never deleted — and its subfolders move up to its parent. Returns `204`.

To file a code into a folder, pass `folder` (an `id` or `slug`) when creating it, or send `folder` to the update endpoint to move it — `"folder": null` takes it out of its folder.

### List templates

`GET https://hovercode.com/api/v2/workspace/{WORKSPACE-ID}/templates/`

Returns the QR design templates saved in your workspace (created in the dashboard), paginated 50 per page. Each result includes `id`, `title`, `primary_color`, `eye_color`, `background_color`, `pattern`, `eye_style`, `frame_color`, `has_border`, `text`, `frame`, `has_logo`, and a `preview` image URL. Pass a template's `id` as the `template` field on the create endpoint to apply its design.

### Get a single QR code

`GET https://hovercode.com/api/v2/hovercode/{QR-CODE-ID}/`

### Get QR code tracking activity

`GET https://hovercode.com/api/v2/hovercode/{QR-CODE-ID}/activity/`

Paginated (max 200 via `page_size`). Each result: `qr_code_id`, `time_utc`, `time_timezone_aware`, `location`, `device`, `scanner_id`, `id`.

### Get scan analytics

`GET https://hovercode.com/api/v2/hovercode/{QR-CODE-ID}/analytics/`

Aggregated stats for one code over a period: `total_scans`, `unique_scans`, `scans_by_day`, and top `top_countries`, `top_cities`, `top_os`, `top_browsers`.

The period is the last 30 days by default. Add `?days=` (up to 365), or set an explicit range with `?start=2026-01-01&end=2026-03-31` (ISO dates or datetimes; `start` alone means "since then", which is how you get all-time figures).

For the whole workspace, use `GET https://hovercode.com/api/v2/workspace/{WORKSPACE-ID}/analytics/` — same shape, plus `top_codes` (each with `id`, `display_name`, `destination`, `scans` and `unique_scans`; `?top=` returns more than the default 10, up to 100). It's one aggregate for the workspace — for per-code numbers, use `?include=scans` on the [code listing](#list-qr-codes) or the single-code endpoint above.

### Download images

`GET https://hovercode.com/api/v2/hovercode/{QR-CODE-ID}/download/`

Returns print-ready image URLs: `png_url` (high-resolution PNG) and `svg_url` (vector SVG — best for large sizes/print). Images are generated on first request.

### Update a QR code

`POST https://hovercode.com/api/v2/hovercode/{QR-CODE-ID}/update/` (`PATCH` also works)

Change `display_name`, `qr_data` (Link type only), `gps_tracking`, or `folder` (a folder `id` or `slug` to move the code; `null` takes it out of its folder). Returns the same shape as GET.

### Update a QR code's design

`PATCH https://hovercode.com/api/v2/hovercode/{QR-CODE-ID}/design/`

Restyle an existing code **in place** — the code, its destination, and its scan history are unchanged, so an already-printed code keeps working. The QR is re-rendered and the new `svg` is returned. Accepts any of: `primary_color`, `eye_color`, `background_color`, `pattern`, `eye_style`, `frame`, `frame_color`, `has_border`, `text`, `text_secondary`, `error_correction`, `logo_url`, `logo_base64` (a JPEG/PNG/GIF/WebP under 2 MB, base64-encoded, `data:` prefix allowed), `logo_round`, and `remove_logo` (set `true` to clear the logo). Same values as the create endpoint.

Sending `primary_color` alone recolors the eyes and frame to match too, unless `eye_color`/`frame_color` are also given — restyling a code made outside the API (dashboard, browser extension, Figma, Canva) now behaves the same way. Sending `text` without a frame that can show it picks one (`card`) automatically rather than silently dropping the caption; an explicitly chosen frame is never overridden.

```python
import requests

response = requests.patch(
    "https://hovercode.com/api/v2/hovercode/{QR-CODE-ID}/design/",
    headers={"Authorization": "Token YOUR-TOKEN"},
    json={"primary_color": "#1DA1F2", "pattern": "Circles", "eye_style": "Rounded"},
    timeout=10,
)
```

### Tags

`GET https://hovercode.com/api/v2/workspace/{WORKSPACE-ID}/tags/`

The workspace's tags. Each result: `id`, `title`, `slug`.

`POST https://hovercode.com/api/v2/hovercode/{QR-CODE-ID}/tags/add/`

Adds tags to a code. Send a JSON list of `{"title": "..."}` objects (a tag is created in the workspace if it doesn't exist) or `{"id": "..."}` for existing tags. Returns the code with its `tags`.

`DELETE https://hovercode.com/api/v2/hovercode/{QR-CODE-ID}/tag/{TAG-ID-OR-SLUG}/remove/`

Removes one tag from a code (the tag itself is kept). Returns the code with its remaining `tags`.

### Delete a QR code

`DELETE https://hovercode.com/api/v2/hovercode/{QR-CODE-ID}/delete/`

Returns `204` on success.

---

## PDF codes

Host a PDF and get a dynamic QR code that points at it — for menus, flyers, tickets, and similar. Because it's dynamic you can swap the PDF later without reprinting the code, and scans are tracked. PDF codes are dynamic, so they count toward your plan's dynamic-code limit.

### Create a PDF code

`POST https://hovercode.com/api/v2/pdf/create/`

Provide the PDF as **either** `pdf_url` (Hovercode fetches it) **or** `pdf_base64` (the file's base64-encoded bytes). Returns the QR code plus its shareable short link and the hosted PDF URL.

**Parameters**

| Name | Required | Description |
|---|---|---|
| `workspace` | required | Your workspace ID |
| `pdf_url` | required* | A URL Hovercode fetches the PDF from. *Provide this or `pdf_base64` |
| `pdf_base64` | required* | Base64-encoded PDF bytes (a `data:` URI prefix is allowed), max 20 MB. *Provide this or `pdf_url` |
| `filename` | optional | Name for the stored file, e.g. `menu.pdf` |
| `display_name` | optional | Internal name for organising codes |
| `domain` | optional | A custom short-link domain available to your workspace |

```python
import requests

data = {
    "workspace": "YOUR-WORKSPACE-ID",
    "pdf_url": "https://example.com/menu.pdf",
    "filename": "menu.pdf"
}

response = requests.post(
    "https://hovercode.com/api/v2/pdf/create/",
    headers={"Authorization": "Token YOUR-TOKEN"},
    json=data,
    timeout=30,
)
```

The response is the standard QR code shape (`id`, `qr_data`, `shortlink_url`, `dynamic`, `svg`, …) plus `pdf_file_url` for the raw hosted file.

---

## Short links

A short link is a dynamic redirect with an associated QR code (the SVG is generated immediately; the `.png` is generated on first request).

### Create a short link

`POST https://hovercode.com/api/v2/link/create/`

**Parameters**

| Name | Required | Description |
|---|---|---|
| `workspace` | required | Your workspace ID |
| `link` | required | The destination URL |
| `slug` | optional | Custom slug (letters, numbers, hyphens). Unique per domain; not a reserved word. Auto-generated if omitted |
| `domain` | optional | One of the short link domains available to your workspace |
| `display_name` | optional | Internal organising name |
| `gps` | optional | Enable GPS location tracking |

```python
import requests

data = {
    "workspace": "YOUR-WORKSPACE-ID",
    "link": "https://twitter.com/hovercodeHQ",
    "slug": "hovercode-twitter"
}

response = requests.post(
    "https://hovercode.com/api/v2/link/create/",
    headers={"Authorization": "Token YOUR-TOKEN"},
    json=data,
    timeout=10,
)
```

### Get short links

`GET https://hovercode.com/api/v2/workspace/{WORKSPACE-ID}/links/`

Paginated. Add `?q=` to search destinations, display names, and slugs.

### Get a short link

`GET https://hovercode.com/api/v2/link/{LINK-ID}/`

### Update a short link

`PATCH https://hovercode.com/api/v2/link/{LINK-ID}/update/`

Update `link`, `display_name`, or `gps`.

### Delete a short link

`DELETE https://hovercode.com/api/v2/link/{LINK-ID}/delete/`

Returns `204`.

### Short link QR image

`GET https://hovercode.com/api/v2/link/{LINK-ID}/qr/?format=png`

`?format=svg` returns the SVG inline; `?format=png` (default) redirects to a generated `.png`.

### Short link activity

`GET https://hovercode.com/api/v2/link/{LINK-ID}/activity/`

Same paginated format as QR code activity.

---

## Landing pages

A landing page (micro "link-in-bio" page) is a hosted page with a title and a list of link buttons, plus its own short URL and QR code. Pages count toward your plan's page limit.

### Create a landing page

`POST https://hovercode.com/api/v2/page/create/`

Creates and publishes a page in one call, and returns its live URL and QR code.

**Parameters**

| Name | Required | Description |
|---|---|---|
| `workspace` | required | Your workspace ID |
| `title` | required | The page heading |
| `links` | optional | An ordered array of link buttons. Each: `url` (required), `title`, `type` (e.g. `Website` (default), `Instagram`, `YouTube`) |
| `description` | optional | Text shown under the title |
| `bg_color` | optional | Hex background color |
| `text_color` | optional | Hex text color |
| `domain` | optional | A custom page domain available to your workspace |
| `slug` | optional | Custom URL slug; auto-generated if omitted |

```python
import requests

data = {
    "workspace": "YOUR-WORKSPACE-ID",
    "title": "My Links",
    "links": [
        {"url": "https://example.com", "title": "Website"},
        {"url": "https://instagram.com/example", "title": "Instagram", "type": "Instagram"}
    ]
}

response = requests.post(
    "https://hovercode.com/api/v2/page/create/",
    headers={"Authorization": "Token YOUR-TOKEN"},
    json=data,
    timeout=10,
)
```

Response: `id`, `title`, `description`, `status`, `share_url` (the live page URL), `slug`, `qr_id`, `svg` (the QR), `links`, `created`.

### Get a landing page

`GET https://hovercode.com/api/v2/page/{PAGE-ID}/`

### List landing pages

`GET https://hovercode.com/api/v2/workspace/{WORKSPACE-ID}/pages/`

Returns the workspace's pages, newest first.

---

## Forms

Forms collect submissions through a shareable link or QR code. Building a form is a three-step flow: **create** the form, **save** its fields, then **publish** it — publishing mints the short link and QR code and makes the form live. You can then read submissions. Publishing creates a dynamic code, so it counts toward your dynamic-code limit.

### Create a form

`POST https://hovercode.com/api/v2/form/create/`

**Parameters**

| Name | Required | Description |
|---|---|---|
| `workspace` | required | Your workspace ID |
| `title` | required | The form title |
| `description` | optional | Shown above the fields |

Returns the form, including its `id`.

### Save fields

`POST https://hovercode.com/api/v2/form/{FORM-ID}/save/`

Saves the form's fields (and optional form-level settings) in one atomic call. Pass a `fields` array in display order. **New fields must be given an `id` that starts with `temp-`** (existing fields keep their real `id`); any field left out of the array is removed.

Each field: `field_type` (`text`, `email`, `phone`, `number`, `textarea`, `select`, `radio`, `checkbox`, `date`, `time`, `url`, `rating`, `scale`, `yes_no`, `image`), `label` (required), and optionally `required`, `placeholder`, and `options` (for `select`/`radio`/`checkbox`, as `[{"value": …, "label": …}]`). `image` (respondent photo/image upload) is available on paid plans — publishing a form containing one requires an active subscription.

The same call also accepts form-level settings: `title`, `description`, `redirect_url`, `thank_you_message`, and **branding** — `bg_color`, `text_color`, `button_color`, `button_text_color` (hex). To **edit** an existing form later, send the fields again including each field's real `id` to keep it (omit a field to remove it, leave `id` off a new one), then re-publish.

### Set a form's logo

`POST https://hovercode.com/api/v2/form/{FORM-ID}/logo-url/`

Set the form's logo from an image URL — `{"logo_url": "https://…/logo.png"}` (JPEG/PNG/GIF/WebP, max 2 MB). The JSON alternative to the multipart logo upload.

```python
import requests

fields = {
    "fields": [
        {"id": "temp-0", "field_type": "text", "label": "Name", "required": True},
        {"id": "temp-1", "field_type": "email", "label": "Email", "required": True},
        {"id": "temp-2", "field_type": "select", "label": "Shirt size",
         "options": [{"value": "S", "label": "S"}, {"value": "M", "label": "M"}]}
    ]
}

response = requests.post(
    "https://hovercode.com/api/v2/form/{FORM-ID}/save/",
    headers={"Authorization": "Token YOUR-TOKEN"},
    json=fields,
    timeout=10,
)
```

### Publish a form

`POST https://hovercode.com/api/v2/form/{FORM-ID}/publish/`

Snapshots the current fields as the live form, creates the short link and QR code, and sets the form to `Published`. Optional body: `domain` and `slug` (as with short links). Returns the form including `shortlink_url`, `qr_code_svg`, and (on first publish) `share_url`.

### Close or reopen a form

`POST https://hovercode.com/api/v2/form/{FORM-ID}/close/` stops accepting responses; `POST .../reopen/` resumes.

### List forms

`GET https://hovercode.com/api/v2/forms/?workspace={WORKSPACE-ID}`

The workspace's forms (newest first). `?workspace=` is required. Add `?q=` to search by title. Paginated (`?page=`, `?page_size=` up to 100).

### List responses

`GET https://hovercode.com/api/v2/forms/{FORM-ID}/responses/`

A form's submissions, newest first. Each response includes its per-field answers.

---

## GS1 Digital Link

A GS1 Digital Link encodes one identifier (e.g. a GTIN) that resolves to a destination. The quickest way to make one is a single call to [Create a GS1 QR code](#create-a-gs1-qr-code) — pass an identifier and a destination.

The product/link endpoints below are for **advanced** use: managing an identifier's *multiple* link types (product info, instructions, recalls…) over time, and reusing one identifier across several codes. You don't need them for a basic GS1 QR. Resolution is handled by Hovercode's resolver.

### Create a GS1 product

`POST https://hovercode.com/api/v2/gs1/products/`

**Parameters**

| Name | Required | Description |
|---|---|---|
| `workspace` | required | Your workspace ID |
| `identifier_value` | required | The identifier value, e.g. a GTIN. Validated incl. GS1 check digit |
| `identifier_type` | optional | GS1 Application Identifier. One of: `01` (GTIN), `00` (SSCC), `414` (GLN), `417` (Party GLN), `253` (GDTI), `255` (GCN), `401` (GINC), `402` (GSIN), `8003` (GRAI), `8004` (GIAI), `8006` (ITIP), `8013` (GMN), `8017` (GSRN – Provider), `8018` (GSRN – Recipient). Defaults to `01` |
| `batch_lot`, `serial_number` | optional | Qualifiers added to the Digital Link path |
| `expiry_date` | optional | `YYMMDD` format (e.g. `261231`) |
| `destination_url` | optional | Seeds a default product information page (`gs1:pip`) link |

```python
import requests

data = {
    "workspace": "YOUR-WORKSPACE-ID",
    "identifier_type": "01",
    "identifier_value": "09506000134369",
    "destination_url": "https://example.com/product"
}

response = requests.post(
    "https://hovercode.com/api/v2/gs1/products/",
    headers={"Authorization": "Token YOUR-TOKEN"},
    json=data,
    timeout=10,
)
```

### List GS1 products

`GET https://hovercode.com/api/v2/workspace/{WORKSPACE-ID}/gs1/products/`

### Get, update or delete a GS1 product

`GET | PATCH | DELETE https://hovercode.com/api/v2/gs1/products/{PRODUCT-ID}/`

### Manage link types

`POST https://hovercode.com/api/v2/gs1/products/{PRODUCT-ID}/links/`

Each product resolves to one destination per link type. Exactly one is the default — setting a new default unsets the others, and deleting the default promotes another.

**Parameters**

| Name | Required | Description |
|---|---|---|
| `link_type` | required | One of: `gs1:pip`, `gs1:quickStartGuide`, `gs1:instructions`, `gs1:safetyInfo`, `gs1:recipeInfo`, `gs1:traceability`, `gs1:hasRetailers`, `gs1:recallStatus`, `gs1:review`, `gs1:ePIL`, `gs1:productSustainabilityInfo`, `gs1:promotion`, `gs1:masterData`, `gs1:smpc`, `gs1:certificationInfo`, `gs1:registerProduct` |
| `title` | required | Human-readable label |
| `destination_url` | required | Where this link type resolves to |
| `is_default`, `language`, `media_type` | optional | Default flag, language (e.g. `en`), media type (defaults to `text/html`) |

Update or delete a link with `PATCH | DELETE https://hovercode.com/api/v2/gs1/products/{PRODUCT-ID}/links/{LINK-ID}/`.

### Get the linkset

`GET https://hovercode.com/api/v2/gs1/products/{PRODUCT-ID}/linkset/`

Returns an [RFC 9264](https://www.rfc-editor.org/rfc/rfc9264.html) linkset (`application/linkset+json`) — a preview of what the resolver serves. Optional `?domain=` sets the anchor domain.

### Create a GS1 QR code

`POST https://hovercode.com/api/v2/hovercode/create/`

The quickest way to make a GS1 QR. Set `qr_type` to `GS1` and pass a `gs1_product` **object** with an `identifier_value` and a `destination_url` — this creates the GS1 product and the QR in one call. GS1 codes are always dynamic — the QR encodes the Digital Link URI, which the resolver redirects by link type.

```python
import requests

data = {
    "workspace": "YOUR-WORKSPACE-ID",
    "qr_type": "GS1",
    "gs1_product": {
        "identifier_value": "09506000134369",
        "destination_url": "https://example.com/product"
    }
}

response = requests.post(
    "https://hovercode.com/api/v2/hovercode/create/",
    headers={"Authorization": "Token YOUR-TOKEN"},
    json=data,
    timeout=10,
)
```

To reuse a product you already created (see the advanced endpoints above), pass its id as a string instead: `"gs1_product": "{PRODUCT-ID}"`.

---

## Webhooks

Enable webhooks from your workspace API settings (Business Plus plan). Every scan of a dynamic QR code or short link triggers a `POST` to your URL with `Content-Type: application/json` and an `x-signature` header. Verify `x-signature` against your webhook secret before processing.

```json
{
  "qr_code_id": "2fbb014a-4b5a-4ecd-95a3-p914d4aa167b",
  "time_utc": "2026-06-01 17:44:48.920050+00:00",
  "time_timezone_aware": "Jun. 1, 2026, 05:44 p.m.",
  "location": "London, England, United Kingdom",
  "device": "iPhone, iOS, Mobile Safari",
  "scanner_id": "5dd831a872687315f54a11fa62d089e66887647c67d5ad2cd89ebd3a38084bd3",
  "id": "0acb2379-c9e3-4245-a1e3-6e542cc02637"
}
```

---

## MCP server

Hovercode has a [Model Context Protocol](https://modelcontextprotocol.io/) server, so AI assistants like Claude and Cursor can create and manage QR codes, PDF codes, short links, pages, and forms conversationally — no API calls to write. Anything the assistant creates lands in your dashboard like any other code.

Endpoint: `https://hovercode.com/mcp/`

Connect it two ways:

- **OAuth** — in a client that supports remote MCP connectors, add `https://hovercode.com/mcp/` and approve access (you'll go through a normal Hovercode login). Nothing to copy.
- **API token** — for CLI/config clients, pass your token as a bearer token. In Claude Code:

```
claude mcp add --transport http hovercode https://hovercode.com/mcp/ \
  --header "Authorization: Bearer YOUR-TOKEN"
```

- **npm bridge** — for clients that only support local (stdio) servers, `npx -y hovercode-mcp` ([hovercode-mcp](https://www.npmjs.com/package/hovercode-mcp)) bridges to the hosted server. Set `HOVERCODE_API_TOKEN` to skip the browser login.

**Things you can ask for once it's connected:**

- "Point my printed *menu* QR code at the new PDF at this link" — retarget a dynamic code without reprinting it.
- "Make a form to collect name, email, and a rating, and give me a QR for the table."
- "Turn this PDF into a QR code for the flyer."
- "How did my pricing-page QR do this month, and where were people scanning it?"
- "Make a link-in-bio page with my Instagram, shop, and booking links, and a QR for the window."
- "Summarize this week's responses to my feedback form."

Building your own agent? This same guide is served as markdown at `https://hovercode.com/mcp/docs`.

---

## Support

This API is in active development — [send feedback or questions](https://hovercode.com/contact/) any time.
