Clientes y fragmentos
No publicamos un SDK oficial. La API es HTTP + JSON con autenticación Bearer — cualquier cliente HTTP la resuelve, y no te atas a una dependencia nuestra.
Cada página de la referencia trae el fragmento listo en curl, Node.js, Python, PHP, Go y Ruby, con el payload de esa operación ya relleno. Lo que sigue aquí es el esqueleto reutilizable.
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,
});
// Respeta Retry-After en lugar 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 error = new Error(payload?.error?.message ?? 'Error de la API de Joinotify');
error.type = payload?.error?.type;
error.status = response.status;
error.meta = payload?.error?.meta;
throw error;
}
return payload;
}
enviarTexto(to, body, extras = {}) {
return this.request('POST', '/messages', { to, body, ...extras });
}
enviarPlantilla(to, name, componentes = [], language = 'pt_BR') {
return this.request('POST', '/messages', {
to,
name,
language,
components: componentes,
});
}
listarPlantillas(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:
error = payload.get("error", {})
raise JoinotifyError(
error.get("message", "Error de la API de Joinotify"),
type=error.get("type"),
status=response.status_code,
meta=error.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,
]);
$respuesta = curl_exec($ch);
$estado = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$payload = json_decode($respuesta, true);
if ($estado >= 400) {
throw new RuntimeException($payload['error']['message'] ?? 'Error de la API de Joinotify', $estado);
}
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') || 'Error de la API de Joinotify' unless res.is_a?(Net::HTTPSuccess)
payload
end
def enviar_texto(to, body)
request(:post, '/messages', { to: to, body: body })
end
end
Lo que todo cliente necesita
Un tiempo de espera. Sin él, una petición atascada retiene un worker indefinidamente. 30 segundos cubren de sobra el propio tiempo de espera de 30s del proxy.
Tratamiento del 429. Respeta Retry-After. Consulta
Límites de uso.
Backoff en 5xx. 500, 502 y 504 merecen reintento con esperas crecientes. Los
4xx no — el reintento fallará igual.
El x-request-id en tus registros. Es lo primero que pide soporte.
El token fuera del código. Variable de entorno o gestor de secretos, nunca en el repositorio.