AFLINK / API
Conectar seu SaaS ao Aflink
Este guia mostra o caminho mais curto para seu SaaS pagar comissões a afiliados pelo Aflink. A integração tem três partes e nenhuma delas fica no caminho do seu checkout ou do carregamento das suas páginas:
- Clique — o afiliado divulga um link
https://aflink.app/r/{código}. O Aflink registra o clique e redireciona o visitante para o seu site com?_aflink=att_…. - Captura — seu site guarda esse token junto do cadastro do cliente.
- Eventos — quando o cliente indicado paga, renova ou é reembolsado, seu servidor envia um evento assinado ao Aflink. A comissão é calculada pela política do seu programa.
Sem script de terceiros, sem pixel e sem chamada síncrona ao Aflink no checkout.
0. Prepare no Aflink (5 minutos)
- Entre em aflink.app com sua Conta CIATTO e abra sua empresa.
- Programas → Criar programa: informe o endereço do seu site (o destino dos links), a política de comissão e os termos. Os valores sugeridos (15%, 12 pagamentos mensais, 30 dias de atribuição, 30 dias de retenção) podem ser alterados.
- Integrações → Nova credencial: crie uma credencial TEST para validar e, depois, uma LIVE para produção. A chave de API e o segredo de assinatura aparecem uma única vez: guarde-os no cofre de segredos do seu servidor, nunca no navegador, no app ou no repositório.
Use o modelo de pagamentos mensais (padrão) sempre que possível: ele não exige enviar janelas de serviço. O modelo por meses de serviço está descrito no final.
1. Capture o token do clique
O visitante chega ao seu site com ?_aflink=att_…. Guarde o token por, no máximo, o prazo de atribuição do programa (30 dias por padrão). Se chegar um token novo, ele substitui o anterior (vale o último clique) até o primeiro pagamento.
Opção A — no servidor (recomendada)
No middleware que atende suas páginas públicas:
se a URL tem _aflink e ele casa com ^att_[A-Za-z0-9_-]{20,128}$:
gravar cookie aflink_att=<token>; Max-Age=2592000; Path=/; Secure; HttpOnly; SameSite=Lax
(use Domain=.seudominio.com se o cadastro acontece em outro subdomínio)
Opção B — no navegador
Um trecho inline de poucas linhas, sem dependência e sem rede. Coloque-o na página de destino:
<script>
(function () {
try {
var t = new URLSearchParams(location.search).get("_aflink");
if (!t || !/^att_[A-Za-z0-9_-]{20,128}$/.test(t)) return;
document.cookie = "aflink_att=" + t + "; Max-Age=2592000; Path=/; SameSite=Lax; Secure";
var u = new URL(location.href);
u.searchParams.delete("_aflink");
history.replaceState(null, "", u);
} catch (e) {}
})();
</script>
No cadastro
Quando a conta for criada, leia o cookie aflink_att no servidor e grave o token no registro do cliente, por exemplo nas colunas aflink_token e aflink_token_at. Sem token, o cliente não foi indicado e nenhum evento deve ser enviado para ele.
2. Escolha os eventos
Envie eventos somente de clientes indicados, ou seja, que têm token ou cupom de afiliado. Isso reduz volume e evita compartilhar dados de quem não veio de um afiliado.
| Quando acontece no seu SaaS | Evento | Campos obrigatórios além dos comuns |
|---|---|---|
| Primeiro pagamento de uma assinatura | subscription.started | attributionToken (ou couponCode), externalSubscriptionId, customerExternalId, occurredAt |
| Cada pagamento seguinte da assinatura | subscription.renewed | externalSubscriptionId, customerExternalId, subscriptionCycle (2, 3, …), occurredAt |
| Compra avulsa | order.paid | attributionToken (ou couponCode), customerExternalId |
| Reembolso total ou parcial | refund.created | originalExternalPaymentId |
| Chargeback | chargeback.created | originalExternalPaymentId |
Campos comuns a todos os eventos:
| Campo | Regra |
|---|---|
externalPaymentId | ID do pagamento no seu sistema ou no gateway (3–255 caracteres). Único por pagamento. |
customerEmail | E-mail do cliente. |
customerExternalId | ID estável do cliente no seu SaaS, nunca o e-mail (até 255). Ex.: seusaas:conta_123. |
value | Valor sobre o qual a comissão é calculada, como texto decimal: "189.00". Sempre positivo, inclusive em reembolsos (até 2 casas). |
currency | Código de 3 letras igual ao do programa, ex.: BRL. |
occurredAt | Momento do pagamento em RFC 3339 (UTC). Não pode estar mais de 5 min no futuro. |
metadata | Opcional. Não coloque dados pessoais. |
O primeiro pagamento amarra o cliente ao afiliado de forma permanente: renovações e reembolsos são reconhecidos pelo customerExternalId e pelo originalExternalPaymentId, sem precisar do token de novo. Envie o pagamento original antes do seu reembolso.
Exemplo de subscription.started:
{
"type": "subscription.started",
"data": {
"attributionToken": "att_AbCdEf…",
"customerExternalId": "seusaas:conta_123",
"customerEmail": "[email protected]",
"externalPaymentId": "in_1Q2w3E",
"externalSubscriptionId": "sub_9Z8y7X",
"value": "189.00",
"currency": "BRL",
"occurredAt": "2026-10-22T14:30:00Z"
}
}
3. Assine e envie
POST https://api.aflink.dev/v1/events com Content-Type: application/json e:
| Cabeçalho | Valor |
|---|---|
Authorization | Bearer <chave de API> |
Idempotency-Key | Chave determinística do evento, ex.: subscription.started:in_1Q2w3E (8–255). |
X-Aflink-Timestamp | Unix em segundos (10 dígitos). Relógio com até 5 min de diferença. |
X-Aflink-Nonce | Aleatório novo a cada tentativa, 16–128 caracteres [A-Za-z0-9_-]. |
X-Aflink-Signature | v1= + hex de HMAC-SHA256(segredo de assinatura, timestamp + "." + nonce + "." + corpo). |
A assinatura usa os bytes exatos do corpo enviado. O corpo tem limite de 64 KB. Chamadas feitas do navegador (com Cookie ou Origin) são recusadas.
Node.js 18+
import crypto from "node:crypto";
export async function sendAflinkEvent(idempotencyKey, body) {
// body: string JSON exata, guardada no outbox (não serialize de novo).
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(24).toString("base64url");
const signature = crypto
.createHmac("sha256", process.env.AFLINK_SIGNING_SECRET)
.update(`${timestamp}.${nonce}.${body}`)
.digest("hex");
return fetch("https://api.aflink.dev/v1/events", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.AFLINK_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"X-Aflink-Timestamp": timestamp,
"X-Aflink-Nonce": nonce,
"X-Aflink-Signature": `v1=${signature}`,
},
body,
signal: AbortSignal.timeout(10_000),
});
}
Python 3
import hashlib, hmac, os, secrets, time, urllib.request
def send_aflink_event(idempotency_key: str, body: bytes):
timestamp = str(int(time.time()))
nonce = secrets.token_urlsafe(24)
message = timestamp.encode() + b"." + nonce.encode() + b"." + body
signature = hmac.new(os.environ["AFLINK_SIGNING_SECRET"].encode(), message, hashlib.sha256).hexdigest()
request = urllib.request.Request(
"https://api.aflink.dev/v1/events", data=body, method="POST",
headers={
"Content-Type": "application/json",
"Authorization": "Bearer " + os.environ["AFLINK_API_KEY"],
"Idempotency-Key": idempotency_key,
"X-Aflink-Timestamp": timestamp,
"X-Aflink-Nonce": nonce,
"X-Aflink-Signature": "v1=" + signature,
})
return urllib.request.urlopen(request, timeout=10)
Go
func sendAflinkEvent(ctx context.Context, idempotencyKey string, body []byte) (*http.Response, error) {
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
raw := make([]byte, 24)
if _, err := rand.Read(raw); err != nil {
return nil, err
}
nonce := base64.RawURLEncoding.EncodeToString(raw)
mac := hmac.New(sha256.New, []byte(os.Getenv("AFLINK_SIGNING_SECRET")))
mac.Write([]byte(timestamp + "." + nonce + "."))
mac.Write(body)
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.aflink.dev/v1/events", bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+os.Getenv("AFLINK_API_KEY"))
req.Header.Set("Idempotency-Key", idempotencyKey)
req.Header.Set("X-Aflink-Timestamp", timestamp)
req.Header.Set("X-Aflink-Nonce", nonce)
req.Header.Set("X-Aflink-Signature", "v1="+hex.EncodeToString(mac.Sum(nil)))
return (&http.Client{Timeout: 10 * time.Second}).Do(req)
}
PHP
function send_aflink_event(string $idempotencyKey, string $body) {
$timestamp = (string) time();
$nonce = rtrim(strtr(base64_encode(random_bytes(24)), '+/', '-_'), '=');
$signature = hash_hmac('sha256', $timestamp . '.' . $nonce . '.' . $body, getenv('AFLINK_SIGNING_SECRET'));
$ch = curl_init('https://api.aflink.dev/v1/events');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . getenv('AFLINK_API_KEY'),
'Idempotency-Key: ' . $idempotencyKey,
'X-Aflink-Timestamp: ' . $timestamp,
'X-Aflink-Nonce: ' . $nonce,
'X-Aflink-Signature: v1=' . $signature,
],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [$status, $response];
}
4. Entregue sem perder dados e sem pesar seu sistema
Não chame o Aflink dentro da requisição de pagamento. Use um outbox:
- Na mesma transação que grava o pagamento no seu banco, grave o evento numa tabela de saída com a
Idempotency-Keye o corpo JSON exato que será enviado. - Um job em segundo plano envia os pendentes. A cada tentativa gera timestamp, nonce e assinatura novos, mas mantém a mesma chave e o mesmo corpo.
- Marque como entregue ao receber
202(aceito) ou200(já recebido antes).
CREATE TABLE aflink_outbox (
idempotency_key text PRIMARY KEY,
body text NOT NULL, -- bytes exatos; nunca reserializar
attempts int NOT NULL DEFAULT 0,
next_attempt_at timestamptz NOT NULL DEFAULT now(),
delivered_at timestamptz,
last_error text
);
-- job: SELECT … WHERE delivered_at IS NULL AND next_attempt_at <= now()
-- ORDER BY next_attempt_at LIMIT 50 FOR UPDATE SKIP LOCKED
Reenviar é sempre seguro: a mesma chave com o mesmo corpo é deduplicada, e o mesmo externalPaymentId nunca gera duas comissões.
| Resposta | O que fazer |
|---|---|
202 ou 200 | Entregue. O processamento continua no Aflink. |
409 request_replay | Nonce repetido. Tente de novo com nonce novo. |
429 rate_limited | Aguarde o Retry-After (60 s). Limite: 300 eventos/min por origem. |
5xx, timeout ou erro de rede | Tente de novo com espera crescente (1, 5, 15, 60 min… por até 3 dias). |
400 invalid_integration_headers | Corrija relógio, nonce ou chave de idempotência e reenvie. |
400 invalid_event | Dados inválidos. Não repita; corrija o evento e alerte. |
401 invalid_credential / invalid_signature | Chave revogada ou segredo errado. Pause o envio e alerte. |
409 idempotency_conflict | Mesma chave com corpo diferente: erro na sua integração. Não repita. |
413 event_too_large | Corpo acima de 64 KB. Remova metadata desnecessário. |
5. Teste e entre em produção
- Configure a credencial TEST e envie um
subscription.startedusando um token real (clique em um link de teste do seu programa). Eventos TEST nunca geram comissão. - Em Integrações → Recibos de eventos, confira o status:
PROCESSED: tudo certo.FAILEDcomprocessing_requirements_not_met: o evento não corresponde a uma atribuição válida. Verifique o token, o prazo de atribuição, a moeda, se o programa está ativo e se o afiliado foi aprovado.RECEIVEDcomoriginal_payment_pending: o reembolso chegou antes do pagamento original; o Aflink tenta novamente por cerca de 10 minutos.
- Troque para a credencial LIVE.
Checklist de produção
- Chave e segredo LIVE no cofre do servidor; nada no front-end.
- Token
_aflinkcapturado e gravado no cadastro do cliente. - Eventos gravados no outbox na mesma transação do pagamento.
-
Idempotency-Keydeterminística e corpo exato reaproveitado nos reenvios. - Apenas clientes indicados geram eventos.
-
customerExternalIdestável e moeda igual à do programa. - Reembolsos e chargebacks enviados com o pagamento original.
- Alerta para respostas
400,401e409 idempotency_conflict.
Programas por meses de serviço (avançado)
Se o programa paga comissão por meses de serviço (por exemplo, durante o primeiro ano), cada evento de assinatura precisa de serviceWindow:
"serviceWindow": {
"start": "2026-10-22T14:30:00Z",
"end": "2027-10-22T14:30:00Z",
"components": [
{ "id": "plano-anual", "start": "2026-10-22T14:30:00Z", "end": "2027-10-22T14:30:00Z", "amountMinor": 561600 }
]
}
starté o início do primeiro serviço pago do cliente e não muda nas renovações.endé exatamentestartmais a duração do programa em meses, mantendo o horário e usando o último dia do mês quando o dia não existe.- Os componentes são os períodos cobrados (cada um com até 12 meses), em segundos inteiros. A soma prorrateada dentro da janela precisa ser igual a
value(em centavos).
Prefira o modelo de pagamentos mensais quando não precisar dessa precisão.
Referência
- Contrato completo (OpenAPI 3.1):
api/openapi.jsonno repositório do Aflink. - Dúvidas: [email protected].