ScanMosaic
REST API · v1

把 QR Code 和短链接集成到你的 App

从你自己的代码创建动态 QR Code、短链接和条码,更改它们的目的地,读取扫描统计,并产生可直接印刷的 QR Code 图片。通过 HTTPS 发送 JSON,一把 API 密钥,错误消息清楚易懂。

基础网址
https://scanmosaic.com/api/v1
验证
Authorization: Bearer tsk_…
方案
商业

快速入门

  1. 在控制台打开 API 密钥(Business 方案)并创建密钥。请拷贝保存:密钥只会显示一次。
  2. 把密钥存成服务器上的环境变量,例如 TESSERA_API_KEY。绝对不要放在浏览器或 App 的代码中。
  3. 创建动态 QR Code。回应的 url 字段是它的短链接;把这个链接编码,或从 /qr 下载现成的图片。

cURL

curl "https://scanmosaic.com/api/v1/items" \
  -H "Authorization: Bearer $TESSERA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"kind": "qr", "name": "Table menu", "dest": "https://example.com/menu"}'

JavaScript

const res = await fetch("https://scanmosaic.com/api/v1/items", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.TESSERA_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "kind": "qr",
    "name": "Table menu",
    "dest": "https://example.com/menu"
  })
});
const body = await res.json();
if (!res.ok) throw new Error(body.error.message);
console.log(body.data);

Python

import os, requests

r = requests.post(
    "https://scanmosaic.com/api/v1/items",
    headers={"Authorization": f"Bearer {os.environ['TESSERA_API_KEY']}"},
    json={
    "kind": "qr",
    "name": "Table menu",
    "dest": "https://example.com/menu"
},
)
r.raise_for_status()
print(r.json()["data"])

cURL

curl "https://scanmosaic.com/api/v1/qr?item=k3Xp9Qa&size=1024" \
  -H "Authorization: Bearer $TESSERA_API_KEY" \
  -o menu.png

JavaScript

import { writeFile } from "node:fs/promises";

const res = await fetch("https://scanmosaic.com/api/v1/qr?item=k3Xp9Qa&size=1024", {
  headers: { Authorization: `Bearer ${process.env.TESSERA_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error.message);
await writeFile("menu.png", Buffer.from(await res.arrayBuffer()));

Python

import os, requests

r = requests.get(
    "https://scanmosaic.com/api/v1/qr?item=k3Xp9Qa&size=1024",
    headers={"Authorization": f"Bearer {os.environ['TESSERA_API_KEY']}"},
)
r.raise_for_status()
open("menu.png", "wb").write(r.content)

身分验证

每个请求都要在 Authorization 标头中附上你的密钥。密钥以 tsk_ 开头并属於单一工作区:可读取和更改该工作区编辑者能操作的所有内容,但无法访问其他工作区。

Authorization: Bearer tsk_AbC123…
  • 拥有者和管理员可在控制台的「API 密钥」中创建和删除密钥。删除密钥后会立即停用。
  • API 是 Business 方案的功能。方案结束后,请求会收到 402 business_required,直到你续订为止;你的 QR Code 和数据都会保留。
  • 使用密钥所做的变更,会以「API key tsk_…」的名义记录在工作区的活动纪录中。
  • CORS 已开放,可从任何来源调用 API,但放在网页中的密钥,任何打开网页的人都能拷贝。请从你的服务器调用 API。

错误

所有错误都使用相同的 JSON 格式和对应的 HTTP 状态码。code 是固定的,可以据此开发。message 是给记录和人看的英文说明。有些错误会附加细节,例如 param。

{
  "error": {
    "code": "bad_param",
    "message": "One of the parameters isn't valid.",
    "status": 400,
    "param": "size",
    "hint": "64 to 2048"
  }
}
错误代码
状态验证码意义
400bad_json内文不是有效的 JSON。
400bad_param查找或内文参数超出范围。details:param、hint。
400bad_destinationdest 不是完整的 http(s) 网址。
400bad_kindkind 不是 qr、link 或 barcode。
400bad_aliasalias 不是 3–40 个字母、数字或连字号,或是保留字。
400domain_not_verifieddomain 不是你工作区中已验证的域名。
400unsafe_destinationGoogle Web Risk 将目的地标示为有风险。
400empty / too_long / bad_formatQR Code 图片:没有数据、数据过多,或格式不是 png 或 svg。
400bad_cursor这个 cursor 不是我们提供的。
401api_key_required缺少 Authorization 标头,或格式不是 Bearer tsk_…
401invalid_api_key密钥不存在或已被删除。
402business_required这个工作区不在 Business 方案中(例如方案已结束)。
403limit_reached已达到你方案中该类项目的上限。details:limit、kind。
404not_found这个工作区中没有此项目。
409alias_taken这个别名在该域名上已被使用。
413too_big内文太大(项目上限为 250 KB)。
429rate_limited / daily_limit请求太多。请等待 Retry-After 秒数。
500server_error我们这边发生错误。请以退避方式重试。

速率限制

每把密钥每分钟可发出 120 个请求,每天 20,000 个。每个回应都会告诉你目前的用量:

标头意义
X-RateLimit-Limit每分钟允许的请求数。
X-RateLimit-Remaining本分钟剩余的请求数。
X-RateLimit-Reset本分钟重设的 Unix 时间。
Retry-After仅限 429:重试前需等待的秒数。

每个工作区每小时最多创建 300 个项目,可保留的项目数量则依你的方案而定。需要更多?请联系我们。

分页

清单由新到旧回传,最多 limit 笔(缺省 25,最多 100)。has_more 为 true 时,把 next_cursor 当作 cursor 传入即可取得下一页。添加项目时,cursor 仍然有效。

{
  "data": [
    "\u2026"
  ],
  "has_more": true,
  "next_cursor": "MTc2NzIyNTYwMC5rM1hwOVFh"
}

项目对象

QR Code、链接和条码都属于项目。单一项目以 data 回传;清单以 data 加上分页字段回传。

项目字段
名称类型说明
idstring简短 id,也是短链接的缺省结尾。
kindqr | link | barcode项目种类。创建后无法变更。
typestring内容类型,QR Code 例如 url(缺省)、pdf、menu、vcard;条码例如 ean13、code128。
namestring你的标签,最多 80 个字符。
deststring | null扫描和点击的目的地。代管页面和条码为 null。
urlstringQR Code 所编码的短链接,例如 https://go.yourbrand.com/menu。
aliasstring | null自定义结尾(3–40 个字母、数字或连字号),在同一域名中不可重复。
domainstring | null你已验证的自定义域名,或用 null 表示使用我们的域名。
campaignstring | null将项目分组以便报表统计。
contentobject | null代管页面数据,或条码数据({ "data": "…" })。
styleobjectQR Code 的设计(颜色、点和定位角样式)。
rulesobject | null导向规则(依设备、国家、时间…)。
pausedboolean已暂停的项目会显示「已暂停」页面,而不会转址。
disabledboolean因违反条款而被我们停用。
hitsinteger累计扫描数(QR Code)或点击数(链接)。
last_hitinteger | null最后一次扫描或点击的 Unix 时间。
created / updatedintegerUnix 时间。

端点

GET /api/v1/me

账号

此密钥所属的工作区、方案、上限、用量、已验证的域名和速率限制。方便检查密钥是否可用。

cURL

curl "https://scanmosaic.com/api/v1/me" \
  -H "Authorization: Bearer $TESSERA_API_KEY"

JavaScript

const res = await fetch("https://scanmosaic.com/api/v1/me", {
  headers: { Authorization: `Bearer ${process.env.TESSERA_API_KEY}` },
});
const body = await res.json();
if (!res.ok) throw new Error(body.error.message);
console.log(body.data);

Python

import os, requests

r = requests.get(
    "https://scanmosaic.com/api/v1/me",
    headers={"Authorization": f"Bearer {os.environ['TESSERA_API_KEY']}"},
)
r.raise_for_status()
print(r.json()["data"])
回应 200 OK
{
  "data": {
    "workspace": {
      "id": "w_8fK2mQ",
      "name": "Acme"
    },
    "plan": "business",
    "key": {
      "prefix": "tsk_AbC123",
      "name": "Shopify sync"
    },
    "limits": {
      "qr": 1000,
      "link": 5000,
      "barcode": 10000,
      "domains": 5
    },
    "usage": {
      "dynamic": 12,
      "links": 40,
      "barcodes": 3,
      "storageBytes": 0,
      "workspaces": 1
    },
    "domains": [
      "go.yourbrand.com"
    ],
    "rate_limits": {
      "per_minute": 120,
      "per_day": 20000
    }
  }
}

GET /api/v1/items

列出项目

工作区中的项目,最新的在前。

参数
名称类型说明
kindqr | link | barcode只显示此种类。
typestring只显示此内容类型,例如 url 或 pdf。
campaignstring只显示此活动。
domainstring只显示此自定义域名上的项目。
pausedtrue | false只显示已暂停或使用中的项目。
qstring搜索名称、目的地和别名。
limitinteger1–100,缺省 25。
cursorstring上一页的 next_cursor。

cURL

curl "https://scanmosaic.com/api/v1/items?kind=link&campaign=spring&limit=50" \
  -H "Authorization: Bearer $TESSERA_API_KEY"

JavaScript

const res = await fetch("https://scanmosaic.com/api/v1/items?kind=link&campaign=spring&limit=50", {
  headers: { Authorization: `Bearer ${process.env.TESSERA_API_KEY}` },
});
const body = await res.json();
if (!res.ok) throw new Error(body.error.message);
console.log(body.data);

Python

import os, requests

r = requests.get(
    "https://scanmosaic.com/api/v1/items?kind=link&campaign=spring&limit=50",
    headers={"Authorization": f"Bearer {os.environ['TESSERA_API_KEY']}"},
)
r.raise_for_status()
print(r.json()["data"])
回应 200 OK
{
  "data": [
    {
      "id": "k3Xp9Qa",
      "kind": "link",
      "type": "url",
      "name": "Spring menu",
      "dest": "https://example.com/menu",
      "content": null,
      "style": {},
      "rules": null,
      "alias": "menu",
      "domain": "go.yourbrand.com",
      "campaign": "spring",
      "og": null,
      "created": 1767225600,
      "updated": 1767225600,
      "paused": false,
      "disabled": false,
      "hits": 42,
      "last_hit": 1767312000,
      "url": "https://go.yourbrand.com/menu"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

POST /api/v1/items

创建项目

创建动态 QR Code、短链接或保存的条码。会计入你的方案上限。

参数
名称类型说明
kindqr | link | barcode缺省 qr。
dest 必填string要转址到的完整 http(s) 网址。条码和代管页面不使用此字段。
namestring最多 80 个字符。
typestring缺省为 url(QR Code、链接)或 code128(条码)。
aliasstring自定义结尾,例如 menu。3–40 个字母、数字或连字号,在同一域名中不可重复。
domainstring你已验证的自定义域名之一(见 /domains)。
campaignstring最多 60 个字符。
styleobjectQR Code 设计,例如 {"fg": "#112233", "dot": "rounded"}。
contentobject代管页面或条码数据,例如条码可用 {"bcid": "ean13", "text": "4006381333931"}。
rulesobject导向规则。
pausedboolean创建时即暂停。

cURL

curl "https://scanmosaic.com/api/v1/items" \
  -H "Authorization: Bearer $TESSERA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"kind": "link", "name": "Spring menu", "dest": "https://example.com/menu", "alias": "menu", "domain": "go.yourbrand.com", "campaign": "spring"}'

JavaScript

const res = await fetch("https://scanmosaic.com/api/v1/items", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.TESSERA_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "kind": "link",
    "name": "Spring menu",
    "dest": "https://example.com/menu",
    "alias": "menu",
    "domain": "go.yourbrand.com",
    "campaign": "spring"
  })
});
const body = await res.json();
if (!res.ok) throw new Error(body.error.message);
console.log(body.data);

Python

import os, requests

r = requests.post(
    "https://scanmosaic.com/api/v1/items",
    headers={"Authorization": f"Bearer {os.environ['TESSERA_API_KEY']}"},
    json={
    "kind": "link",
    "name": "Spring menu",
    "dest": "https://example.com/menu",
    "alias": "menu",
    "domain": "go.yourbrand.com",
    "campaign": "spring"
},
)
r.raise_for_status()
print(r.json()["data"])
回应 201 Created
{
  "data": {
    "id": "k3Xp9Qa",
    "kind": "link",
    "type": "url",
    "name": "Spring menu",
    "dest": "https://example.com/menu",
    "content": null,
    "style": {},
    "rules": null,
    "alias": "menu",
    "domain": "go.yourbrand.com",
    "campaign": "spring",
    "og": null,
    "created": 1767225600,
    "updated": 1767225600,
    "paused": false,
    "disabled": false,
    "hits": 42,
    "last_hit": 1767312000,
    "url": "https://go.yourbrand.com/menu"
  }
}

GET /api/v1/items/{id}

取得项目

依 id 取得单一项目。

cURL

curl "https://scanmosaic.com/api/v1/items/k3Xp9Qa" \
  -H "Authorization: Bearer $TESSERA_API_KEY"

JavaScript

const res = await fetch("https://scanmosaic.com/api/v1/items/k3Xp9Qa", {
  headers: { Authorization: `Bearer ${process.env.TESSERA_API_KEY}` },
});
const body = await res.json();
if (!res.ok) throw new Error(body.error.message);
console.log(body.data);

Python

import os, requests

r = requests.get(
    "https://scanmosaic.com/api/v1/items/k3Xp9Qa",
    headers={"Authorization": f"Bearer {os.environ['TESSERA_API_KEY']}"},
)
r.raise_for_status()
print(r.json()["data"])
回应 200 OK
{
  "data": {
    "id": "k3Xp9Qa",
    "kind": "link",
    "type": "url",
    "name": "Spring menu",
    "dest": "https://example.com/menu",
    "content": null,
    "style": {},
    "rules": null,
    "alias": "menu",
    "domain": "go.yourbrand.com",
    "campaign": "spring",
    "og": null,
    "created": 1767225600,
    "updated": 1767225600,
    "paused": false,
    "disabled": false,
    "hits": 42,
    "last_hit": 1767312000,
    "url": "https://go.yourbrand.com/menu"
  }
}

PATCH /api/v1/items/{id}

更新项目

只发送要更改的字段。更改 dest 会在下次扫描时生效,已印出的 QR Code 仍可使用。更改 domain 或 alias 会改变短链接,已印出的 QR Code 将会失效。

参数
名称类型说明
name, dest, alias, domain, campaign, style, content, rules, paused同「创建」。kind 无法变更。发送 null 可清除 alias、domain、campaign 或 rules。

cURL

curl -X PATCH "https://scanmosaic.com/api/v1/items/k3Xp9Qa" \
  -H "Authorization: Bearer $TESSERA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"dest": "https://example.com/summer-menu", "paused": false}'

JavaScript

const res = await fetch("https://scanmosaic.com/api/v1/items/k3Xp9Qa", {
  method: "PATCH",
  headers: { Authorization: `Bearer ${process.env.TESSERA_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "dest": "https://example.com/summer-menu",
    "paused": false
  })
});
const body = await res.json();
if (!res.ok) throw new Error(body.error.message);
console.log(body.data);

Python

import os, requests

r = requests.patch(
    "https://scanmosaic.com/api/v1/items/k3Xp9Qa",
    headers={"Authorization": f"Bearer {os.environ['TESSERA_API_KEY']}"},
    json={
    "dest": "https://example.com/summer-menu",
    "paused": False
},
)
r.raise_for_status()
print(r.json()["data"])
回应 200 OK
{
  "data": {
    "id": "k3Xp9Qa",
    "kind": "link",
    "type": "url",
    "name": "Spring menu",
    "dest": "https://example.com/summer-menu",
    "content": null,
    "style": {},
    "rules": null,
    "alias": "menu",
    "domain": "go.yourbrand.com",
    "campaign": "spring",
    "og": null,
    "created": 1767225600,
    "updated": 1767225600,
    "paused": false,
    "disabled": false,
    "hits": 42,
    "last_hit": 1767312000,
    "url": "https://go.yourbrand.com/menu"
  }
}

DELETE /api/v1/items/{id}

删除项目

删除项目及其扫描纪录。它的短链接会立即失效。此动作无法复原。

cURL

curl -X DELETE "https://scanmosaic.com/api/v1/items/k3Xp9Qa" \
  -H "Authorization: Bearer $TESSERA_API_KEY"

JavaScript

const res = await fetch("https://scanmosaic.com/api/v1/items/k3Xp9Qa", {
  method: "DELETE",
  headers: { Authorization: `Bearer ${process.env.TESSERA_API_KEY}` },
});
const body = await res.json();
if (!res.ok) throw new Error(body.error.message);
console.log(body.data);

Python

import os, requests

r = requests.delete(
    "https://scanmosaic.com/api/v1/items/k3Xp9Qa",
    headers={"Authorization": f"Bearer {os.environ['TESSERA_API_KEY']}"},
)
r.raise_for_status()
print(r.json()["data"])
回应 200 OK
{
  "data": {
    "id": "k3Xp9Qa",
    "deleted": true
  }
}

GET /api/v1/items/{id}/stats

项目统计

一段期间内的扫描或点击:每日数列,以及依国家、设备、操作系统、浏览器、语言和导向规则的分析。不计入机器人。

参数
名称类型说明
daysinteger1–365,缺省 30。

cURL

curl "https://scanmosaic.com/api/v1/items/k3Xp9Qa/stats?days=7" \
  -H "Authorization: Bearer $TESSERA_API_KEY"

JavaScript

const res = await fetch("https://scanmosaic.com/api/v1/items/k3Xp9Qa/stats?days=7", {
  headers: { Authorization: `Bearer ${process.env.TESSERA_API_KEY}` },
});
const body = await res.json();
if (!res.ok) throw new Error(body.error.message);
console.log(body.data);

Python

import os, requests

r = requests.get(
    "https://scanmosaic.com/api/v1/items/k3Xp9Qa/stats?days=7",
    headers={"Authorization": f"Bearer {os.environ['TESSERA_API_KEY']}"},
)
r.raise_for_status()
print(r.json()["data"])
回应 200 OK
{
  "data": {
    "days": 7,
    "total": 42,
    "last_hit": 1767312000,
    "inRange": 12,
    "series": [
      [
        "2026-01-01",
        3
      ],
      [
        "2026-01-02",
        0
      ],
      [
        "…",
        0
      ]
    ],
    "countries": [
      {
        "k": "GB",
        "n": 7
      },
      {
        "k": "US",
        "n": 5
      }
    ],
    "devices": [
      {
        "k": "Mobile",
        "n": 10
      },
      {
        "k": "Desktop",
        "n": 2
      }
    ],
    "os": [
      {
        "k": "iOS",
        "n": 6
      }
    ],
    "browsers": [
      {
        "k": "Safari",
        "n": 6
      }
    ],
    "langs": [
      {
        "k": "en-gb",
        "n": 7
      }
    ],
    "rules": [
      {
        "k": "default",
        "n": 12
      }
    ]
  }
}

GET /api/v1/qr

QR Code 图片

将 QR Code 产生为 PNG 或 SVG。可用 GET 加查找参数,或以 JSON POST 相同的参数。传入 item 可依保存的设计和短链接绘制已保存的 QR Code,或传入 data 编码任何文本。

参数
名称类型说明
datastring要编码的文本或链接(最多 2,953 字节)。未设置 item 时为必填。
itemstring已保存的 QR Code 或链接的 id:以保存的设计编码它的短链接。
formatpng | svg缺省 png。
sizeinteger像素,PNG 为 64–2048(缺省 512),SVG 为 64–4096(缺省 1024)。
color#RRGGBB点的颜色。缺省 #141A2E。
background#RRGGBB缺省 #FFFFFF。
transparentboolean透明背景。
corner_color#RRGGBB定位角方块。缺省:color。
gradient_color#RRGGBB仅限 SVG:对角渐层的第二个颜色。
error_correctionL | M | Q | HQR Code 可承受的损坏程度:7%、15%、25%、30%。缺省 M(PNG)或 Q(SVG)。
margininteger静区的模块数,0–8。缺省 4。
dot_stylestringsquare, dots, rounded, extra-rounded, classy, classy-rounded
corner_stylestringsquare, extra-rounded, dot
corner_dot_stylestringsquare, dot
downloadstring文件名称:加上 Content-Disposition: attachment。

PNG 由我们的服务器绘制,每个模块都对齐整数像素,因此边缘在印刷时依然清晰。渐层和 Logo 仅支持 SVG;若要加上 Logo,请使用生成器。

cURL

curl "https://scanmosaic.com/api/v1/qr" \
  -H "Authorization: Bearer $TESSERA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"data": "https://example.com", "format": "png", "size": 1024, "color": "#4865F0", "dot_style": "rounded", "corner_style": "extra-rounded"}' \
  -o qr.png

JavaScript

import { writeFile } from "node:fs/promises";

const res = await fetch("https://scanmosaic.com/api/v1/qr", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.TESSERA_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "data": "https://example.com",
    "format": "png",
    "size": 1024,
    "color": "#4865F0",
    "dot_style": "rounded",
    "corner_style": "extra-rounded"
  })
});
if (!res.ok) throw new Error((await res.json()).error.message);
await writeFile("qr.png", Buffer.from(await res.arrayBuffer()));

Python

import os, requests

r = requests.post(
    "https://scanmosaic.com/api/v1/qr",
    headers={"Authorization": f"Bearer {os.environ['TESSERA_API_KEY']}"},
    json={
    "data": "https://example.com",
    "format": "png",
    "size": 1024,
    "color": "#4865F0",
    "dot_style": "rounded",
    "corner_style": "extra-rounded"
},
)
r.raise_for_status()
open("qr.png", "wb").write(r.content)

GET /api/v1/domains

域名

工作区的自定义域名。创建项目时,请使用 ready 为 true 的主机作为 domain。

cURL

curl "https://scanmosaic.com/api/v1/domains" \
  -H "Authorization: Bearer $TESSERA_API_KEY"

JavaScript

const res = await fetch("https://scanmosaic.com/api/v1/domains", {
  headers: { Authorization: `Bearer ${process.env.TESSERA_API_KEY}` },
});
const body = await res.json();
if (!res.ok) throw new Error(body.error.message);
console.log(body.data);

Python

import os, requests

r = requests.get(
    "https://scanmosaic.com/api/v1/domains",
    headers={"Authorization": f"Bearer {os.environ['TESSERA_API_KEY']}"},
)
r.raise_for_status()
print(r.json()["data"])
回应 200 OK
{
  "data": [
    {
      "host": "go.yourbrand.com",
      "verified": true,
      "ready": true
    }
  ]
}

自定义域名

在 Business 方案中,你的短链接可以使用自己的域名,例如 go.yourbrand.com/menu。请在控制台的「域名」中添加并验证域名:添加一笔指向我们的 CNAME 纪录,以及一笔证明域名属于你的 TXT 纪录,我们就会核发 SSL 凭证。之后在创建或更新项目时传入 domain 即可。

每个域名都可以把根网址(go.yourbrand.com)和不存在的路径转址到你指定的页面。

OpenAPI

完整的 API 说明请见 openapi.json(OpenAPI 3.1)。可导入 Postman、Insomnia 或用户端生成器。

版本管理

v1 是稳定版。我们可能会添加端点、字段和错误代码,所以请忽略你不认得的字段。我们不会移除或重命名 v1 中的任何内容;破坏性变更会以 v2 推出,并会提早公告。