Referência
Exemplos em TypeScript
Exemplos simples com fetch para autenticação, criação de pagamento e webhooks.
Os exemplos abaixo usam fetch nativo e não dependem de SDK oficial.
Cliente HTTP simples
const ALLYA_BASE_URL = process.env.ALLYA_BASE_URL ?? "https://payments-api.allyasolutions.com";
const ALLYA_API_KEY = process.env.ALLYA_API_KEY!;
async function allyaFetch<T>(path: string, init: RequestInit = {}): Promise<T> {
const response = await fetch(`${ALLYA_BASE_URL}${path}`, {
...init,
headers: {
Authorization: `Bearer ${ALLYA_API_KEY}`,
"Content-Type": "application/json",
...init.headers,
},
});
const body = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(body?.error ?? `HTTP ${response.status}`);
}
return body as T;
}Testar autenticação
type AuthTestResponse = {
ok: true;
organization: { id: string };
project: { id: string };
environment: { id: string; kind: "sandbox" | "production" };
apiKey: { id: string };
};
const context = await allyaFetch<AuthTestResponse>("/v1/auth/test");
console.log(context.environment.kind);Criar pagamento Pix
type PaymentStatus =
| "created"
| "pending"
| "processing"
| "paid"
| "failed"
| "canceled"
| "expired";
type GatewayProvider = "abacate_pay" | "asaas" | "pagarme";
type PaymentResponse = {
id: string;
status: PaymentStatus;
method: "pix" | "card";
gateway: GatewayProvider;
amount: number;
currency: "BRL";
externalId: string | null;
gatewayRef: string | null;
customer: {
name: string | null;
email: string | null;
documentLast4: string | null;
} | null;
checkoutUrl: string | null;
pix: {
qrCode?: string;
qrCodeText?: string;
} | null;
card: {
brand?: string;
last4?: string;
} | null;
};
const payment = await allyaFetch<PaymentResponse>("/v1/payments", {
method: "POST",
body: JSON.stringify({
amount: 4990,
currency: "BRL",
method: "pix",
externalId: "pedido_123",
customer: {
name: "Cliente Teste",
email: "cliente@example.com",
document: "00000000000",
},
}),
});
console.log(payment.id, payment.status, payment.pix?.qrCodeText);Criar pagamento com cartão
const cardPayment = await allyaFetch<PaymentResponse>("/v1/payments", {
method: "POST",
body: JSON.stringify({
amount: 12990,
currency: "BRL",
method: "card",
externalId: "pedido_124",
customer: {
name: "Cliente Teste",
email: "cliente@example.com",
document: "00000000000",
phone: "11999999999",
},
card: {
installments: 1,
},
metadata: {
description: "Pedido 124",
},
}),
});
console.log(cardPayment.checkoutUrl);Validar webhook
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWebhookSignature(
rawBody: string,
timestamp: string,
signature: string,
secret: string,
) {
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const received = signature.replace(/^sha256=/, "");
const a = Buffer.from(received, "hex");
const b = Buffer.from(expected, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}Processar evento
type PaymentEventType =
| "payment.created"
| "payment.pending"
| "payment.paid"
| "payment.failed"
| "payment.expired"
| "payment.canceled";
type AllyaEvent = {
id: string;
type: PaymentEventType;
createdAt: string;
data: {
paymentId: string;
externalId: string | null;
status: PaymentStatus;
method: "pix" | "card";
provider: GatewayProvider;
amount: number;
currency: "BRL";
gatewayRef: string | null;
};
};
function handleEvent(event: AllyaEvent) {
switch (event.type) {
case "payment.created":
case "payment.pending":
return markOrderAsAwaitingPayment(event.data.externalId);
case "payment.paid":
return markOrderAsPaid(event.data.externalId);
case "payment.failed":
case "payment.expired":
case "payment.canceled":
return markOrderAsUnpaid(event.data.externalId, event.data.status);
default:
return ignoreUnknownPaymentEvent(event);
}
}
async function markOrderAsAwaitingPayment(externalId: string | null) {
if (!externalId) return;
// Atualize seu pedido no banco como aguardando pagamento.
}
async function markOrderAsPaid(externalId: string | null) {
if (!externalId) return;
// Atualize seu pedido no banco.
}
async function markOrderAsUnpaid(externalId: string | null, _status: PaymentStatus) {
if (!externalId) return;
// Atualize seu pedido no banco.
}
function ignoreUnknownPaymentEvent(_event: AllyaEvent) {
return;
}Criar assinatura
type SubscriptionResponse = {
id: string;
status: "trialing" | "active" | "past_due" | "canceled";
method: "card";
gateway: "abacate_pay" | "asaas" | "pagarme";
amount: number;
currency: "BRL";
interval: "monthly" | "yearly";
trialDays: number;
trialEndsAt: string | null;
nextBillingAt: string | null;
cancelAtPeriodEnd: boolean;
canceledAt: string | null;
externalId: string | null;
gatewayRef: string | null;
checkoutUrl: string | null;
customer: {
name: string | null;
email: string | null;
documentLast4: string | null;
} | null;
createdAt: string;
};
const subscription = await allyaFetch<SubscriptionResponse>("/v1/subscriptions", {
method: "POST",
body: JSON.stringify({
amount: 4990,
currency: "BRL",
interval: "monthly",
externalId: "cliente-42-mensal",
customer: {
name: "Cliente Teste",
email: "cliente@example.com",
document: "12345678901",
phone: "11999999999",
},
// Obrigatório quando o roteamento resolve para Pagar.me.
// Ignorado nos demais. Pode passar sempre.
gateway: { pagarme: { planId: "plan_xxx" } },
}),
});
console.log(subscription.checkoutUrl); // redirecione o cliente para essa URLReceber webhook de assinatura
type AllyaSubscriptionEvent = {
id: string;
type:
| "subscription.created"
| "subscription.renewed"
| "subscription.payment_failed"
| "subscription.past_due"
| "subscription.canceled";
createdAt: string;
data: {
subscriptionId: string;
externalId: string | null;
status: "trialing" | "active" | "past_due" | "canceled";
method: "card";
provider: "abacate_pay" | "asaas" | "pagarme";
amount: number;
currency: "BRL";
interval: "monthly" | "yearly";
gatewayRef: string | null;
nextBillingAt: string | null;
trialEndsAt: string | null;
cancelAtPeriodEnd: boolean;
canceledAt: string | null;
};
};
function handleSubscriptionEvent(event: AllyaSubscriptionEvent) {
switch (event.type) {
case "subscription.created":
case "subscription.renewed":
return giveAccess(event.data.externalId, event.data.nextBillingAt);
case "subscription.payment_failed":
return notifyCustomerPaymentIssue(event.data.externalId);
case "subscription.past_due":
return startDunningCampaign(event.data.externalId);
case "subscription.canceled":
return revokeAccess(event.data.externalId, event.data.canceledAt);
}
}Padrão "criar + esperar webhook + fallback sync"
Quando você cria um Pix e quer agir assim que ele for pago:
- Caminho feliz: o webhook
payment.paidchega → você reage. - Webhook atrasou ou caiu: depois de N segundos sem evento, você chama
POST /v1/payments/:id/syncpara forçar reconciliação.
async function createPaymentWithFallback(order: Order) {
const payment = await allyaFetch<PaymentResponse>("/v1/payments", {
method: "POST",
body: JSON.stringify({
amount: order.amount,
currency: "BRL",
method: "pix",
externalId: `pedido_${order.id}`,
customer: order.customer,
}),
});
// Agende fallback para 3 minutos depois (ajustar conforme expectativa do gateway).
setTimeout(async () => {
const stillPending = await allyaFetch<PaymentResponse>(
`/v1/payments/${payment.id}`,
);
if (stillPending.status === "pending" || stillPending.status === "processing") {
const synced = await allyaFetch<PaymentResponse>(
`/v1/payments/${payment.id}/sync`,
{ method: "POST" },
);
if (synced.status === "paid") {
await markOrderAsPaid(synced.externalId);
}
}
}, 180_000);
return payment;
}Em produção real, use uma fila/job scheduler em vez de setTimeout: setTimeout morre com o processo.
Cliente com retry e timeout
import { setTimeout as delay } from "node:timers/promises";
const TIMEOUT_MS = 10_000;
const MAX_ATTEMPTS = 3;
async function allyaFetchRobust<T>(
path: string,
init: RequestInit = {},
): Promise<T> {
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
try {
const res = await fetch(`${ALLYA_BASE_URL}${path}`, {
...init,
signal: controller.signal,
headers: {
Authorization: `Bearer ${ALLYA_API_KEY}`,
"Content-Type": "application/json",
...init.headers,
},
});
if (res.status === 429) {
const retryAfter = Number(res.headers.get("Retry-After") ?? "1");
await delay(retryAfter * 1000);
continue;
}
if (res.status >= 500 && attempt < MAX_ATTEMPTS) {
await delay(500 * attempt);
continue;
}
const body = await res.json().catch(() => null);
if (!res.ok) throw new Error(body?.error ?? `HTTP ${res.status}`);
return body as T;
} catch (err) {
if (attempt === MAX_ATTEMPTS) throw err;
await delay(500 * attempt);
} finally {
clearTimeout(timer);
}
}
throw new Error("unreachable");
}Retries são seguros em POST /v1/payments e POST /v1/subscriptions por causa da idempotência: desde que o externalId esteja presente.
Veja também
- Webhooks: formato e validação dos eventos.
- Rate limiting: backoff em 429.
- Idempotência: quando retries são seguros.