SDKs
Os SDKs oficiais cobrem a API inteira. Eles trocam a chave pelo token na primeira chamada, reaproveitam o token enquanto ele vale e pedem um novo ao receber 401. Cada falha vira uma exceção tipada, com code, status, requestId e retryAfter.
Instalação
pnpm add smskit
pip install smskit
Primeiro envio
A chave entra no construtor e nada mais precisa ser configurado. A troca pelo token, a renovação e a leitura do erro ficam com o SDK.
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.
O construtor é new SMSKit(apiKey, { baseUrl?, timeoutMs?, fetch? }) em TypeScript e SMSKit(api_key, base_url=, timeout=) em Python. baseUrl muda o destino das chamadas, útil para apontar para um ambiente local; o padrão é https://api.smskit.dev.
Uma diferença entre as duas bibliotecas: no corpo da requisição o remetente é from, mas from é palavra reservada em Python, então o SDK de lá recebe from_. Em TypeScript o nome é o mesmo do JSON.
Métodos
Tudo o que as duas bibliotecas fazem. Os nomes seguem a convenção de cada linguagem e o comportamento é o mesmo.
// Enviar SMS
sms.send({ to, text | template + variables, kind?, scheduledAt?, metadata?, idempotencyKey?, acceptReview? })
// Consultar envio
sms.get(smsId)
// Listar envios
sms.list({ limit?, startingAfter? })
// Conta e saldo
sms.account()
// Modelos
sms.templates()
// Listar webhooks
sms.webhooks.list()
// Criar webhook
sms.webhooks.create({ url, events, description? })
// Remover webhook
sms.webhooks.delete(webhookId)
// Testar webhook
sms.webhooks.test(webhookId)
// Verificar assinatura
await verifySignature(secret, header, rawBody)
# Enviar SMS
sms.send(to=, text=, template=, variables=, kind=, scheduled_at=, metadata=, idempotency_key=, accept_review=)
# Consultar envio
sms.get(sms_id)
# Listar envios
sms.list(limit=20, starting_after=None)
# Conta e saldo
sms.account()
# Modelos
sms.templates()
# Listar webhooks
sms.webhooks()
# Criar webhook
sms.create_webhook(url=, events=, description=)
# Remover webhook
sms.delete_webhook(webhook_id)
# Testar webhook
sms.test_webhook(webhook_id)
# Verificar assinatura
verify_signature(secret, header, body_bytes)
Erros e novas tentativas
Toda falha vira SMSKitError com o mesmo code do envelope HTTP. Dois deles pedem uma nova tentativa e o resto não: 429 rate_limited traz em retryAfter quantos segundos esperar, e 503 provider_unavailable não descarta o envio — ele segue na fila. 422 validation_error traz o campo recusado em details, e repetir sem mudar nada devolve o mesmo erro.
import { SMSKit, SMSKitError } from "smskit";
const sms = new SMSKit(process.env.SMSKIT_KEY!);
async function sendWithRetry(params: Parameters<SMSKit["send"]>[0]) {
for (let attempt = 0; ; attempt++) {
try {
return await sms.send(params);
} catch (error) {
if (!(error instanceof SMSKitError)) throw error;
// 429 diz quanto esperar; 503 nao descarta o envio, so adia.
const wait = error.retryAfter ?? 2 ** attempt;
if ((error.status === 429 || error.status === 503) && attempt < 4) {
await new Promise((r) => setTimeout(r, wait * 1000));
continue;
}
// 422 traz o campo recusado em details; o resto e definitivo.
if (error.code === "validation_error") console.error(error.details);
throw error;
}
}
}
import time
from smskit import SMSKit, SMSKitError
sms = SMSKit(os.environ["SMSKIT_KEY"])
def send_with_retry(**params):
for attempt in range(5):
try:
return sms.send(**params)
except SMSKitError as error:
# 429 diz quanto esperar; 503 nao descarta o envio, so adia.
if error.status in (429, 503) and attempt < 4:
time.sleep(error.retry_after or 2**attempt)
continue
# 422 traz o campo recusado em details; o resto e definitivo.
if error.code == "validation_error":
print(error.details)
raise
Repetir um envio com a mesma Idempotency-Key devolve o envio original em vez de criar outro, então uma nova tentativa depois de um timeout de rede é segura.
Receber eventos
A verificação da assinatura é uma chamada, e a única regra é passar o corpo cru: um JSON.parse seguido de JSON.stringify muda os bytes e invalida o HMAC. A função confere o carimbo de tempo junto com a assinatura e devolve false em qualquer dúvida.
import { verifySignature } from "smskit";
// Express: mantenha o corpo BRUTO na rota do webhook.
app.post("/webhooks/smskit", express.raw({ type: "*/*" }), async (req, res) => {
const ok = await verifySignature(
process.env.SMSKIT_WEBHOOK_SECRET!,
req.header("X-SMSKit-Signature"),
req.body, // Buffer, nao o JSON ja analisado
);
if (!ok) return res.sendStatus(400);
res.sendStatus(204); // responda primeiro, processe depois
const event = JSON.parse(req.body.toString("utf8"));
// event.type, event.data, event.id — o mesmo id numa reentrega.
});
from fastapi import HTTPException, Request
from smskit import verify_signature
@app.post("/webhooks/smskit", status_code=204)
async def webhook(request: Request):
body = await request.body() # bytes, nao o JSON ja analisado
if not verify_signature(
os.environ["SMSKIT_WEBHOOK_SECRET"],
request.headers.get("X-SMSKit-Signature"),
body,
):
raise HTTPException(status_code=400)
event = json.loads(body)
# event["type"], event["data"], event["id"] — o mesmo id numa reentrega.
Assíncrono e paginação
Em Python, AsyncSMSKit tem a mesma superfície com await e fecha o cliente ao sair do async with. Em TypeScript o cliente já é assíncrono. As listagens vêm por página: siga o id do último item em startingAfter até has_more virar false.
// Paginacao: siga o ultimo id ate has_more virar false.
let startingAfter: string | undefined;
do {
const page = await sms.list({ limit: 50, startingAfter });
for (const sent of page.data) console.log(sent.id, sent.status);
startingAfter = page.data.at(-1)?.id;
} while (startingAfter && page.has_more);
import asyncio
from smskit import AsyncSMSKit
async def main():
async with AsyncSMSKit(os.environ["SMSKIT_KEY"]) as sms:
sent = await sms.send(to="+5511999990123", text="Seu pedido saiu.")
print(await sms.get(sent["id"]))
starting_after = None
while True:
page = await sms.list(limit=50, starting_after=starting_after)
for item in page["data"]:
print(item["id"], item["status"])
if not page["has_more"] or not page["data"]:
break
starting_after = page["data"][-1]["id"]
asyncio.run(main())
Modo de teste
Uma chave sk_test_ percorre as mesmas rotas, com as mesmas respostas, sem entregar nem cobrar. O cliente diz em qual modo está, o que evita mandar um ensaio para produção por engano.
// sk_test_ percorre as mesmas rotas sem entregar nem cobrar.
const sandbox = new SMSKit(process.env.SMSKIT_TEST_KEY!);
console.log(sandbox.testMode); // true
// Numeros terminados em 0000 falham; os demais sao entregues.
await sandbox.send({ to: "+5511999990000", text: "Falha simulada" });
# sk_test_ percorre as mesmas rotas sem entregar nem cobrar.
sandbox = SMSKit(os.environ["SMSKIT_TEST_KEY"])
print(sandbox.test_mode) # True
# Numeros terminados em 0000 falham; os demais sao entregues.
sandbox.send(to="+5511999990000", text="Falha simulada")
Detalhes do que muda em Modo de teste, e o texto que a rede aceita em Limites e preço.