aflink.

AFLINK / API

Conecta tu SaaS a Aflink

Esta guía es el camino más corto para que tu SaaS pague comisiones a afiliados con Aflink. La integración tiene tres partes y ninguna está en el camino de tu checkout ni hace más lentas tus páginas:

  1. Clic — el afiliado comparte un enlace https://aflink.app/r/{código}. Aflink registra el clic y redirige al visitante a tu sitio con ?_aflink=att_….
  2. Captura — tu sitio guarda ese token junto con el registro del cliente.
  3. Eventos — cuando un cliente referido paga, renueva o recibe un reembolso, tu servidor envía un evento firmado a Aflink. La comisión sigue la política de tu programa.

Sin scripts de terceros, sin píxel y sin llamadas síncronas a Aflink en el checkout.

  1. Entra en aflink.app con tu cuenta CIATTO y abre tu empresa.
  2. Programas → Crear programa: indica la dirección de tu sitio (destino de los enlaces), la política de comisión y los términos. Los valores sugeridos (15%, 12 pagos mensuales, 30 días de atribución, 30 días de retención) son editables.
  3. Integraciones → Nueva credencial: crea una credencial TEST para validar y luego una LIVE para producción. La clave de API y el secreto de firma se muestran una sola vez: guárdalos en el almacén de secretos de tu servidor, nunca en el navegador, la app o el repositorio.

Prefiere el modelo de pagos mensuales (predeterminado): no requiere ventanas de servicio. El modelo por meses de servicio se describe al final.

1. Captura el token del clic

Los visitantes llegan con ?_aflink=att_…. Guarda el token como máximo durante el plazo de atribución del programa (30 días por defecto). Un token más nuevo reemplaza al anterior (gana el último clic) hasta el primer pago.

Opción A — en el servidor (recomendada)

En el middleware que sirve tus páginas públicas:

si la URL tiene _aflink y coincide con ^att_[A-Za-z0-9_-]{20,128}$:
    crear cookie aflink_att=<token>; Max-Age=2592000; Path=/; Secure; HttpOnly; SameSite=Lax
    (usa Domain=.tudominio.com si el registro ocurre en otro subdominio)

Opción B — en el navegador

Unas pocas líneas en línea, sin dependencias ni red. Colócalas en la 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>

En el registro

Cuando se cree la cuenta, lee la cookie aflink_att en el servidor y guarda el token en el registro del cliente, por ejemplo en columnas aflink_token y aflink_token_at. Sin token, el cliente no fue referido y no se debe enviar ningún evento.

2. Elige los eventos

Envía eventos solo de clientes referidos, es decir, con token o cupón de afiliado. Así reduces el volumen y no compartes datos de clientes que no llegaron por un afiliado.

Cuando ocurre en tu SaaSEventoCampos obligatorios además de los comunes
Primer pago de una suscripciónsubscription.startedattributionToken (o couponCode), externalSubscriptionId, customerExternalId, occurredAt
Cada pago siguiente de la suscripciónsubscription.renewedexternalSubscriptionId, customerExternalId, subscriptionCycle (2, 3, …), occurredAt
Compra únicaorder.paidattributionToken (o couponCode), customerExternalId
Reembolso total o parcialrefund.createdoriginalExternalPaymentId
Contracargochargeback.createdoriginalExternalPaymentId

Campos comunes a todos los eventos:

CampoRegla
externalPaymentIdID del pago en tu sistema o pasarela (3–255 caracteres). Único por pago.
customerEmailCorreo del cliente.
customerExternalIdID estable del cliente en tu SaaS, nunca el correo (hasta 255). Ej.: tusaas:cuenta_123.
valueImporte sobre el que se calcula la comisión, como texto decimal: "189.00". Siempre positivo, también en reembolsos (hasta 2 decimales).
currencyCódigo de 3 letras igual al del programa, ej.: BRL.
occurredAtMomento del pago en RFC 3339 (UTC). Como máximo 5 min en el futuro.
metadataOpcional. No incluyas datos personales.

El primer pago vincula al cliente con el afiliado de forma permanente: las renovaciones y los reembolsos se reconocen por customerExternalId y originalExternalPaymentId, sin volver a enviar el token. Envía el pago original antes de su reembolso.

Ejemplo de subscription.started:

{
  "type": "subscription.started",
  "data": {
    "attributionToken": "att_AbCdEf…",
    "customerExternalId": "tusaas:cuenta_123",
    "customerEmail": "[email protected]",
    "externalPaymentId": "in_1Q2w3E",
    "externalSubscriptionId": "sub_9Z8y7X",
    "value": "189.00",
    "currency": "BRL",
    "occurredAt": "2026-10-22T14:30:00Z"
  }
}

3. Firma y envía

POST https://api.aflink.dev/v1/events con Content-Type: application/json y:

CabeceraValor
AuthorizationBearer <clave de API>
Idempotency-KeyClave determinística del evento, ej.: subscription.started:in_1Q2w3E (8–255).
X-Aflink-TimestampUnix en segundos (10 dígitos). Reloj con hasta 5 min de diferencia.
X-Aflink-NonceValor aleatorio nuevo en cada intento, 16–128 caracteres [A-Za-z0-9_-].
X-Aflink-Signaturev1= + hex de HMAC-SHA256(secreto de firma, timestamp + "." + nonce + "." + cuerpo).

La firma cubre los bytes exactos del cuerpo enviado. El cuerpo tiene un límite de 64 KB. Las llamadas desde un navegador (con Cookie u Origin) se rechazan.

Node.js 18+

import crypto from "node:crypto";

export async function sendAflinkEvent(idempotencyKey, body) {
  // body: cadena JSON exacta guardada en el outbox (no la serialices de nuevo).
  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. Entrega sin perder datos y sin cargar tu sistema

Nunca llames a Aflink dentro de la solicitud de pago. Usa un outbox:

  1. En la misma transacción que guarda el pago en tu base de datos, guarda el evento en una tabla de salida con la Idempotency-Key y el cuerpo JSON exacto que se enviará.
  2. Un proceso en segundo plano envía los pendientes. Cada intento genera timestamp, nonce y firma nuevos, pero mantiene la misma clave y el mismo cuerpo.
  3. Márcalo como entregado al recibir 202 (aceptado) o 200 (ya recibido).
CREATE TABLE aflink_outbox (
  idempotency_key text PRIMARY KEY,
  body            text NOT NULL,          -- bytes exactos; nunca volver a serializar
  attempts        int  NOT NULL DEFAULT 0,
  next_attempt_at timestamptz NOT NULL DEFAULT now(),
  delivered_at    timestamptz,
  last_error      text
);
-- proceso: SELECT … WHERE delivered_at IS NULL AND next_attempt_at <= now()
--          ORDER BY next_attempt_at LIMIT 50 FOR UPDATE SKIP LOCKED

Reenviar siempre es seguro: la misma clave con el mismo cuerpo se deduplica, y el mismo externalPaymentId nunca genera dos comisiones.

RespuestaQué hacer
202 o 200Entregado. El procesamiento continúa en Aflink.
409 request_replayNonce repetido. Reintenta con un nonce nuevo.
429 rate_limitedEspera el Retry-After (60 s). Límite: 300 eventos/min por origen.
5xx, timeout o error de redReintenta con espera creciente (1, 5, 15, 60 min… hasta 3 días).
400 invalid_integration_headersCorrige el reloj, el nonce o la clave de idempotencia y reenvía.
400 invalid_eventDatos inválidos. No repitas; corrige el evento y alerta.
401 invalid_credential / invalid_signatureClave revocada o secreto incorrecto. Pausa el envío y alerta.
409 idempotency_conflictMisma clave con otro cuerpo: error en tu integración. No repitas.
413 event_too_largeCuerpo de más de 64 KB. Elimina metadata innecesario.

5. Prueba y pasa a producción

  1. Configura la credencial TEST y envía un subscription.started con un token real (haz clic en un enlace de prueba de tu programa). Los eventos TEST nunca generan comisión.
  2. En Integraciones → Recibos de eventos, revisa el estado:
    • PROCESSED: todo correcto.
    • FAILED con processing_requirements_not_met: el evento no corresponde a una atribución válida. Revisa el token, el plazo de atribución, la moneda, que el programa esté activo y que el afiliado esté aprobado.
    • RECEIVED con original_payment_pending: el reembolso llegó antes del pago original; Aflink reintenta durante unos 10 minutos.
  3. Cambia a la credencial LIVE.

Lista de verificación de producción

Programas por meses de servicio (avanzado)

Si el programa paga comisión por meses de servicio (por ejemplo, durante el primer año), cada evento de suscripción necesita serviceWindow:

"serviceWindow": {
  "start": "2026-10-22T14:30:00Z",
  "end": "2027-10-22T14:30:00Z",
  "components": [
    { "id": "plan-anual", "start": "2026-10-22T14:30:00Z", "end": "2027-10-22T14:30:00Z", "amountMinor": 561600 }
  ]
}

Prefiere el modelo de pagos mensuales cuando no necesites esta precisión.

Referencia

Ir a Aflink