# SDK de servidor Node.js

URL: https://traqio.app/es/docs/sdks/node
Last updated: 2026-10-16

Reporta registros y compras desde un backend Node.js con @traqio/node y vincula cada uno a su clic en el anuncio. Express, Next.js, NestJS, Fastify.

`@traqio/node` es el SDK de Traqio para backends Node.js — la mitad servidor del SDK web de Traqio. La conversión que cuenta en un sitio web — un registro, una suscripción, una compra — ocurre en tu backend, donde se conoce el id de usuario. Este paquete la reporta con el **trackID**, el click id del anuncio que [@traqio/web](https://traqio.app/es/docs/sdks/web) guardó en almacenamiento first-party desde la página de llegada, y Traqio acredita la campaña que trajo al usuario — [de forma determinista](https://traqio.app/es/docs/concepts/web-attribution), por el id de ese clic, nunca por una suposición. Sin dependencias; Express, Next.js, NestJS, Fastify o `node:http` simple.

## Requisitos

- Node.js 18 o posterior (el SDK usa el `fetch` integrado). ESM y CommonJS, con tipos TypeScript.
- Una **clave de servidor** para el producto (`tq_sk_…`). Ni la clave SDK de la app ni una clave web funcionan aquí.
- [@traqio/web](https://traqio.app/es/docs/sdks/web) en el sitio, y anuncios que apunten a [enlaces de seguimiento](https://traqio.app/es/docs/concepts/tracking-links) con un destino web — así es como el trackID llega a tu formulario de registro.

## Configuración

### Crear una clave de servidor

En la consola, abre **Integraciones → SDK** y crea una **clave de servidor**. Se muestra una sola vez: ponla en el entorno del backend, nunca en el código ni en una página. Traqio solo guarda su huella; una clave perdida se sustituye por una nueva y la antigua se revoca.

.env:
```bash
TRAQIO_SECRET_KEY=tq_sk_...
```

### Instalar el paquete

```bash
npm install @traqio/node
```

### Crear el cliente

```ts
import { Traqio } from "@traqio/node";

// One client per process. The key comes from the server's environment, never from code.
export const traqio = new Traqio({ secretKey: process.env.TRAQIO_SECRET_KEY! });
```

El cliente no mantiene ninguna conexión: basta con uno por proceso. Rechaza una clave web (`tq_web_…`) y se niega a arrancar en un navegador.

### Reportar el registro con su trackID

Llama a `signup()` una vez, donde se crea la cuenta. @traqio/web escribe el trackID en la cookie `traq_click` de tu propio dominio, así que el navegador lo envía con la petición de registro:

Express:
```ts
import { trackIdFromRequest } from "@traqio/node";
import { traqio } from "./traqio";

app.post("/signup", async (req, res) => {
  const user = await users.create(req.body);

  // The trackID arrives with the request: @traqio/web set the traq_click cookie.
  const result = await traqio
    .signup({ userId: user.id, trackId: trackIdFromRequest(req) })
    .catch((err) => logger.warn(err)); // never fail the sign-up itself

  res.status(201).json(user);
});
```

Next.js:
```ts
// app/api/signup/route.ts (Next.js App Router)
import { cookies } from "next/headers";
import { TRACK_ID_COOKIE } from "@traqio/node";
import { traqio } from "@/lib/traqio";

export async function POST(request: Request) {
  const user = await users.create(await request.json());
  const trackId = (await cookies()).get(TRACK_ID_COOKIE)?.value ?? null;
  await traqio.signup({ userId: user.id, trackId }).catch(console.warn);
  return Response.json(user, { status: 201 });
}
```

NestJS:
```ts
// users.service.ts (NestJS)
import { Injectable, Logger } from "@nestjs/common";
import { Traqio } from "@traqio/node";

@Injectable()
export class UsersService {
  private readonly traqio = new Traqio({ secretKey: process.env.TRAQIO_SECRET_KEY! });
  private readonly log = new Logger(UsersService.name);

  async register(dto: RegisterDto, trackId: string | null): Promise<User> {
    const user = await this.users.create(dto);
    await this.traqio.signup({ userId: user.id, trackId }).catch((e: Error) => this.log.warn(e.message));
    return user;
  }
}
```

Si el backend está en otro dominio que el sitio, la cookie no le llega: envía en su lugar `Traqio.getTrackId()` desde el navegador con el formulario. El registro es el momento en que el usuario es **captado**; el último clic anterior, dentro de tu [ventana de atribución](https://traqio.app/es/docs/concepts/attribution-windows), se lleva el crédito.

### Registrar las conversiones

```ts
await traqio.track({
  userId: user.id,
  name: "purchase",
  value: 49,
  currency: "EUR",
  transactionId: paymentIntent.id, // one payment, one row — whoever reports it
  store: "stripe",
});
```

Las conversiones siguen al usuario: se acreditan a la campaña que lo captó. Se pueden enviar varios eventos en una sola llamada:

```ts
await traqio.send({
  userId: user.id,
  events: [
    { name: "trial_started" },
    { name: "purchase", value: 49, currency: "EUR", transactionId: invoice.id, store: "stripe" },
  ],
});
```

## Qué te dice la respuesta

```ts
const result = await traqio.signup({ userId: user.id, trackId });

result.trackId;     // "matched" | "unknown" | "expired" | "claimed" | "refused" (absent without a trackID)
result.acquired;    // true: the sign-up is recorded
result.attribution; // { type: "deterministic", network: "meta", campaignId: "…", adsetId, adId }
```

| trackId | Significado |
| --- | --- |
| matched | El clic pertenece ahora a este usuario. Un reintento vuelve a responder matched. |
| unknown | Ningún clic de este producto tiene ese id — una errata, un enlace de otro producto o un valor que no escribió @traqio/web. |
| expired | El clic es más antiguo que la ventana de atribución del producto. |
| claimed | Otro usuario, o una instalación de app, ya tiene ese clic. Un clic acredita una sola identidad. |
| refused | El clic no superó una regla antifraude (pasó por una red de anonimización). Se contabiliza, nunca se acredita. |

## Qué hace hoy y qué no

|  | Hoy |
| --- | --- |
| Vincular un registro a su clic e indicar a quién se acreditó | Sí — en la respuesta |
| Registrar compras con valor, moneda e id de pago, sin duplicados | Sí |
| Dejar intactas las instalaciones, los ingresos y el ROAS de tus apps | Sí — nunca se vincula a una instalación de app |
| Mostrar las conversiones web en los informes de la consola | Sí — por campaña, junto a las instalaciones |
| Enviar las conversiones web a Meta o TikTok | Todavía no |

Las conversiones enviadas aquí aparecen en **Eventos en directo**, marcadas como no vinculadas a una instalación — lo esperado para un sitio web. Un registro acreditado a una campaña se cuenta en el informe de ROAS como una conversión web de esa campaña, junto a sus instalaciones, y la respuesta de cada llamada te da la atribución al instante.

## Errores y reintentos

Un error de red, un timeout, un `429` o un `5xx` se reintenta — dos veces por defecto, con el mismo cuerpo, así que nada se cuenta dos veces. Un `4xx` (clave errónea o revocada, payload no válido) no se reintenta. Ambos terminan en un `TraqioError`:

```ts
import { TraqioError } from "@traqio/node";

try {
  await traqio.signup({ userId: user.id, trackId });
} catch (err) {
  if (err instanceof TraqioError) {
    // err.status: 401 = wrong or revoked key, 400 = invalid payload, undefined = network
    logger.warn({ status: err.status, body: err.body }, err.message);
  }
}
```

> **Reporta después de tu propia escritura** Llama a Traqio una vez guardada la cuenta o el pago, y nunca dejes que un error de Traqio lo haga fallar.

## Referencia de la API

TraqioOptions:
```ts
interface TraqioOptions {
  secretKey: string;     // the server key, tq_sk_… — required
  endpoint?: string;     // default "https://api.traqio.app"
  timeoutMs?: number;    // per attempt — default 10000
  maxRetries?: number;   // after a network error, a 429 or a 5xx — default 2
  fetch?: typeof fetch;  // a custom fetch (proxies, tests)
}
```

| Método | Qué hace |
| --- | --- |
| signup({ userId, trackId?, events? }) | Reporta el registro — la captación — con su trackID. Se conserva el primero. |
| track({ userId, name, value?, currency?, transactionId?, store?, eventId?, trackId? }) | Registra una conversión de un usuario conocido. |
| send({ userId, trackId?, signup?, events? }) | La llamada en bruto detrás de ambos, para varios eventos a la vez. |
| trackIdFromRequest(req) | El trackID de las cookies de una petición (Express, Fastify, Koa, node:http, una Request Fetch), o null. |
| trackIdFromCookieHeader(header) | Lo mismo, a partir de una cabecera Cookie en bruto. |

> **Nivel de prueba** Con tests unitarios, y ejecutado de principio a fin — builds ESM y CommonJS — contra una API de Traqio en funcionamiento: un registro con trackID volvió matched con su campaña, una compra reportada dos veces se guardó una sola vez, una clave errónea fue rechazada. Todavía no se ha ejecutado en el backend de producción de un cliente.

## Preguntas frecuentes

**¿Por qué un SDK de servidor necesita su propia clave?**
Porque es el único autorizado a vincular una conversión a un clic en un anuncio. La clave web de @traqio/web está en tu página, donde cualquiera puede leerla, así que nunca puede atribuir nada; la clave de servidor se queda en tu backend, se envía en una cabecera Authorization y Traqio solo guarda su huella.

**¿Puedo usar @traqio/node en el navegador?**
No. Se niega a arrancar cuando detecta un documento de navegador, y rechaza una clave web. En el navegador, usa @traqio/web, que guarda el trackID hasta que la petición de registro lo lleva a tu backend.

**¿Y si el visitante no vino de un anuncio?**
Pasa un trackId null, o ninguno. El registro se guarda igualmente, y el usuario es orgánico: no se acredita ninguna campaña.

**¿Un reintento contará una compra dos veces?**
No. Cada llamada genera sus ids de evento una sola vez y reintenta exactamente el mismo cuerpo, y una compra se identifica además por su transactionId, así que el mismo pago reportado dos veces se guarda una sola vez.
