Saltar al contenido principal

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​

joinotify.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​

joinotify.py
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​

Joinotify.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.