Documentação
API de SMS para o Brasil.
Uma API HTTP em https://api.smskit.dev, SDKs para TypeScript e Python e um servidor MCP para agentes. Números brasileiros (+55), preços em reais por segmento, respostas em JSON e erros com códigos estáveis.
Três passos até o primeiro envio
Crie uma chave no console
Em Configurações → Chaves de API, gere uma chave
sk_test_…para ensaiar ousk_live_…para produção. A chave aparece uma única vez; guarde-a como segredo.terminal export SMSKIT_KEY="sk_test_…"Troque a chave por um token Bearer
Envie a chave no header
X-API-KeyparaPOST /v1/auth/token. O token vale 48 horas e é a única credencial usada nas demais rotas. Os SDKs fazem esta troca sozinhos, guardam o token em memória e renovam quando ele vence.cURL curl -X POST https://api.smskit.dev/v1/auth/token \ -H "X-API-Key: $SMSKIT_KEY"Envie o SMS
POST /v1/smsaceita um ou vários destinatários e responde202com o envio na fila. O resultado por destinatário chega por webhook ou emGET /v1/sms/{id}.cURL curl -X POST https://api.smskit.dev/v1/sms \ -H "Authorization: Bearer $SMSKIT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "to": "+5511999990123", "text": "Sua mesa está pronta. Até já." }'TypeScript · SDK import { SMSKit } from "smskit"; const sms = new SMSKit(process.env.SMSKIT_KEY!); await sms.send({ to: "+5511999990123", text: "Sua mesa está pronta. Até já.", });Python · SDK import os from smskit import SMSKit sms = SMSKit(os.environ["SMSKIT_KEY"]) sms.send(to="+5511999990123", text="Sua mesa está pronta. Até já.")
Autenticação em dois passos
A chave de API nunca trafega em cada requisição. Ela é trocada uma vez por um token Bearer em POST /v1/auth/token; só o token vai no header Authorization das demais rotas.
- Validade. Cada token expira em 48 horas (
expires_inem segundos,expires_atem ISO 8601). Ao receber401 token_expired, gere outro. - Revogação. Revogar a chave no console invalida todos os tokens emitidos a partir dela na hora.
- Ambiente. O token herda o modo da chave:
sk_live_gera tokenslive;sk_test_gera tokenstest. - Limites. Até 20 chaves por workspace. Use uma chave por integração para revogar sem afetar as outras.
{
"access_token": "smt_…",
"expires_at": "2026-09-13T14:03:00Z",
"expires_in": 172800,
"mode": "live",
"workspace_id": "ws_3h7k9d",
"token_type": "bearer"
}
curl https://api.smskit.dev/v1/account \
-H "Authorization: Bearer $SMSKIT_TOKEN"
Modo de teste
Chaves sk_test_ percorrem exatamente os mesmos endpoints, com as mesmas respostas. A diferença está no que acontece depois:
- Nada é cobrado e nenhuma mensagem sai para a operadora.
- A entrega é simulada: números terminados em
0000falham; todos os outros são entregues. - Os objetos retornam
"test": true, e os webhooks disparam normalmente, também com"test": trueno envelope. - Modelos, saldo e webhooks são os do seu workspace; só o envio é simulado.
{
"id": "sms_7f3k2m",
"object": "sms",
"status": "queued",
"test": true,
"recipients": 1,
"segments": 1,
"credits": 1
}
Referência da API
Gerada a partir do OpenAPI 3.1.0 da versão 1.0.0. Base: https://api.smskit.dev. Todas as rotas, exceto POST /v1/auth/token, exigem Authorization: Bearer <token>. Ids nos exemplos como sms_7f3k2m são ilustrativos.
Auth
POST/v1/auth/token
Trocar a chave de API por um token Bearer de 48h
Envie a chave em X-API-Key (ou no corpo como api_key). O token retornado vale 48 horas e é o único credencial usado nas demais rotas; a chave nunca trafega em cada requisição. Chaves sk_test_ geram tokens de modo de teste: mesmas rotas, respostas idênticas, nada é cobrado nem enviado.
| Nome | Onde | Tipo |
|---|---|---|
x-api-key | header | string |
| Campo | Tipo | Descrição |
|---|---|---|
api_key | string | null |
| Status | Descrição |
|---|---|
200 | Objeto TokenResponseaccess_token, expires_at, expires_in, mode, token_type, workspace_id |
401 | Credencial ausente, inválida ou expirada |
422 | validation_error |
429 | rate_limited — respeite Retry-After |
curl -X POST https://api.smskit.dev/v1/auth/token \
-H "X-API-Key: $SMSKIT_KEY"
const response = await fetch("https://api.smskit.dev/v1/auth/token", {
method: "POST",
headers: {
"X-API-Key": process.env.SMSKIT_KEY,
},
});
const data = await response.json();
import httpx
response = httpx.post(
"https://api.smskit.dev/v1/auth/token",
headers={"X-API-Key": os.environ["SMSKIT_KEY"]},
)
data = response.json()
{
"access_token": "smt_…",
"expires_at": "2026-09-13T14:03:00Z",
"expires_in": 172800,
"mode": "live",
"workspace_id": "ws_3h7k9d",
"token_type": "bearer"
}
SMS
POST/v1/sms
Enviar SMS
Aceita um ou vários destinatários (to), com text livre ou template + variables. Todo envio passa por análise de risco (spam, golpe, phishing, links): risco baixo entra na fila (queued); risco médio ou alto fica em revisão manual (review) com os créditos reservados até a decisão. Envios em produção exigem o cadastro do titular concluído no console (identity_required). Responde 202; a entrega chega por webhook ou por GET /v1/sms/{id}. Envie Idempotency-Key para repetir com segurança.
| Nome | Onde | Tipo |
|---|---|---|
idempotency-key | header | string |
| Campo | Tipo | Descrição |
|---|---|---|
from | string | null | até 40 caracteres |
kind | authentication | utility | null | Which delivery route a message takes.valores: authentication, utility |
metadata | object<string> | null | |
scheduled_at | date-time | null | |
template | string | null | até 100 caracteres |
text | string | null | até 1000 caracteres |
toobrigatório | string | string[] | |
variables | object<string> | null |
| Status | Descrição |
|---|---|
202 | Objeto SmsOutcompleted_at, created_at, credits, from, id, kind, messages, metadata, object, recipients, scheduled_at, segments, status, template, test, text |
401 | Credencial ausente, inválida ou expirada |
402 | insufficient_balance |
403 | template_not_approved ou identity_required |
409 | conflict (Idempotency-Key reutilizada com outro corpo) |
422 | validation_error |
429 | rate_limited — respeite Retry-After |
curl -X POST https://api.smskit.dev/v1/sms \
-H "Authorization: Bearer $SMSKIT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": "+5511999990123",
"text": "Sua mesa está pronta. Até já."
}'
const response = await fetch("https://api.smskit.dev/v1/sms", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
to: "+5511999990123",
text: "Sua mesa está pronta. Até já.",
}),
});
const data = await response.json();
import httpx
response = httpx.post(
"https://api.smskit.dev/v1/sms",
headers={"Authorization": f"Bearer {token}"},
json={
"to": "+5511999990123",
"text": "Sua mesa está pronta. Até já.",
},
)
data = response.json()
{
"id": "sms_7f3k2m",
"object": "sms",
"text": "Sua mesa está pronta. Até já.",
"template": "order-confirmed",
"status": "queued",
"created_at": "2026-09-11T14:03:00Z",
"credits": 1,
"kind": "utility",
"recipients": 1,
"segments": 1,
"test": false,
"completed_at": null,
"from": null,
"messages": [
{
"id": "msg_2b9x4c",
"object": "message",
"to": "+5511999990123",
"status": "queued",
"updated_at": "2026-09-11T14:03:05Z",
"error": null
}
],
"metadata": {
"order_id": "1042"
},
"scheduled_at": null
}
GET/v1/sms
Listar envios
| Nome | Onde | Tipo |
|---|---|---|
limit | query | integerpadrão: 20 |
starting_after | query | string |
| Status | Descrição |
|---|---|
200 | Objeto SmsListdata, has_more, next_cursor, object |
401 | Credencial ausente, inválida ou expirada |
422 | validation_error |
429 | rate_limited — respeite Retry-After |
curl https://api.smskit.dev/v1/sms?limit=20 \
-H "Authorization: Bearer $SMSKIT_TOKEN"
const response = await fetch("https://api.smskit.dev/v1/sms?limit=20", {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await response.json();
import httpx
response = httpx.get(
"https://api.smskit.dev/v1/sms?limit=20",
headers={"Authorization": f"Bearer {token}"},
)
data = response.json()
{
"object": "list",
"data": [
{
"id": "sms_7f3k2m",
"object": "sms",
"text": "Sua mesa está pronta. Até já.",
"template": "order-confirmed",
"status": "queued",
"created_at": "2026-09-11T14:03:00Z",
"credits": 1,
"kind": "utility",
"recipients": 1,
"segments": 1,
"test": false,
"completed_at": null,
"from": null,
"messages": [
{
"id": "msg_2b9x4c",
"object": "message",
"to": "+5511999990123",
"status": "queued",
"updated_at": "2026-09-11T14:03:05Z",
"error": null
}
],
"metadata": {
"order_id": "1042"
},
"scheduled_at": null
}
],
"has_more": false,
"next_cursor": null
}
GET/v1/sms/{sms_id}
Consultar um envio e seus destinatários
| Nome | Onde | Tipo |
|---|---|---|
sms_idobrigatório | path | string |
| Status | Descrição |
|---|---|
200 | Objeto SmsOutcompleted_at, created_at, credits, from, id, kind, messages, metadata, object, recipients, scheduled_at, segments, status, template, test, text |
401 | Credencial ausente, inválida ou expirada |
404 | not_found |
422 | validation_error |
429 | rate_limited — respeite Retry-After |
curl https://api.smskit.dev/v1/sms/sms_7f3k2m \
-H "Authorization: Bearer $SMSKIT_TOKEN"
const response = await fetch("https://api.smskit.dev/v1/sms/sms_7f3k2m", {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await response.json();
import httpx
response = httpx.get(
"https://api.smskit.dev/v1/sms/sms_7f3k2m",
headers={"Authorization": f"Bearer {token}"},
)
data = response.json()
{
"id": "sms_7f3k2m",
"object": "sms",
"text": "Sua mesa está pronta. Até já.",
"template": "order-confirmed",
"status": "queued",
"created_at": "2026-09-11T14:03:00Z",
"credits": 1,
"kind": "utility",
"recipients": 1,
"segments": 1,
"test": false,
"completed_at": null,
"from": null,
"messages": [
{
"id": "msg_2b9x4c",
"object": "message",
"to": "+5511999990123",
"status": "queued",
"updated_at": "2026-09-11T14:03:05Z",
"error": null
}
],
"metadata": {
"order_id": "1042"
},
"scheduled_at": null
}
Conta
GET/v1/account
Conta, saldo e preços
| Status | Descrição |
|---|---|
200 | Objeto AccountOutbalance, id, mode, name, object, pricing |
401 | Credencial ausente, inválida ou expirada |
422 | validation_error |
429 | rate_limited — respeite Retry-After |
curl https://api.smskit.dev/v1/account \
-H "Authorization: Bearer $SMSKIT_TOKEN"
const response = await fetch("https://api.smskit.dev/v1/account", {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await response.json();
import httpx
response = httpx.get(
"https://api.smskit.dev/v1/account",
headers={"Authorization": f"Bearer {token}"},
)
data = response.json()
{
"id": "ws_3h7k9d",
"object": "account",
"balance": {
"credits": 1250
},
"mode": "live",
"name": "Studio",
"pricing": {
"currency": "BRL",
"creditsPerSegment": 1
}
}
Modelos
GET/v1/templates
Modelos do workspace e sua aprovação
| Status | Descrição |
|---|---|
200 | Objeto TemplateListdata, object |
401 | Credencial ausente, inválida ou expirada |
422 | validation_error |
429 | rate_limited — respeite Retry-After |
curl https://api.smskit.dev/v1/templates \
-H "Authorization: Bearer $SMSKIT_TOKEN"
const response = await fetch("https://api.smskit.dev/v1/templates", {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await response.json();
import httpx
response = httpx.get(
"https://api.smskit.dev/v1/templates",
headers={"Authorization": f"Bearer {token}"},
)
data = response.json()
{
"object": "list",
"data": [
{
"id": "order-confirmed",
"object": "template",
"variables": {
"empresa": "Studio",
"pedido": "1042"
},
"approved": true,
"body": "{{empresa}}: seu pedido {{pedido}} foi confirmado. Acompanhe: {{link}}",
"category": "transactional",
"kind": "utility",
"name": "Studio"
}
]
}
Webhooks
POST/v1/webhooks
Criar webhook
O secret é exibido apenas nesta resposta. Use-o para verificar X-SMSKit-Signature.
| Campo | Tipo | Descrição |
|---|---|---|
description | string | até 200 caracteres |
eventsobrigatório | WebhookEventType[] | valores: message.sent, message.delivered, message.failed, sms.completed, webhook.test |
urlobrigatório | string | até 500 caracteres |
| Status | Descrição |
|---|---|
201 | Objeto WebhookOutactive, created_at, description, events, id, object, secret, url |
401 | Credencial ausente, inválida ou expirada |
422 | validation_error |
429 | rate_limited — respeite Retry-After |
curl -X POST https://api.smskit.dev/v1/webhooks \
-H "Authorization: Bearer $SMSKIT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/smskit",
"events": [
"message.delivered",
"sms.completed"
]
}'
const response = await fetch("https://api.smskit.dev/v1/webhooks", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://example.com/webhooks/smskit",
events: ["message.delivered", "sms.completed"],
}),
});
const data = await response.json();
import httpx
response = httpx.post(
"https://api.smskit.dev/v1/webhooks",
headers={"Authorization": f"Bearer {token}"},
json={
"url": "https://example.com/webhooks/smskit",
"events": [
"message.delivered",
"sms.completed",
],
},
)
data = response.json()
{
"id": "wh_4d8q1z",
"object": "webhook",
"url": "https://example.com/webhooks/smskit",
"events": [
"message.delivered",
"sms.completed"
],
"active": true,
"created_at": "2026-09-11T14:03:00Z",
"description": "Entregas em produção",
"secret": "whsec_6k2p9v4d1z8m"
}
GET/v1/webhooks
Listar webhooks
| Status | Descrição |
|---|---|
200 | Objeto WebhookListdata, object |
401 | Credencial ausente, inválida ou expirada |
422 | validation_error |
429 | rate_limited — respeite Retry-After |
curl https://api.smskit.dev/v1/webhooks \
-H "Authorization: Bearer $SMSKIT_TOKEN"
const response = await fetch("https://api.smskit.dev/v1/webhooks", {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await response.json();
import httpx
response = httpx.get(
"https://api.smskit.dev/v1/webhooks",
headers={"Authorization": f"Bearer {token}"},
)
data = response.json()
{
"object": "list",
"data": [
{
"id": "wh_4d8q1z",
"object": "webhook",
"url": "https://example.com/webhooks/smskit",
"events": [
"message.delivered",
"sms.completed"
],
"active": true,
"created_at": "2026-09-11T14:03:00Z",
"description": "Entregas em produção",
"secret": "whsec_6k2p9v4d1z8m"
}
]
}
DELETE/v1/webhooks/{webhook_id}
Remover webhook
| Nome | Onde | Tipo |
|---|---|---|
webhook_idobrigatório | path | string |
| Status | Descrição |
|---|---|
204 | Sucesso |
401 | Credencial ausente, inválida ou expirada |
404 | not_found |
422 | validation_error |
429 | rate_limited — respeite Retry-After |
curl -X DELETE https://api.smskit.dev/v1/webhooks/wh_4d8q1z \
-H "Authorization: Bearer $SMSKIT_TOKEN"
const response = await fetch("https://api.smskit.dev/v1/webhooks/wh_4d8q1z", {
method: "DELETE",
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
import httpx
response = httpx.delete(
"https://api.smskit.dev/v1/webhooks/wh_4d8q1z",
headers={"Authorization": f"Bearer {token}"},
)
response.raise_for_status()
GET/v1/webhooks/{webhook_id}/deliveries
Entregas recentes de um webhook
| Nome | Onde | Tipo |
|---|---|---|
webhook_idobrigatório | path | string |
| Status | Descrição |
|---|---|
200 | Objeto WebhookDeliveryListdata, object |
401 | Credencial ausente, inválida ou expirada |
422 | validation_error |
429 | rate_limited — respeite Retry-After |
curl https://api.smskit.dev/v1/webhooks/wh_4d8q1z/deliveries \
-H "Authorization: Bearer $SMSKIT_TOKEN"
const response = await fetch("https://api.smskit.dev/v1/webhooks/wh_4d8q1z/deliveries", {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await response.json();
import httpx
response = httpx.get(
"https://api.smskit.dev/v1/webhooks/wh_4d8q1z/deliveries",
headers={"Authorization": f"Bearer {token}"},
)
data = response.json()
{
"object": "list",
"data": [
{
"id": "whd_9k2p7r",
"object": "webhook_delivery",
"status": "pending",
"attempts": 1,
"created_at": "2026-09-11T14:03:00Z",
"event_id": "evt_8n3v5t",
"event_type": "message.delivered",
"delivered_at": "2026-09-11T14:03:05Z",
"last_error": null,
"last_status_code": 200,
"next_attempt_at": null,
"test": false
}
]
}
POST/v1/webhooks/{webhook_id}/test
Disparar um evento webhook.test
| Nome | Onde | Tipo |
|---|---|---|
webhook_idobrigatório | path | string |
| Status | Descrição |
|---|---|
202 | Sucesso |
401 | Credencial ausente, inválida ou expirada |
422 | validation_error |
429 | rate_limited — respeite Retry-After |
curl -X POST https://api.smskit.dev/v1/webhooks/wh_4d8q1z/test \
-H "Authorization: Bearer $SMSKIT_TOKEN"
const response = await fetch("https://api.smskit.dev/v1/webhooks/wh_4d8q1z/test", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await response.json();
import httpx
response = httpx.post(
"https://api.smskit.dev/v1/webhooks/wh_4d8q1z/test",
headers={"Authorization": f"Bearer {token}"},
)
data = response.json()
Referência
GET/v1/errors
Códigos de erro estáveis (v1)
| Status | Descrição |
|---|---|
200 | Sucesso |
curl https://api.smskit.dev/v1/errors \
-H "Authorization: Bearer $SMSKIT_TOKEN"
const response = await fetch("https://api.smskit.dev/v1/errors", {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await response.json();
import httpx
response = httpx.get(
"https://api.smskit.dev/v1/errors",
headers={"Authorization": f"Bearer {token}"},
)
data = response.json()
GET/v1/events
Tipos de evento de webhook (v1)
| Status | Descrição |
|---|---|
200 | Sucesso |
curl https://api.smskit.dev/v1/events \
-H "Authorization: Bearer $SMSKIT_TOKEN"
const response = await fetch("https://api.smskit.dev/v1/events", {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await response.json();
import httpx
response = httpx.get(
"https://api.smskit.dev/v1/events",
headers={"Authorization": f"Bearer {token}"},
)
data = response.json()
Webhooks assinados
Cadastre uma URL HTTPS em POST /v1/webhooks com os eventos que quer receber. A resposta traz o secret (whsec_…) uma única vez. Cada entrega é um POST com JSON e dois headers:
X-SMSKit-Signature: t=<unix>,v1=<hex>— HMAC-SHA256 com osecretsobre a string"<t>.<corpo bruto>".X-SMSKit-Event-Id— id do evento, útil para descartar duplicatas.
Verificação
Recalcule o HMAC sobre os bytes exatos que chegaram, compare em tempo constante e rejeite carimbos com mais de 5 minutos de diferença. JSON re-serializado muda espaços e ordem de chaves e invalida a assinatura.
import { createHmac, timingSafeEqual } from "node:crypto";
// Verifique sobre o corpo BRUTO da requisição, nunca sobre JSON re-serializado.
export function verify(secret: string, header: string, rawBody: Buffer) {
const parts = Object.fromEntries(
header.split(",").map((part) => part.trim().split("=", 2)),
);
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t));
if (!parts.t || !parts.v1 || Number.isNaN(age) || age > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${parts.t}.`)
.update(rawBody)
.digest();
const received = Buffer.from(parts.v1, "hex");
return expected.length === received.length && timingSafeEqual(expected, received);
}
// Express: mantenha o corpo bruto no handler do webhook.
app.post("/webhooks/smskit", express.raw({ type: "application/json" }), (req, res) => {
const header = req.header("X-SMSKit-Signature") ?? "";
if (!verify(process.env.SMSKIT_WEBHOOK_SECRET!, header, req.body)) {
return res.status(400).end();
}
const event = JSON.parse(req.body.toString("utf8"));
// event.type, event.data …
res.status(204).end();
});
import hmac
import time
from hashlib import sha256
def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
parts = dict(p.strip().partition("=")[::2] for p in header.split(","))
stamp, sent = parts.get("t", ""), parts.get("v1", "")
if not stamp.isdigit() or abs(int(time.time()) - int(stamp)) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{stamp}.".encode() + body, sha256).hexdigest()
return hmac.compare_digest(sent.lower(), expected)
# FastAPI: use o corpo bruto, não o JSON já analisado.
@app.post("/webhooks/smskit", status_code=204)
async def webhook(request: Request):
body = await request.body()
if not verify(SECRET, request.headers.get("X-SMSKit-Signature", ""), body):
raise HTTPException(status_code=400)
event = json.loads(body)
# event["type"], event["data"] …
Com os SDKs, a verificação é uma chamada:
import { verifySignature } from "smskit";
// Assíncrono: usa Web Crypto, funciona em Node, Bun, Deno e edge runtimes.
const ok = await verifySignature(
process.env.SMSKIT_WEBHOOK_SECRET!,
request.headers.get("X-SMSKit-Signature"),
rawBody, // string ou Uint8Array com o corpo bruto
);
if (!ok) return new Response(null, { status: 400 });
from smskit import verify_signature
ok = verify_signature(
SECRET,
request.headers.get("X-SMSKit-Signature"),
await request.body(), # bytes do corpo bruto
)
if not ok:
raise HTTPException(status_code=400)
Envelope
Todo evento tem o mesmo envelope. data é o objeto do evento: a mensagem de um destinatário em message.*, o resumo do envio em sms.completed.
{
"id": "evt_8n3v5t",
"object": "event",
"type": "message.delivered",
"api_version": "v1",
"created_at": "2026-09-11T14:03:05Z",
"test": false,
"data": {
"id": "msg_2b9x4c",
"sms_id": "sms_7f3k2m",
"to": "+5511999990123",
"status": "delivered",
"error": null,
"kind": "utility",
"updated_at": "2026-09-11T14:03:05Z",
"metadata": { "order_id": "1042" }
}
}
| Evento | Quando dispara |
|---|---|
message.sent | A operadora aceitou a mensagem para um destinatário. |
message.delivered | A operadora confirmou a entrega no aparelho. |
message.failed | A entrega falhou para um destinatário. |
sms.completed | Todos os destinatários de um envio têm resultado final. |
webhook.test | Evento de verificação disparado pelo console ou pela API. |
Tentativas
Qualquer resposta 2xx conta como entregue. Sem isso, o evento é reenviado até 6 vezes com espera crescente: 1 min, 5 min, 30 min, 2 h, 12 h. Responda rápido e processe depois; GET /v1/webhooks/{id}/deliveries mostra as tentativas recentes e POST /v1/webhooks/{id}/test dispara um webhook.test para validar a integração.
Erros estáveis
Toda falha responde com o mesmo envelope e um code do enum versionado em GET /v1/errors. Trate pelo código, não pela mensagem: a mensagem é para humanos e pode mudar. details aparece só em validation_error; request_id identifica a requisição no suporte.
{
"error": {
"code": "insufficient_balance",
"message": "O saldo não cobre este envio.",
"status": 402,
"request_id": "req_8f3k2m"
}
}
{
"error": {
"code": "validation_error",
"message": "Um ou mais campos são inválidos.",
"status": 422,
"details": [
{
"field": "to",
"message": "Número fora do padrão +55DDDNÚMERO."
}
],
"request_id": "req_2c7v9q"
}
}
| Código | HTTP | Título | Descrição |
|---|---|---|---|
validation_error | 422 | Parâmetros inválidos | Um ou mais campos não passaram na validação. O campo details lista cada problema. |
invalid_request | 400 | Requisição malformada | O corpo não é um JSON válido ou o content-type não é aceito. |
authentication_required | 401 | Autenticação obrigatória | Envie Authorization: Bearer <token> obtido em POST /v1/auth/token. |
invalid_api_key | 401 | Chave de API inválida | A chave não existe, foi revogada ou pertence a outro ambiente. |
invalid_token | 401 | Token inválido | O token Bearer não foi reconhecido. Gere um novo em POST /v1/auth/token. |
token_expired | 401 | Token expirado | O token Bearer venceu (validade de 48 horas). Gere um novo. |
forbidden | 403 | Sem permissão | A credencial não pode executar esta operação. |
template_not_approved | 403 | Modelo não aprovado | Em produção, o texto precisa corresponder a um modelo aprovado do workspace. Nada foi cobrado. |
identity_required | 403 | Titular não verificado | Envios em produção exigem o cadastro do titular (nome, CPF e data de nascimento) concluído no console e não recusado na verificação. Nada foi cobrado. |
not_found | 404 | Não encontrado | O recurso não existe ou não pertence a este workspace. |
conflict | 409 | Conflito | A mesma Idempotency-Key foi usada com um corpo diferente, ou o recurso mudou desde a leitura. |
insufficient_balance | 402 | Saldo insuficiente | O workspace não tem créditos para este envio. Recarregue via PIX no console. Nada foi enviado. |
rate_limited | 429 | Limite de requisições | Aguarde o tempo indicado em Retry-After antes de tentar novamente. |
provider_unavailable | 503 | Operadora indisponível | A rota de envio não respondeu. O envio permanece na fila e será tentado novamente. |
internal_error | 500 | Erro interno | Falha inesperada. Informe o request_id ao suporte. |
Em 429 rate_limited, o header Retry-After informa quantos segundos esperar. Os SDKs expõem esse valor em error.retryAfter / error.retry_after. 503 provider_unavailable não descarta o envio: ele permanece na fila e é tentado novamente.
SDKs
Os SDKs oficiais cobrem a API inteira, trocam a chave pelo token Bearer na primeira chamada, guardam o token em memória e renovam ao receber 401. Erros viram exceções tipadas com code, status, requestId e retryAfter.
pnpm add smskit
pip install smskit
import { SMSKit, SMSKitError } from "smskit";
const sms = new SMSKit(process.env.SMSKIT_KEY!, {
baseUrl: "https://api.smskit.dev", // opcional
});
try {
const sent = await sms.send({
to: ["+5511999990123", "+5521988880456"],
template: "order-confirmed",
variables: { empresa: "Studio", pedido: "1042", link: "https://exemplo.com/p/1042" },
idempotencyKey: "pedido-1042",
});
const status = await sms.get(sent.id);
const { balance } = await sms.account();
} catch (error) {
if (error instanceof SMSKitError) {
console.error(error.code, error.status, error.requestId, error.retryAfter);
}
}
import os
from smskit import SMSKit, SMSKitError
sms = SMSKit(os.environ["SMSKIT_KEY"], base_url="https://api.smskit.dev") # base_url opcional
try:
sent = sms.send(
to=["+5511999990123", "+5521988880456"],
template="order-confirmed",
variables={"empresa": "Studio", "pedido": "1042", "link": "https://exemplo.com/p/1042"},
idempotency_key="pedido-1042",
)
status = sms.get(sent["id"])
balance = sms.account()["balance"]
except SMSKitError as error:
print(error.code, error.status, error.request_id, error.retry_after)
# Assíncrono: from smskit import AsyncSMSKit — mesma superfície com await.
Construtor: new SMSKit(apiKey, { baseUrl?, timeoutMs?, fetch? }) em TypeScript; SMSKit(api_key, base_url=, timeout=) em Python. baseUrl muda o destino das chamadas (útil para um ambiente local); o padrão é https://api.smskit.dev.
| Ação | TypeScript | Python |
|---|---|---|
| Enviar SMS | sms.send({ to, text | template + variables, kind?, scheduledAt?, metadata?, idempotencyKey? }) | sms.send(to=, text=, template=, variables=, kind=, scheduled_at=, metadata=, idempotency_key=) |
| Consultar envio | sms.get(smsId) | sms.get(sms_id) |
| Listar envios | sms.list({ limit?, startingAfter? }) | sms.list(limit=20, starting_after=None) |
| Conta e saldo | sms.account() | sms.account() |
| Modelos | sms.templates() | sms.templates() |
| Listar webhooks | sms.webhooks.list() | sms.webhooks() |
| Criar webhook | sms.webhooks.create({ url, events, description? }) | sms.create_webhook(url=, events=, description=) |
| Remover webhook | sms.webhooks.delete(webhookId) | sms.delete_webhook(webhook_id) |
| Testar webhook | sms.webhooks.test(webhookId) | sms.test_webhook(webhook_id) |
| Verificar assinatura | await verifySignature(secret, header, rawBody) | verify_signature(secret, header, body_bytes) |
MCP para agentes
O servidor MCP expõe as ferramentas abaixo por HTTP streamable em <MCP_URL>/mcp. A chave de API vai no header Authorization; use uma chave sk_test_ para o agente ensaiar sem enviar nem cobrar nada.
{
"mcpServers": {
"smskit": {
"url": "https://mcp.smskit.dev/mcp",
"headers": { "Authorization": "Bearer sk_live_..." }
}
}
}
| Ferramenta | O que faz |
|---|---|
sms_send | Envia um SMS: to e text (modelo aprovado) ou template + variables. |
sms_status | Consulta um envio e o status por destinatário. |
sms_list | Lista os envios mais recentes do workspace. |
account_balance | Saldo de créditos e modo (live/test) da credencial. |
templates_list | Modelos do workspace, com variáveis e aprovação. |
Limites e preço
| Limite | Valor | Observação |
|---|---|---|
| Tamanho da mensagem | 160 caracteres | Após substituir as variáveis do modelo. |
| Destinatários por requisição | 1.000 | Rota de utilidade. Na rota de autenticação, 100. |
| Chaves de API por workspace | 20 | Revogue e recrie sem afetar as demais. |
| Webhooks por workspace | 10 | Até 6 tentativas por evento. |
Como o preço é calculado
Um crédito por segmento, por destinatário. Texto GSM-7 cabe em 160 caracteres num único segmento e em 153 por segmento a partir do segundo; com emojis ou caracteres fora do GSM-7 a codificação passa a Unicode e os limites caem para 70 e 67. A resposta de POST /v1/sms traz segments e credits já calculados; nada é cobrado quando o envio é recusado.
| A partir de | Por segmento |
|---|---|
| 500 créditos | R$ 0,110 |
| 1.000 créditos | R$ 0,100 |
| 5.000 créditos | R$ 0,090 |
| 10.000 créditos | R$ 0,080 |
| 30.000 créditos | R$ 0,075 |