El catálogo de planes, afuera de tu app

Una API key por producto. No una por cuenta.

La clave ya sabe de qué empresa y de qué producto es. Tu app le pide los planes y los suscriptores de ese producto, y no alcanza nada más — ni siquiera el producto que tenés al lado.

La clave nace con el producto y se muestra una sola vez. Guardamos un hash SHA-256 y los 12 caracteres del prefijo.

En cuatro líneas

Lo que hay acá, sin scrollear.

  • Una API key por producto, no una por cuenta: nace junto con el producto, se muestra una sola vez y sólo alcanza a ese producto.
  • Cinco rutas bajo /api/partner —cuatro de lectura y el PUT que reporta el uso—. Ni la URL ni el cuerpo llevan un id de empresa o de producto: los dos los resuelve la clave.
  • Cuatro modelos de precio validados —flat, por tramos, por volumen y graduado— con el mismo esquema en la API, en el dominio y al releer la base.
  • Los días de prueba bajan solos y la prueba se termina en su fecha. El cobro recurrente corre por MercadoPago sobre la cuenta de cada empresa: Membresía no retiene fondos ni emite comprobantes fiscales.
Cómo se empieza

Por qué lo pagarías

Cuatro razones, y las cuatro se pueden verificar

Ninguna es una promesa: cada una es un comportamiento que podés comprobar en cinco minutos con tu propia cuenta. Las secciones de abajo son la evidencia de estas cuatro.

La clave que le pasás a alguien no llega a tu otro producto

Cuando integrás con un tercero —un contratista, un equipo de al lado, otra app tuya— le entregás una clave que sólo ve un producto. No es una regla que alguien tenga que respetar del otro lado del cable: la empresa y el producto los resuelve el sistema a partir de la propia clave, y no hay ningún campo del contrato donde el que llama pueda pedir entrar a otra parte.

Pedir una suscripción del producto de al lado —tuyo, de tu misma empresa— devuelve exactamente lo mismo que pedir una de una empresa ajena. No se puede distinguir «no existe» de «no es tuyo», y por eso la respuesta no filtra nada.

Un permiso que no se puede pedir es un permiso que nadie olvida revisar.

La prueba gratis se apaga sola

Los días de prueba se cuentan solos: una tarea diaria baja un día en todas las empresas a la vez, y al que se le terminó le queda el estado en payment_pending para que tu app lo vea. Nadie tiene que acordarse.

Y si un operador edita la suscripción a mano, el contador se pone en cero igual. La regla es más fuerte que la tarea.

Un precio mal armado no entra por ningún lado

El mismo esquema revisa lo que llega por la API, lo que se guarda y lo que vuelve a leerse de la base. Un tramo que pisa al anterior, dos tramos abiertos o un importe con tres decimales se rechazan en el momento.

Incluso una fila editada a mano se rechaza al leerla. No queda esperando a que alguien la encuentre en producción.

Lo que no vas a construir

Las tablas de productos, planes, clientes y suscripciones. El panel para mirarlas. La tarea diaria. El sistema de credenciales. Los guardas que impiden borrar un plan que alguien está usando. Nada de eso es tu producto, y todo eso hay que mantenerlo.

Acá ya está escrito, y con tests que lo prueban.

Lo que sigue siendo tuyo

Membresía no retiene la plata en ningún momento: el cobro recurrente corre sobre la cuenta de MercadoPago de cada empresa, y la tuya la conectás vos —o no la conectás, si sólo querés el catálogo y los planes gratis. Tampoco frena a nadie por pasarse del límite: te da el número consumido y el tope, y la decisión la toma tu app, que es la que conoce a su usuario.

Es un catálogo, y la frontera está escrita en cada sección que la toca.

001 · Autenticación

La credencial es el alcance.

No mandás un id de empresa ni un id de producto. La clave que viaja en la cabecera ya resuelve las dos cosas, y lo que la clave no resuelve tu request no lo puede pedir.

Dos formas, misma clave
Authorization: Bearer sub_a91f4c2e… # se lee primero X-API-Key: sub_a91f4c2e… # alternativa equivalente

Si la cabecera llega duplicada no se usa ninguna de las dos: sólo se confía en un valor único.

401 Clave ausente, desconocida o rotada
{ "errorCode": "AUTH_TOKEN_INVALID", "category": "AUTHENTICATION" }

Sin clave, con una clave inexistente o con una clave ya rotada: la misma respuesta. No hay nada que enumerar.

Recreación del modal de secreto que aparece al crear un producto.

sub_a91f4c2e••••••••••••

Así se ve el único momento en que la clave existe en claro

Aparece cuando creás el producto, con el botón para copiarla, y no vuelve. Después de cerrar ese modal la lista te muestra sub_a91f4c2e y nada más — que es suficiente para identificarla y no alcanza para usarla.

Perder la clave no es un problema de recuperación, es un request de rotación. Es la misma propiedad, mirada del otro lado.

  1. 01

    Nace con el producto

    No hay una pantalla de «generá una API key» ni un endpoint para crearla. POST /api/products devuelve la clave en claro en el mismo response que crea el producto, y ese es el único momento en que existe en texto plano.

  2. 02

    Se muestra una sola vez

    De la clave se guardan el digest SHA-256 y los primeros 12 caracteres, para poder mostrarte sub_a91f4c2e en la lista. El resto no queda en ningún lado, así que no se puede recuperar: se rota.

  3. 03

    Sólo se rota

    POST /api/developers/me/api-keys/:productId/regenerate, con la sesión del dashboard —nunca con la clave misma—. Rota la misma fila: la anterior deja de autenticar en el acto.

  4. 04

    SHA-256, y por qué

    Una API key son 32 bytes aleatorios detrás del prefijo sub_, no una contraseña que eligió una persona. Con esa entropía el modelo de amenaza de argon2 no aplica, y un digest indexable da la búsqueda en O(1) que hace falta en cada request. Es una decisión, no un atajo.

002 · La referencia

Cinco rutas. No hay una sexta.

Todo lo que tu aplicación necesita leer cabe en cinco líneas de tabla. Ninguna recibe un id de empresa ni un id de producto: los dos ya vinieron en la clave.

Método Ruta Qué devuelve
GET /api/partner/plans Los planes con estado active del producto de la clave, con su modelo de precio y su usageLimit. Sobre { data, total }.
GET /api/partner/products El producto de la clave, y sólo ese. Mismo sobre, con un elemento — o con ninguno si el producto ya no existe.
GET /api/partner/subscriptions Los suscriptores de ese producto, con los nombres de cliente y plan ya resueltos. Query: limit (1–100, por defecto 50), offset, status y planId.
GET /api/partner/subscriptions/:externalId Una suscripción por su externalId, el UUID estable que te sirve de handle. 404 si no es de tu producto.
PUT /api/partner/subscriptions/:externalId/usage Fija el contador de uso. Devuelve { externalId, usage, usageLimit }.

Tres cosas que conviene saber antes

  • El sobre es siempre el mismo

    Las tres listas devuelven { data, total }, con total contando el conjunto completo y no la página.

  • Los nombres vienen resueltos

    clientName, productName y planName se resuelven en una sola consulta por página, así que no tenés que pedir cada uno por separado. Si la entidad ya no existe, el campo viene en null

  • El handle es externalId

    Un UUID inmutable y único que se genera con la suscripción. Guardalo de tu lado: es con lo que la leés y con lo que reportás su uso.

003 · El precio

El modelo de precio es un dato validado.

Un plan pago siempre cobra un importe fijo. El esquema admite además tres modelos por tramos, que se guardan y se devuelven por la API pero no se cobran. El mismo esquema revisa lo que llega por la API, lo que se guarda y lo que vuelve a leerse de la base.

flat

Un importe y listo

Un solo monto por período, sin importar la cantidad. El caso más común y el más aburrido, que es exactamente lo que se espera de él.

tiered

Cada tramo, a su precio

La cantidad se reparte entre los tramos y cada porción se cuenta al precio de su propio tramo. Las primeras mil unidades a 12, las siguientes a 9, el resto a 6.

volume

Todo, al tramo donde cae

El total entero se cuenta al precio del único tramo en el que aterriza. Mismas filas que arriba, resultado distinto: 4.000 unidades van todas a 9.

graduated

Una base con unidades incluidas

Un monto base cubre N unidades y a partir de ahí se suma por unidad. Es el que usa casi todo el que empieza a medir consumo.

Lo que guarda el esquema

Los cuatro modelos existen en el dato; el panel arma planes de importe fijo

Desde el panel, un plan pago siempre es de importe fijo (flat). Los otros tres modelos se validan con el mismo esquema y la API los devuelve tal cual, pero no se cobran: si tu app los usa, el cálculo lo hace ella.

Y después, medir el uso

Lo que un esquema de tramos tiene que cumplir

  • Al menos una fila. Un esquema vacío no es un esquema.
  • Exactamente una fila abierta (upTo: null), y tiene que ser la última.
  • Los upTo finitos son enteros positivos y estrictamente crecientes: un 0 o un tramo que pisa al anterior no entra.
  • Los montos son finitos, no negativos y con hasta dos decimales.
  • includedUnits es un entero no negativo.

Lo que no cumple no se guarda: vuelve un 400 con el motivo. No queda una fila rara esperando a que alguien la descubra en producción.

Dos decisiones que conviene tener claras

Sin moneda

Los montos del modelo son números pelados: la moneda no vive en el plan. El cobro por MercadoPago se hace en pesos argentinos, una sola moneda por despliegue.

Sin cálculo

Membresía guarda el modelo, lo valida y te lo devuelve tal cual. No evalúa una cantidad contra el esquema para producir un importe. Eso lo hace tu app, que es la que conoce su propio contexto. Está escrito acá porque es una frontera, no un olvido.

004 · Reportar uso

Mandás el total, no el incremento.

El contador es tuyo: vos sabés cuántas unidades consumió el suscriptor y acá se guarda ese número. Como es un total y no un delta, repetir el mismo request deja el mismo valor — se puede reintentar sin miedo.

  • Se puede reintentar

    usage es un entero entre 0 y 1.000.000.000 y reemplaza el valor guardado. Dos requests iguales dejan el sistema en el mismo estado, así que un reintento no infla nada.

  • Primero verifica, después escribe

    Antes de tocar el contador se comprueba que la suscripción sea de tu producto. Si no lo es, devuelve 404 y no escribe nada: no hay mutación parcial.

  • El límite es dato, no puerta

    La respuesta te devuelve el usageLimit del plan (o null si el plan no mide) para que no tengas que ir a buscarlo. Membresía no bloquea a nadie por pasarse: decide tu app, que es la que sabe qué hacer con su propio usuario.

  • No se reinicia por período

    usage es un contador acumulado desde siempre. No existe un corte mensual que lo vuelva a cero: si tu ciclo lo necesita, el corte lo llevás vos y escribís el total que corresponda.

005 · La prueba

Tu otro producto tampoco existe.

Los dos requests que siguen son idénticos: misma ruta, mismo externalId. Lo único que cambia es la clave. La segunda pertenece a otro producto de la misma empresa y aun así no alcanza el dato.

01

Clave del producto A

Request con la clave del producto A: un GET a la suscripción por externalId con la cabecera Authorization Bearer sub guion bajo a91f4c2e.

02

Clave del producto B misma empresa

El mismo request, cambiando únicamente la clave por la de otro producto de la misma empresa.

03

404

Respuesta 404 Not Found con errorCode RESOURCE_NOT_FOUND, categoría NOT_FOUND y el externalId en details. La suscripción no aparece.

GET /subscriptions/:externalId # Clave del producto A 200 OK { "productName": "Analytics", "status": "active" }
GET /subscriptions/:externalId # Clave del producto B Authorization: Bearer sub_…(B)
404 Not Found { "errorCode": "RESOURCE_NOT_FOUND" }

Ver las tres llamadas

El alcance sale de la credencial

La clave se busca por su hash y de ahí salen la empresa y el producto. No hay ningún lugar del contrato donde el que llama pueda decir a qué producto quiere entrar.

Las listas también quedan atadas

GET /api/partner/plans y GET /api/partner/subscriptions fijan el producto de la clave del lado del servidor, antes de consultar. No es un filtro que se pueda pisar por query: es parte de la consulta.

Ajeno y vecino se ven igual

Una suscripción de otra empresa y una de tu propio producto de al lado devuelven exactamente la misma respuesta. No se puede distinguir «no existe» de «no es tuyo», que es justamente lo que hace que la respuesta no filtre nada.

Antes de integrar

Preguntas frecuentes: cinco de las ocho se contestan que no.

Están acá porque la respuesta corta te ahorra media hora de lectura, y porque una frontera dicha a tiempo vale más que una función prometida.

¿Qué necesita mi aplicación para leer el catálogo?

Una cabecera. Con Authorization: Bearer y la clave del producto, tu app consulta las cinco rutas bajo /api/partner: los planes activos, el producto, la lista de suscriptores, uno solo por externalId y el PUT que reporta el uso. Ni la URL ni el cuerpo llevan un id de empresa ni de producto, porque los dos los resuelve la propia clave. Esa clave nace junto con el producto y se muestra una sola vez: no hay una pantalla intermedia para generarla.

¿La API key de un producto puede leer el producto de al lado?

No. Pedir una suscripción de otro producto —aunque sea de tu misma empresa— devuelve 404, no 403: para esa clave el dato no existe, exactamente igual que si fuera de una empresa ajena. La empresa y el producto salen del hash de la credencial, y no hay ningún campo del contrato donde el que llama pueda pedir otro alcance.

¿Membresía cobra las suscripciones de mis clientes?

Sí, si la empresa conecta su propia cuenta de MercadoPago: un plan pago se cobra por débito recurrente y la plata se acredita en esa cuenta —Membresía no retiene fondos ni emite comprobantes fiscales. La API de partner no interviene en el cobro: lee planes, suscriptores y uso, y el estado de cada suscripción (active, payment_pending) es lo que tu app ve.

¿Calcula cuánto tengo que cobrarle a cada suscriptor a partir del modelo de precio?

No. Los cuatro modelos —flat, por tramos, por volumen y graduado— se guardan, se validan y se devuelven tal cual, pero nada evalúa una cantidad contra el esquema para producir un importe. Ese cálculo lo hace tu app. Los montos del modelo no llevan moneda; el cobro por MercadoPago se hace en pesos argentinos.

¿Qué pasa cuando se terminan los días de prueba?

Una tarea diaria baja un día en todas las empresas a la vez, y la suscripción que llega a cero queda en payment_pending. Nadie cobra nada: Membresía le avisa por mail al suscriptor antes y al terminar la prueba, y a la empresa de quien no activó; a tu app no le llega nada, el cambio lo ves leyendo el estado. Y si un operador edita la suscripción a mano, el contador se pone en cero igual — la regla vive en el dominio, no sólo en la tarea.

¿Bloquea a un suscriptor que se pasó del límite de uso?

No, y es una decisión escrita. La respuesta te devuelve el uso reportado y el usageLimit del plan (o null si el plan no mide) y ahí termina: bloquear, avisar o dejar pasar lo decide tu app, que es la que conoce a su usuario. El contador tampoco se reinicia por período: es acumulado desde siempre, así que si tu ciclo necesita un corte, el corte lo llevás vos y escribís el total que corresponda.

¿Hay webhooks para enterarme de un cambio?

No hay webhooks hacia tu app. Tu app se entera leyendo: pide la suscripción por externalId cuando la necesita y ve el estado y el uso del momento. Membresía sí le manda mails al suscriptor y a la empresa —alta, prueba por terminar, cuota cobrada, cuota rechazada—, pero ninguno le llega a tu app: el estado lo leés vos.

¿Puedo invitar a mi equipo?

Todavía no. Hay un usuario dueño por empresa, no hay invitaciones y no hay roles: la sesión no lleva rol y ninguna pantalla está restringida. Si tu operación necesita varias personas con permisos distintos, hoy no está.

El camino entero

Tres llamadas hasta tu primer request autenticado.

No hay una pantalla intermedia para generar una credencial, porque no hace falta: el producto nace con la suya. El paso que casi siempre falta acá, acá no existe.

  1. 01

    Tu empresa

    La empresa y su usuario dueño se crean en una sola transacción. Si el mail o el nombre ya existen, te lo dice con precisión en vez de con un error genérico.

    POST /api/auth/register
  2. 02

    Tu producto

    El response del alta trae la clave sub_ en claro. Es la única vez que existe en texto plano: copiala al gestor de secretos de tu app en ese momento.

    POST /api/products
  3. 03

    Tu app leyendo

    Con esa clave, y sin nombrar ninguna empresa ni ningún producto, tu aplicación ya lee el catálogo de ese producto. De acá en adelante no vuelve a pasar por el panel.

    GET /api/partner/plans
De cero a integrable
POST /api/auth/register # 1 · la empresa y su usuario dueño, en una sola transacción POST /api/products # 2 · el producto → el response trae la clave sub_ en claro, una vez GET /api/partner/plans # 3 · tu app lee el catálogo de ESE producto

Los pasos 1 y 2 usan la sesión del dashboard. El 3 en adelante, la clave.

Membresía es el catálogo de planes y suscriptores que tu aplicación lee, con una API key por producto. El cobro recurrente corre por MercadoPago sobre la cuenta de cada empresa; Membresía no retiene fondos, no emite comprobantes fiscales y no calcula importes a partir de los modelos por tramos. Lo que aporta es que el catálogo deje de vivir adentro de tu app, y que la credencial que le pasás a una integración no alcance nada que no le corresponda.