Clientes e snippets
Não publicamos SDK oficial. A API é HTTP + JSON com autenticação Bearer — qualquer cliente HTTP resolve, e você não fica preso a uma dependência nossa.
Cada página da referência traz o snippet pronto em curl, Node.js, Python, PHP, Go e Ruby, com o payload da operação já preenchido. O que segue aqui é o esqueleto reutilizável.
Node.js
const BASE_URL = 'https://api.joinotify.com';
export class Joinotify {
constructor(token = process.env.JOINOTIFY_TOKEN) {
this.token = token;
}
async request(method, path, body) {
const response = await fetch(`${BASE_URL}${path}`, {
method,
headers: {
Authorization: `Bearer ${this.token}`,
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
});
// Respeita o Retry-After em vez de insistir.
if (response.status === 429) {
const espera = Number(response.headers.get('retry-after') ?? 5);
await new Promise((r) => setTimeout(r, espera * 1000));
return this.request(method, path, body);
}
const payload = await response.json();
if (!response.ok) {
const erro = new Error(payload?.error?.message ?? 'Erro na API do Joinotify');
erro.type = payload?.error?.type;
erro.status = response.status;
erro.meta = payload?.error?.meta;
throw erro;
}
return payload;
}
enviarTexto(to, body, extras = {}) {
return this.request('POST', '/messages', { to, body, ...extras });
}
enviarTemplate(to, name, componentes = [], language = 'pt_BR') {
return this.request('POST', '/messages', {
to,
name,
language,
components: componentes,
});
}
listarTemplates(params = {}) {
const query = new URLSearchParams(params).toString();
return this.request('GET', `/templates${query ? `?${query}` : ''}`);
}
}
const joinotify = new Joinotify();
const { data } = await joinotify.enviarTexto('5541987111527', 'Pedido confirmado!');
console.log(data.messages[0].id);
Python
import os
import time
import requests
BASE_URL = "https://api.joinotify.com"
class JoinotifyError(Exception):
def __init__(self, message, *, type=None, status=None, meta=None):
super().__init__(message)
self.type = type
self.status = status
self.meta = meta
class Joinotify:
def __init__(self, token=None):
self.session = requests.Session()
self.session.headers["Authorization"] = f"Bearer {token or os.environ['JOINOTIFY_TOKEN']}"
def request(self, method, path, json=None):
response = self.session.request(method, f"{BASE_URL}{path}", json=json, timeout=30)
if response.status_code == 429:
time.sleep(int(response.headers.get("Retry-After", 5)))
return self.request(method, path, json)
payload = response.json()
if not response.ok:
erro = payload.get("error", {})
raise JoinotifyError(
erro.get("message", "Erro na API do Joinotify"),
type=erro.get("type"),
status=response.status_code,
meta=erro.get("meta"),
)
return payload
def enviar_texto(self, to, body, **extras):
return self.request("POST", "/messages", {"to": to, "body": body, **extras})
PHP
<?php
final class Joinotify
{
private const BASE_URL = 'https://api.joinotify.com';
public function __construct(private readonly string $token) {}
public function request(string $method, string $path, ?array $body = null): array
{
$ch = curl_init(self::BASE_URL . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $this->token,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => $body ? json_encode($body, JSON_UNESCAPED_UNICODE) : null,
]);
$resposta = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$payload = json_decode($resposta, true);
if ($status >= 400) {
throw new RuntimeException($payload['error']['message'] ?? 'Erro na API do Joinotify', $status);
}
return $payload;
}
public function enviarTexto(string $to, string $body): array
{
return $this->request('POST', '/messages', ['to' => $to, 'body' => $body]);
}
}
Go
package joinotify
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
)
const baseURL = "https://api.joinotify.com"
type Client struct {
Token string
HTTP *http.Client
}
func New(token string) *Client {
return &Client{Token: token, HTTP: &http.Client{Timeout: 30 * time.Second}}
}
func (c *Client) Do(method, path string, body any, out any) error {
var buf bytes.Buffer
if body != nil {
if err := json.NewEncoder(&buf).Encode(body); err != nil {
return err
}
}
req, err := http.NewRequest(method, baseURL+path, &buf)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+c.Token)
req.Header.Set("Content-Type", "application/json")
res, err := c.HTTP.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode >= 400 {
return fmt.Errorf("joinotify: status %d", res.StatusCode)
}
return json.NewDecoder(res.Body).Decode(out)
}
Ruby
require 'json'
require 'net/http'
class Joinotify
BASE_URL = 'https://api.joinotify.com'
def initialize(token = ENV.fetch('JOINOTIFY_TOKEN'))
@token = token
end
def request(method, path, body = nil)
uri = URI("#{BASE_URL}#{path}")
klass = Net::HTTP.const_get(method.capitalize)
req = klass.new(uri)
req['Authorization'] = "Bearer #{@token}"
req['Content-Type'] = 'application/json'
req.body = body.to_json if body
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
payload = JSON.parse(res.body)
raise payload.dig('error', 'message') || 'Erro na API do Joinotify' unless res.is_a?(Net::HTTPSuccess)
payload
end
def enviar_texto(to, body)
request(:post, '/messages', { to: to, body: body })
end
end
O que todo cliente precisa ter
Timeout. Sem ele, uma requisição travada segura um worker indefinidamente. 30 segundos cobre com folga o timeout de 30s do proxy.
Tratamento de 429. Respeite o Retry-After. Ver
Limites de uso.
Backoff em 5xx. 500, 502 e 504 valem retentativa com espera crescente. 4xx
não — repetir vai falhar igual.
O x-request-id nos logs. É a primeira coisa que o suporte pede.
O token fora do código. Variável de ambiente ou cofre de segredos, nunca no repositório.