Início rápido
- Abra Chaves de API no seu dashboard (plano Business) e crie uma chave. Copie: ela aparece só uma vez.
- Guarde-a como variável de ambiente no seu servidor, por exemplo TESSERA_API_KEY. Nunca a coloque em código de navegador ou de app.
- Crie um QR code dinâmico. A resposta traz o link curto em url; codifique esse link ou baixe uma imagem pronta em /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.pngJavaScript
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)Autenticação
Envie sua chave no cabeçalho Authorization de toda requisição. As chaves começam com tsk_ e pertencem a um workspace: elas podem ler e alterar tudo o que um editor desse workspace pode, e nada em outros workspaces.
Authorization: Bearer tsk_AbC123…
- Proprietários e admins criam e excluem chaves em Chaves de API, no dashboard. Excluir uma chave a desativa na hora.
- A API faz parte do plano Business. Se o plano terminar, as requisições recebem 402 business_required até você renovar; seus códigos e dados são mantidos.
- As alterações feitas com uma chave aparecem no registro de atividades do workspace como "API key tsk_…".
- O CORS está aberto para você chamar a API de qualquer origem, mas uma chave em uma página web pode ser copiada por quem a abrir. Chame a API pelo seu servidor.
Erros
Todo erro usa o mesmo formato JSON e um status HTTP correspondente. code é estável: baseie-se nele. message é texto simples em inglês para logs e pessoas. Alguns erros acrescentam detalhes, como param.
{
"error": {
"code": "bad_param",
"message": "One of the parameters isn't valid.",
"status": 400,
"param": "size",
"hint": "64 to 2048"
}
}
| Status | Código | Significado |
|---|---|---|
| 400 | bad_json | O corpo da requisição não é um JSON válido. |
| 400 | bad_param | Um parâmetro de consulta ou de corpo está fora do intervalo. details: param, hint. |
| 400 | bad_destination | dest não é um endereço web http(s) completo. |
| 400 | bad_kind | kind não é qr, link nem barcode. |
| 400 | bad_alias | o alias não tem de 3 a 40 letras, números ou hifens, ou é reservado. |
| 400 | domain_not_verified | domain não é um domínio verificado do seu workspace. |
| 400 | unsafe_destination | O Google Web Risk sinaliza o destino. |
| 400 | empty / too_long / bad_format | Imagens de QR: sem dados, dados demais ou um formato diferente de png ou svg. |
| 400 | bad_cursor | Esse cursor não foi fornecido por nós. |
| 401 | api_key_required | Sem cabeçalho Authorization, ou ele não é Bearer tsk_… |
| 401 | invalid_api_key | A chave não existe ou foi excluída. |
| 402 | business_required | O workspace não está no plano Business (por exemplo, o plano terminou). |
| 403 | limit_reached | O limite do seu plano para esse tipo de item foi atingido. details: limit, kind. |
| 404 | not_found | Esse item não existe neste workspace. |
| 409 | alias_taken | Esse alias já está em uso nesse domínio. |
| 413 | too_big | O corpo da requisição é grande demais (250 KB para itens). |
| 429 | rate_limited / daily_limit | Muitas requisições. Aguarde os segundos indicados em Retry-After. |
| 500 | server_error | Algo falhou do nosso lado. Tente novamente com backoff. |
Limites de requisições
Cada chave pode fazer 120 requisições por minuto e 20,000 por dia. Toda resposta informa em que ponto você está:
| Cabeçalho | Significado |
|---|---|
X-RateLimit-Limit | Requisições permitidas por minuto. |
X-RateLimit-Remaining | Requisições restantes no minuto atual. |
X-RateLimit-Reset | Horário Unix em que o minuto reinicia. |
Retry-After | Só no 429: segundos de espera antes de tentar de novo. |
A criação de itens também é limitada a 300 por hora por workspace, e seu plano define quantos itens você pode manter. Precisa de mais? Fale conosco.
Paginação
As listas retornam os mais recentes primeiro, até limit itens (padrão 25, no máximo 100). Quando has_more for true, envie next_cursor como cursor para obter a próxima página. Os cursores continuam válidos quando itens são adicionados.
{
"data": [
"\u2026"
],
"has_more": true,
"next_cursor": "MTc2NzIyNTYwMC5rM1hwOVFh"
}
O objeto item
QR codes, links e barcodes são todos itens. Itens individuais voltam como data; listas voltam como data com campos de paginação.
| Nome | Tipo | Descrição |
|---|---|---|
id | string | ID curto, também o final padrão do link curto. |
kind | qr | link | barcode | O que é. Não pode ser alterado depois de criado. |
type | string | Tipo de conteúdo, por exemplo url (padrão), pdf, menu, vcard para QR codes; ean13, code128 para barcodes. |
name | string | Seu rótulo, com até 80 caracteres. |
dest | string | null | Para onde vão as leituras e os cliques. null para páginas hospedadas e códigos de barras. |
url | string | O link curto que o QR code codifica, por exemplo https://go.yourbrand.com/menu. |
alias | string | null | Final personalizado (3 a 40 letras, números ou hifens), único por domínio. |
domain | string | null | Seu domínio personalizado verificado ou null para o nosso. |
campaign | string | null | Agrupe itens para gerar relatórios. |
content | object | null | Dados da página hospedada ou do barcode ({ "data": "…" }). |
style | object | Design do QR code (cores, estilos de pontos e cantos). |
rules | object | null | Regras de roteamento (por dispositivo, país, horário…). |
paused | boolean | Itens pausados mostram uma página 'pausado' em vez de redirecionar. |
disabled | boolean | Desativado por nós por violar os termos. |
hits | integer | Scans de todo o período (QR codes) ou cliques (links). |
last_hit | integer | null | Horário Unix da última leitura ou clique. |
created / updated | integer | Horários Unix. |
Endpoints
GET /api/v1/me
Conta
O workspace a que esta chave pertence, seu plano, limites, uso, domínios verificados e limites de requisições. Útil para conferir se uma chave funciona.
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"])Resposta 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
Listar itens
Itens do workspace, os mais recentes primeiro.
| Nome | Tipo | Descrição |
|---|---|---|
kind | qr | link | barcode | Apenas este tipo. |
type | string | Apenas este tipo de conteúdo, por exemplo url ou pdf. |
campaign | string | Apenas esta campanha. |
domain | string | Apenas itens neste domínio personalizado. |
paused | true | false | Apenas itens pausados ou apenas itens ativos. |
q | string | Busque por nome, destino e apelido. |
limit | integer | 1–100, padrão 25. |
cursor | string | next_cursor da página anterior. |
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"])Resposta 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
Criar um item
Cria um QR code dinâmico, link curto ou código de barras salvo. Conta para os limites do seu plano.
| Nome | Tipo | Descrição |
|---|---|---|
kind | qr | link | barcode | Padrão qr. |
dest obrigatório | string | Endereço http(s) completo para redirecionar. Não é usado em códigos de barras e páginas hospedadas. |
name | string | Até 80 caracteres. |
type | string | Padrão url (códigos, links) ou code128 (barcodes). |
alias | string | Final personalizado, por exemplo menu. De 3 a 40 letras, números ou hifens, único por domínio. |
domain | string | Um dos seus domínios personalizados verificados (veja /domains). |
campaign | string | Até 60 caracteres. |
style | object | Design do QR, por exemplo {"fg": "#112233", "dot": "rounded"}. |
content | object | Dados da página hospedada ou do barcode, por exemplo {"bcid": "ean13", "text": "4006381333931"} para um barcode. |
rules | object | Regras de roteamento. |
paused | boolean | Começar pausado. |
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"])Resposta 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}
Obter um item
Um item por 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"])Resposta 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}
Atualizar um item
Envie apenas os campos que vão mudar. Alterar dest vale a partir da próxima leitura; os códigos impressos continuam funcionando. Alterar domain ou alias muda o link curto, e os códigos já impressos deixam de funcionar.
| Nome | Tipo | Descrição |
|---|---|---|
name, dest, alias, domain, campaign, style, content, rules, paused | Como em Create. kind não pode ser alterado. Envie null para limpar alias, domain, campaign ou 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"])Resposta 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}
Excluir um item
Exclui o item e seu histórico de scans. O link curto dele para de funcionar na hora. Isso não pode ser desfeito.
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"])Resposta 200 OK
{
"data": {
"id": "k3Xp9Qa",
"deleted": true
}
}GET /api/v1/items/{id}/stats
Estatísticas do item
Leituras ou cliques em um período: uma série diária e detalhamentos por país, dispositivo, sistema operacional, navegador, idioma e regra de roteamento. Bots não são contados.
| Nome | Tipo | Descrição |
|---|---|---|
days | integer | 1–365, padrão 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"])Resposta 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
Imagem do QR code
Gera um QR code em PNG ou SVG. Use GET com parâmetros de consulta ou POST com os mesmos parâmetros em JSON. Passe item para desenhar um código salvo com o próprio design e link curto, ou data para qualquer texto.
| Nome | Tipo | Descrição |
|---|---|---|
data | string | Texto ou link a codificar (até 2.953 bytes). Obrigatório, a menos que item esteja definido. |
item | string | ID de um QR code ou link salvo: codifica o link curto com o design salvo. |
format | png | svg | Padrão png. |
size | integer | Pixels, de 64 a 2048 para PNG (padrão 512), de 64 a 4096 para SVG (padrão 1024). |
color | #RRGGBB | Pontos. Padrão #141A2E. |
background | #RRGGBB | Padrão #FFFFFF. |
transparent | boolean | Fundo transparente. |
corner_color | #RRGGBB | Quadrados dos cantos. Padrão: cor. |
gradient_color | #RRGGBB | Somente SVG: segunda cor de um degradê diagonal. |
error_correction | L | M | Q | H | Quanto dano o código suporta: 7%, 15%, 25%, 30%. Padrão M (PNG) ou Q (SVG). |
margin | integer | Zona de silêncio em módulos, de 0 a 8. Padrão 4. |
dot_style | string | square, dots, rounded, extra-rounded, classy, classy-rounded |
corner_style | string | square, extra-rounded, dot |
corner_dot_style | string | square, dot |
download | string | Nome do arquivo: adiciona Content-Disposition: attachment. |
Os PNGs são desenhados nos nossos servidores com pixels inteiros por módulo, então as bordas ficam nítidas para impressão. Degradês e logos só existem em SVG; para logos, use o gerador.
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.pngJavaScript
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
Domínios
Os domínios personalizados do workspace. Use um host cujo ready seja true como domínio ao criar itens.
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"])Resposta 200 OK
{
"data": [
{
"host": "go.yourbrand.com",
"verified": true,
"ready": true
}
]
}Domínios personalizados
No plano Business, seus links curtos podem usar o seu próprio domínio, como go.suamarca.com/menu. Adicione e verifique o domínio em Domínios, no dashboard: você cria um registro CNAME que aponta para nós e um registro TXT que comprova que o domínio é seu, e nós emitimos o certificado SSL. Depois, informe domain ao criar ou atualizar um item.
Cada domínio pode redirecionar o endereço raiz (go.yourbrand.com) e caminhos desconhecidos para as páginas que você escolher.
OpenAPI
A API completa está descrita em openapi.json (OpenAPI 3.1). Importe no Postman, no Insomnia ou em um gerador de clientes.
Versões
A v1 é estável. Podemos adicionar endpoints, campos e códigos de erro, então ignore campos que você não conhece. Não removeremos nem renomearemos nada na v1; mudanças incompatíveis virão na v2, anunciadas com bastante antecedência.