Allya Payments
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 URL

Receber 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:

  1. Caminho feliz: o webhook payment.paid chega → você reage.
  2. Webhook atrasou ou caiu: depois de N segundos sem evento, você chama POST /v1/payments/:id/sync para 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

On this page