Documentación · API v1

Conectá tu software con el catálogo

Todo lo que tu sistema necesita para que un vendedor de la red cobre por los clientes que te trae: credenciales, los dos endpoints que llamás vos, los dos que programás vos, la firma y los errores. Con ejemplos en cURL, Node, PHP y Python.

Empezar en 4 pasos

Igual que con cualquier API: te registrás, se habilita, copiás tus credenciales e integrás. No hay que pedir acceso a nadie.

  1. Creá tu cuenta de developer En app.g360ia.com.ar, con tu cuenta de Google.
  2. Publicá tu software Cargás la ficha y los planes. Al publicarlo se habilita la API para ese producto y se generan sus credenciales.
  3. Copiá tus credenciales En el panel, Mis softwares → Conectar API: una clave y un secreto, propios de cada producto. Van a variables de entorno, nunca al código.
  4. Integrá y probá Seguí esta documentación (o pasale el pedido armado a tu IA) y apretá Probar API en el panel: te dice qué anda y qué falta.

URL base de la API: https://app.g360ia.com.ar/api/v1. Todo viaja por HTTPS y en JSON.

Cómo funciona

Un vendedor de la red le pasa a un negocio su link. El negocio se registra en tu software, y desde ese momento los dos sistemas tienen que ponerse de acuerdo sobre qué cuenta es de qué vendedor y en qué plan está. Eso es toda la integración. Hay dos direcciones:

Quién llamaQuéCuándo
Tu software → nosotrosAvisar un altaCuando alguien se registra
Tu software → nosotrosAvisar un cambio de planCuando una cuenta contrata, sube, baja o cancela
Nosotros → tu softwareConsultar el estadoUna vez por día, por cada cuenta que trajo un vendedor
Nosotros → tu softwareWebhook de activaciónCuando cobramos por nuestro checkout, o una cuenta deja de pagar

El recorrido completo de una venta:

  1. El vendedor reparte su link. Pasa por nuestro servidor y llega a tu pantalla de registro con un identificador: ?g360ref=9wxyg3bqq1hb.
  2. Tu pantalla guarda ese identificador y, al crear la cuenta, nos avisa el alta. Te devolvemos un token que guardás pegado a esa cuenta.
  3. Cuando esa cuenta quiere pagar, tu botón de contratar la manda a nuestro checkout con su token. Cobramos y te mandamos un activar por el webhook.
  4. Si la cuenta contrata adentro de tu software, por tu propio cobro, nos avisás el cambio. Y una vez por día te preguntamos en qué plan está, por si algún aviso se perdió.

Los clientes que llegan solos, sin link de un vendedor, no pasan por nada de esto: los seguís cobrando vos, como siempre.

Credenciales

Cada software publicado tiene su propio par. Las dos se ven y se cambian en el panel, en Mis softwares → Conectar API.

CredencialEmpieza conPara quéVariable sugerida
Clavesk_Autentica lo que tu software nos manda (pasos 2 y 4).G360_CLAVE
Secretowhsec_Firma lo que nosotros te mandamos (pasos 5 y 6), para que puedas comprobar que es nuestro.G360_SECRETO

Cómo se manda la clave

En cada pedido, como Authorization: Bearer. Si tu lenguaje lo complica, también se acepta en el header X-G360-Clave.

Authorization: Bearer sk_4f1c…

Van en variables de entorno, nunca escritas en el código. Así es como se filtran: alguien las pega para probar, el archivo se commitea y el repositorio las guarda para siempre, aunque después se borre la línea.

Si se te escapó una, cambiala desde el panel. La anterior deja de servir en el momento, así que actualizá el entorno y volvé a desplegar.

1 · Guardar el referido

El link del vendedor llega a tu pantalla de registro con el parámetro g360ref. Leelo apenas carga la página y guardalo (una cookie larga o localStorage): entre el clic y el registro pueden pasar días. Al crear la cuenta lo copiás a una columna nueva, por ejemplo g360_ref.

JavaScript · en tu pantalla de registro
const p = new URLSearchParams(location.search);
const ref = p.get("g360ref");

// Validar formato: viene de la URL, o sea de cualquiera.
// Gana el primero: si ya había uno guardado, no se pisa.
if (ref && /^[a-z0-9_-]{1,60}$/.test(ref) && !localStorage.getItem("g360ref")) {
  localStorage.setItem("g360ref", ref);
}

// Al crear la cuenta, mandá localStorage.getItem("g360ref") junto al registro.
  • Se llama g360ref y no ref a propósito. Si tu software ya tiene su propio programa de referidos, ref probablemente esté ocupado. Son dos programas distintos y van en columnas distintas.
  • Gana el primero. Si llega un segundo link, no reemplaces el que ya tenías: el último que tocó un link no puede llevarse una venta que trajo otro.

2 · Avisar un alta

POST https://app.g360ia.com.ar/api/v1/altas Lo llamás vos

Justo después de crear la cuenta, desde tu servidor. Se manda siempre, venga o no con referido.

Cuerpo

CampoTipoDescripción
cuentastring · obligatorioEl id de esa cuenta en tu sistema. Estable y único: es con lo que te vamos a nombrar esa cuenta en el webhook.
negociostring · obligatorioEl nombre con el que se registró.
refstring o nullEl g360ref que guardaste en el paso 1, o null si no vino.
ciudad, provinciastring · opcionalPara ubicar al negocio en el panel.
pruebaboolean · opcionalEn true hace todo el recorrido —clave, campos, referido— y no guarda nada.
cURL
curl -X POST https://app.g360ia.com.ar/api/v1/altas \
  -H "Authorization: Bearer $G360_CLAVE" \
  -H "Content-Type: application/json" \
  -d '{"cuenta":"8821","negocio":"Veterinaria Sur","ref":"9wxyg3bqq1hb"}'
Node
const r = await fetch("https://app.g360ia.com.ar/api/v1/altas", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.G360_CLAVE}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    cuenta: String(cuenta.id),
    negocio: cuenta.nombre,
    ref: cuenta.g360_ref ?? null,
  }),
});

const datos = await r.json();
if (datos.token) await guardarToken(cuenta.id, datos.token);
PHP
$ch = curl_init('https://app.g360ia.com.ar/api/v1/altas');
curl_setopt_array($ch, [
  CURLOPT_POST           => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_TIMEOUT        => 10,
  CURLOPT_HTTPHEADER     => [
    'Authorization: Bearer ' . getenv('G360_CLAVE'),
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS     => json_encode([
    'cuenta'  => (string) $cuenta->id,
    'negocio' => $cuenta->nombre,
    'ref'     => $cuenta->g360_ref,
  ]),
]);

$datos = json_decode(curl_exec($ch), true);
if (!empty($datos['token'])) guardarToken($cuenta->id, $datos['token']);
Python
import os, requests

r = requests.post(
    "https://app.g360ia.com.ar/api/v1/altas",
    headers={"Authorization": f"Bearer {os.environ['G360_CLAVE']}"},
    json={"cuenta": str(cuenta.id), "negocio": cuenta.nombre, "ref": cuenta.g360_ref},
    timeout=10,
)

datos = r.json()
if datos.get("token"):
    guardar_token(cuenta.id, datos["token"])

Respuesta

{ "ok": true, "id": 123, "nuevo": true, "conVendedor": true, "token": "9wxyg3bqq1hb", "aviso": null }
CampoDescripción
tokenGuardalo pegado a la cuenta (por ejemplo en g360_token). Es la pieza central: arma el botón de contratar, va en los avisos de cambio y es por lo que te preguntamos el estado. Puede no ser igual al ref que mandaste: guardá el de la respuesta.
conVendedorfalse (y token: null) quiere decir que ese cliente llegó solo. Es tuyo y no aparece en la plataforma.
nuevofalse si esa cuenta ya nos la habías mandado.
avisoViene cuando el ref no corresponde a ningún vendedor. Casi siempre es un bug en la captura del paso 1: logueálo.

Es idempotente por cuenta: mandarla dos veces no duplica nada y devuelve el mismo token. Por eso, si el aviso falla, no voltees el alta —el cliente ya está adentro—: logueá y reintentalo con un job cada 15 minutos hasta tener token o conVendedor: false.

3 · El botón de contratar

Tu botón de «Mejorar plan» lo ven todos tus clientes. Los que trajo un vendedor pagan por nuestro checkout; los demás, por el tuyo de siempre. No hacen falta dos pantallas: es un if sobre el token.

JavaScript
const destino = cuenta.g360_token
  ? `https://app.g360ia.com.ar/p/${cuenta.g360_token}?plan=${encodeURIComponent(plan)}`
  : miCheckoutDeSiempre;

plan es el nombre exacto de uno de los planes que cargaste en tu ficha. No mandes el precio ni el id de la cuenta: el precio lo ponemos nosotros, sacado de tu ficha, así que nadie puede pagar de menos cambiando la URL.

4 · Avisar un cambio de plan

POST https://app.g360ia.com.ar/api/v1/cambios Lo llamás vos

Desde el lugar de tu código donde el plan cambia de verdad —contratar, subir, bajar, cancelar y la baja automática por falta de pago—, sólo para cuentas con token. Las que llegaron solas no nos interesan.

CampoTipoDescripción
tokenstring · obligatorioEl g360_token de esa cuenta.
planstringEl plan en el que quedó. Obligatorio si activa es true.
activaboolean · opcionalfalse si se dio de baja o dejó de pagar. Si no viene, se toma como true.
cURL
curl -X POST https://app.g360ia.com.ar/api/v1/cambios \
  -H "Authorization: Bearer $G360_CLAVE" \
  -H "Content-Type: application/json" \
  -d '{"token":"9wxyg3bqq1hb","plan":"Pro","activa":true}'
Node
await fetch("https://app.g360ia.com.ar/api/v1/cambios", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.G360_CLAVE}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ token: cuenta.g360_token, plan: "Pro", activa: true }),
});
PHP
$ch = curl_init('https://app.g360ia.com.ar/api/v1/cambios');
curl_setopt_array($ch, [
  CURLOPT_POST           => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_TIMEOUT        => 10,
  CURLOPT_HTTPHEADER     => [
    'Authorization: Bearer ' . getenv('G360_CLAVE'),
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS     => json_encode([
    'token'  => $cuenta->g360_token,
    'plan'   => 'Pro',
    'activa' => true,
  ]),
]);
$datos = json_decode(curl_exec($ch), true);
Python
requests.post(
    "https://app.g360ia.com.ar/api/v1/cambios",
    headers={"Authorization": f"Bearer {os.environ['G360_CLAVE']}"},
    json={"token": cuenta.g360_token, "plan": "Pro", "activa": True},
    timeout=10,
)

Respuesta

{ "ok": true, "planReconocido": true, "aviso": null }

planReconocido: false quiere decir que ese nombre no coincide con ninguno de los planes de tu ficha. Es la única forma de enterarte antes de la liquidación: logueálo.

Idempotente: dos avisos del mismo cambio no cuentan doble, así que reintentar es gratis. Y como el alta, si falla no puede voltear el cambio de tu lado: logueá y reintentá.

5 · Endpoint de estado

POST https://tusitio.com/la-ruta-que-elijas Lo programás vos

Una ruta de tu backend que, dado un token, contesta en qué plan está esa cuenta. La llamamos una vez por día por cada cuenta que te trajo un vendedor. Es lo que hace que una venta hecha adentro de tu software —el que se registró gratis y contrata a los dos meses— igual le pague al vendedor que la trajo. La URL la cargás en tu ficha.

Es obligatorio para estar en el catálogo. Si deja de contestar 24 horas, el producto sale del catálogo y vuelve a mano desde el panel.

Lo que te mandamos

POST /la-ruta-que-elijas
Content-Type:       application/json
X-G360-Api-Version: 1
X-G360-Evento:      estado
X-G360-Firma:       <HMAC-SHA256 del cuerpo crudo, con tu secreto>

{ "token": "9wxyg3bqq1hb", "producto": "tu-producto" }

Lo que contestás

Situación de la cuentaRespuesta
Paga y está al día200 { "activa": true, "plan": "Pro" }
En el plan gratis o en prueba200 { "activa": true, "plan": "Free" }
Suspendida por falta de pago200 { "activa": false, "plan": "Pro" }
Dada de baja o eliminada200 { "activa": false, "plan": null }
Token que no conocés404
Firma inválida401
  • Se pregunta el plan, nunca el precio. El precio sale de los planes de tu ficha, buscado por nombre: los nombres tienen que coincidir exactamente.
  • El plan gratis se contesta igual que cualquier otro. Vale cero, así que no genera comisión, y nos dice que la cuenta sigue viva.
  • El 404 del token desconocido no es un detalle. Un endpoint que contesta activa: true a cualquier token le inventa una suscripción a cada cuenta que preguntemos.
Node · Express
app.post("/api/g360/estado", express.raw({ type: "application/json" }), async (req, res) => {
  if (!firmaValida(req.body, req.get("X-G360-Firma"))) return res.status(401).end();

  const { token } = JSON.parse(req.body);
  const cuenta = await buscarCuentaPorToken(token);
  if (!cuenta) return res.status(404).json({ error: "token desconocido" });

  res.json({ activa: cuenta.activa, plan: cuenta.plan ?? null });
});

6 · Webhook de activación

POST https://tusitio.com/la-ruta-que-elijas Lo programás vos

Cuando una cuenta paga por nuestro checkout, te pedimos que la actives. Si no cargás webhook, te avisamos por mail y la activás a mano. La URL la cargás en tu ficha.

Lo que te mandamos

POST /la-ruta-que-elijas
Content-Type:       application/json
X-G360-Api-Version: 1
X-G360-Evento:      activar
X-G360-Evento-Id:   pago-8821
X-G360-Firma:       <HMAC-SHA256 del cuerpo crudo, con tu secreto>

{
  "evento":    "activar",
  "evento_id": "pago-8821",
  "enviado":   "2026-09-26T19:40:00.000Z",
  "producto":  "tu-producto",
  "cuenta":    "8821",
  "negocio":   "Veterinaria Sur",
  "plan":      "Pro",
  "periodo":   "mensual",
  "pago":      "8821"
}
EventoQué hacer
activarPoner esa cuenta en ese plan, o renovarle el período si ya lo tenía. Trae plan, periodo (mensual o anual) y pago.
pausarDejó pasar el vencimiento de lo que pagó por nuestro checkout. Vos decidís qué hacer con el acceso. Si vuelve a pagar, llega un activar normal.
pingContestar 200 sin tocar nada. Es la prueba del panel.
reactivar, bajaParte del contrato, todavía no se emiten. Contemplalos: el día que existan llegan sin avisar.

Las reglas

  1. Verificá la firma antes de hacer nada (cómo). Si no coincide: 401 y no tocás datos.
  2. Deduplicá por evento_id. Reintentamos hasta recibir un 200, con el mismo evento_id. Guardalo con un índice único en la misma transacción que el cambio: si ya estaba, contestá 200 y no hagas nada. No uses enviado: cambia en cada intento.
  3. Si trae "prueba": true, contestá 200 y no modifiques nada, sea cual sea el evento. Y no guardes su evento_id.
  4. Lo que no podés aplicar no mejora reintentándolo: cuenta que no existe, plan que no vendés o evento desconocido → 200 con el motivo, y no apliques nada. Los únicos que no son 200: firma inválida (401) y cuerpo que no es JSON (400).
  5. Ignorá los campos que no conozcas. Dentro de la versión 1 podemos sumar campos nuevos; nunca sacamos ni cambiamos los que están.
Node · Express
app.post("/api/g360/webhook", express.raw({ type: "application/json" }), async (req, res) => {
  if (!firmaValida(req.body, req.get("X-G360-Firma"))) return res.status(401).end();

  let ev;
  try { ev = JSON.parse(req.body); } catch { return res.status(400).end(); }

  if (ev.prueba || ev.evento === "ping") return res.json({ ok: true });

  // Una sola transacción: registrar evento_id (índice único) y aplicar el cambio.
  const aplicado = await db.transaction(async (tx) => {
    const nuevo = await tx.registrarEventoSiNoExiste(ev.evento_id);
    if (!nuevo) return false;         // ya lo habíamos aplicado
    await aplicarEvento(tx, ev);      // activar / pausar / reactivar / baja
    return true;
  });

  res.json({ ok: true, repetido: !aplicado });
});

Verificar la firma

Todo lo que te mandamos (pasos 5 y 6) viene con X-G360-Firma: un HMAC-SHA256, en hexadecimal, del cuerpo crudo con tu secreto. Es lo que impide que cualquiera que adivine tu URL se active cuentas gratis.

Calculala sobre los bytes tal como llegaron, antes de parsear. Si primero parseás el JSON y después lo volvés a serializar, no coincide nunca. Y comparala en tiempo constante.

Node
import crypto from "node:crypto";

// cuerpoCrudo: el body tal como llegó (Buffer o string), sin parsear.
export function firmaValida(cuerpoCrudo, firma) {
  const esperada = crypto
    .createHmac("sha256", process.env.G360_SECRETO)
    .update(cuerpoCrudo)
    .digest("hex");
  const a = Buffer.from(esperada);
  const b = Buffer.from(String(firma || ""));
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
PHP
$crudo    = file_get_contents('php://input');
$firma    = $_SERVER['HTTP_X_G360_FIRMA'] ?? '';
$esperada = hash_hmac('sha256', $crudo, getenv('G360_SECRETO'));

if (!hash_equals($esperada, $firma)) {
  http_response_code(401);
  exit;
}

$evento = json_decode($crudo, true);
Python
import hmac, hashlib, os
from flask import request, abort

crudo = request.get_data()  # bytes, sin parsear
esperada = hmac.new(os.environ["G360_SECRETO"].encode(), crudo, hashlib.sha256).hexdigest()

if not hmac.compare_digest(esperada, request.headers.get("X-G360-Firma", "")):
    abort(401)

evento = request.get_json()

Esperamos tu respuesta hasta 10 segundos. Tus dos rutas tienen que estar publicadas en internet: un localhost no sirve. Para probar antes de desplegar, exponelas con ngrok o similar.

Errores

Los endpoints que llamás vos (pasos 2 y 4) contestan los errores siempre con la misma forma. Tu código compara el codigo, no el texto: el texto puede cambiar de redacción, el código no cambia dentro de la versión 1.

{ "ok": false, "codigo": "clave_invalida", "error": "Clave inválida." }
HTTPcodigoQué significa
401clave_invalidaLa clave no sirve, o la cambiaste y el entorno tiene la vieja.
400json_invalidoEl cuerpo no es JSON.
400falta_campoFalta un campo obligatorio. El error dice cuál.
404token_desconocidoEse token no es de ninguna cuenta de este producto (paso 4).
500error_internoFalló de nuestro lado. Reintentá: los dos endpoints son idempotentes.

Probar la API

En el panel, Mis softwares → Conectar API → Probar API. Un solo botón que recorre la integración entera y te dice qué anda y qué falta:

  • Un alta de prueba con tu clave, que no guarda nada.
  • Un ping a tu webhook, firmado igual que un cobro.
  • Un activar con prueba: true a la cuenta prueba-g360, con uno de tus planes.
  • Tu endpoint de estado, por una cuenta real y por un token inventado (tiene que dar 404).
  • Si ya nos llegó un alta de verdad y un aviso de cambio de plan.

Los dos últimos no se pueden disparar desde un botón: registrá una cuenta entrando con ?g360ref= y cambiale el plan desde tu software. Después de la prueba, en tu base no tiene que haber cambiado nada: si algo se activó, tu código no está respetando prueba: true.

Integrarla con IA

Si programás con Claude, Cursor o un asistente parecido, en el panel está esta misma documentación escrita como pedido, con tus planes y tus credenciales ya adentro: Conectar API → Documentación de la API → Que lo haga la IA. Le agregás tu stack, lo pegás y te deja la integración hecha. Después la verificás con Probar API.

Tené en cuenta que ese pedido lleva tus credenciales: queda en el historial de la herramienta, igual que si pegaras un .env. Si preferís, borralas antes de enviarlo y cargalas a mano.

Versiones

La versión va en la URL (/api/v1) y en el header X-G360-Api-Version, que viaja en las dos direcciones. Dentro de una versión sólo sumamos cosas: campos nuevos, eventos nuevos. Cualquier cambio que rompa una integración va en una versión nueva, y la anterior sigue andando.

FechaCambio
26/09/2026Sale la v1: /api/v1/altas y /api/v1/cambios, errores con codigo y header de versión. Las rutas sin versión (/api/altas, /api/cambios) siguen andando para quien ya estaba integrado.
¿Todavía no publicaste?

Las credenciales salen al publicar tu software

Te registrás con Google, cargás la ficha y los planes, y la API queda habilitada para ese producto.