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.
- Creá tu cuenta de developer En app.g360ia.com.ar, con tu cuenta de Google.
- Publicá tu software Cargás la ficha y los planes. Al publicarlo se habilita la API para ese producto y se generan sus credenciales.
- 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.
- 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 llama | Qué | Cuándo |
|---|---|---|
| Tu software → nosotros | Avisar un alta | Cuando alguien se registra |
| Tu software → nosotros | Avisar un cambio de plan | Cuando una cuenta contrata, sube, baja o cancela |
| Nosotros → tu software | Consultar el estado | Una vez por día, por cada cuenta que trajo un vendedor |
| Nosotros → tu software | Webhook de activación | Cuando cobramos por nuestro checkout, o una cuenta deja de pagar |
El recorrido completo de una venta:
- El vendedor reparte su link. Pasa por nuestro servidor y llega a tu pantalla de registro con un identificador:
?g360ref=9wxyg3bqq1hb. - Tu pantalla guarda ese identificador y, al crear la cuenta, nos avisa el alta. Te devolvemos un
tokenque guardás pegado a esa cuenta. - Cuando esa cuenta quiere pagar, tu botón de contratar la manda a nuestro checkout con su token. Cobramos y te mandamos un
activarpor el webhook. - 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.
| Credencial | Empieza con | Para qué | Variable sugerida |
|---|---|---|---|
| Clave | sk_ | Autentica lo que tu software nos manda (pasos 2 y 4). | G360_CLAVE |
| Secreto | whsec_ | 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.
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
g360refy norefa propósito. Si tu software ya tiene su propio programa de referidos,refprobablemente 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
Justo después de crear la cuenta, desde tu servidor. Se manda siempre, venga o no con referido.
Cuerpo
| Campo | Tipo | Descripción |
|---|---|---|
cuenta | string · obligatorio | El id de esa cuenta en tu sistema. Estable y único: es con lo que te vamos a nombrar esa cuenta en el webhook. |
negocio | string · obligatorio | El nombre con el que se registró. |
ref | string o null | El g360ref que guardaste en el paso 1, o null si no vino. |
ciudad, provincia | string · opcional | Para ubicar al negocio en el panel. |
prueba | boolean · opcional | En true hace todo el recorrido —clave, campos, referido— y no guarda nada. |
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"}'
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);
$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']);
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 }| Campo | Descripción |
|---|---|
token | Guardalo 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. |
conVendedor | false (y token: null) quiere decir que ese cliente llegó solo. Es tuyo y no aparece en la plataforma. |
nuevo | false si esa cuenta ya nos la habías mandado. |
aviso | Viene 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.
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
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.
| Campo | Tipo | Descripción |
|---|---|---|
token | string · obligatorio | El g360_token de esa cuenta. |
plan | string | El plan en el que quedó. Obligatorio si activa es true. |
activa | boolean · opcional | false si se dio de baja o dejó de pagar. Si no viene, se toma como true. |
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}'
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 }),
});
$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);
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
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 cuenta | Respuesta |
|---|---|
| Paga y está al día | 200 { "activa": true, "plan": "Pro" } |
| En el plan gratis o en prueba | 200 { "activa": true, "plan": "Free" } |
| Suspendida por falta de pago | 200 { "activa": false, "plan": "Pro" } |
| Dada de baja o eliminada | 200 { "activa": false, "plan": null } |
| Token que no conocés | 404 |
| Firma inválida | 401 |
- 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: truea cualquier token le inventa una suscripción a cada cuenta que preguntemos.
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
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"
}| Evento | Qué hacer |
|---|---|
activar | Poner esa cuenta en ese plan, o renovarle el período si ya lo tenía. Trae plan, periodo (mensual o anual) y pago. |
pausar | Dejó 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. |
ping | Contestar 200 sin tocar nada. Es la prueba del panel. |
reactivar, baja | Parte del contrato, todavía no se emiten. Contemplalos: el día que existan llegan sin avisar. |
Las reglas
- Verificá la firma antes de hacer nada (cómo). Si no coincide: 401 y no tocás datos.
- Deduplicá por
evento_id. Reintentamos hasta recibir un 200, con el mismoevento_id. Guardalo con un índice único en la misma transacción que el cambio: si ya estaba, contestá 200 y no hagas nada. No usesenviado: cambia en cada intento. - Si trae
"prueba": true, contestá 200 y no modifiques nada, sea cual sea el evento. Y no guardes suevento_id. - 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).
- Ignorá los campos que no conozcas. Dentro de la versión 1 podemos sumar campos nuevos; nunca sacamos ni cambiamos los que están.
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.
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);
}
$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);
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." }| HTTP | codigo | Qué significa |
|---|---|---|
| 401 | clave_invalida | La clave no sirve, o la cambiaste y el entorno tiene la vieja. |
| 400 | json_invalido | El cuerpo no es JSON. |
| 400 | falta_campo | Falta un campo obligatorio. El error dice cuál. |
| 404 | token_desconocido | Ese token no es de ninguna cuenta de este producto (paso 4). |
| 500 | error_interno | Falló 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
pinga tu webhook, firmado igual que un cobro. - Un
activarconprueba: truea la cuentaprueba-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.
| Fecha | Cambio |
|---|---|
| 26/09/2026 | Sale 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. |
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.