Uso medido (Meters)
Cómo cobrar por consumo dejando la cuenta con Vipter, en el formato de los Billing Meters de Stripe: crear un medidor, ponerle precio al uso en la oferta o en la suscripción, enviar eventos de uso, leer el período abierto y qué pasa cuando cierra el ciclo, con ejemplos en curl y Node.js, franquicia, tramos de precio, umbral de cobro y las diferencias con Stripe.
Con el uso medido, tu sistema solo le avisa a Vipter de cada consumo: una llamada, un GB, un envío. Vipter suma, aplica la franquicia y el precio que definiste, y cobra el total en la tarjeta guardada de la suscripción cuando cierra el ciclo. Es la alternativa al cobro adicional por uso, en el que tú calculas el monto y llamas a POST /v1/subscriptions/{id}/charges.
Los endpoints siguen el formato de los Billing Meters de Stripe (billing.meter, billing.meter_event, event_summaries). Lo que cambia está en Diferencias con Stripe. Cada endpoint, con todos los parámetros, está en la referencia de la API.
Cuándo usar
| Quieres | Usa |
|---|---|
| Mantener la cuenta en tu sistema y decidir cuándo y cuánto cobrar. | Cobro en la tarjeta guardada: POST /v1/subscriptions/{id}/charges con el amount ya calculado. |
| Solo mandar los eventos y dejar que Vipter sume, aplique la franquicia, el precio y los tramos, y cobre al final del ciclo. | Esta página. |
| Cobrar en el momento, en cada consumo. | Cobro en la tarjeta guardada. El uso medido no tiene modo inmediato: acumula y cobra al cierre, o al alcanzar un umbral. |
Los dos caminos generan el mismo tipo de cobro (subscription_charge) y el mismo pedido pagado. Una suscripción puede usar los dos.
El modelo
medidor (meter) → eventos de uso (meter_event) → ítem de uso (usage_item) → período (usage_period) → cobro (subscription_charge)- Medidor. Un tipo de consumo, identificado por un
event_name(api_calls,storage_gb). Dice cómo se agregan los eventos:sumsuma los valores,countcuenta los eventos,lastse queda con el último valor. - Evento de uso. Un registro con el cliente (
payload.customer_id) y el valor (payload.value). Al llegar, Vipter vincula el evento a la suscripción activa del cliente que tiene precio para ese medidor. - Ítem de uso. El precio de un medidor en una suscripción: monto por unidad, franquicia, tramos, redondeo y umbral. Nace de la oferta (configurado en el panel) o se define por la API en la suscripción.
- Período. El ciclo actual de la suscripción (
current_period_start→current_period_end). Mientras está abierto, el uso se recalcula a partir de los eventos en cada corrida. Cuando cierra, se convierte en un cobro. - Cobro. Un cobro en la suscripción con
source: "usage", hecho una vez por período, con una línea por medidor. Se convierte en un pedido pagado conbilling_reason: "usage".
Paso 1: crear el medidor
curl -X POST https://api.vipter.com/v1/billing/meters \
-H "Authorization: Bearer vk_live_…" \
-H "Content-Type: application/json" \
-d '{
"display_name": "Llamadas de API",
"event_name": "api_calls",
"default_aggregation": { "formula": "sum" }
}'{
"id": "mtr_4f8e2c1a9b7d6e5f3a2b1c0d",
"object": "billing.meter",
"display_name": "Llamadas de API",
"event_name": "api_calls",
"default_aggregation": { "formula": "sum" },
"customer_mapping": { "type": "by_id", "event_payload_key": "customer_id" },
"value_settings": { "event_payload_key": "value" },
"status": "active",
"status_transitions": { "deactivated_at": null },
"livemode": true,
"created": 1791100800,
"updated": 1791100800
}El event_name es lo que usan los eventos para encontrar el medidor: solo minúsculas, dígitos, _, . y -, hasta 100 caracteres, único en la tienda (repetirlo recibe 400 event_name_taken). No cambia después; el display_name cambia por POST /v1/billing/meters/{id} y es el nombre que aparece en la línea del pedido del comprador. Si tu sistema ya manda eventos a Stripe con stripe_customer_id en el payload, déjalo: Vipter acepta esa clave como alias de customer_id.
Paso 2: ponerle precio al uso
El precio vive en un ítem de uso, uno por medidor. Hay dos lugares para él:
En la oferta, por el panel. En la página de la oferta, la tarjeta de uso medido lista los medidores de la tienda y acepta el precio por unidad, la franquicia, los tramos, el redondeo y el umbral, por moneda. Toda suscripción de esa oferta hereda el ítem en la primera corrida del uso medido después de ser creada (la corrida ocurre cada 10 minutos). La herencia copia la fila de la moneda de la suscripción o, si no hay, la de la moneda de la tienda. Es una copia: cambiar el precio en la oferta después no altera las suscripciones que ya heredaron.
En la suscripción, por la API. Para un precio negociado o para una suscripción que no vino de una oferta con uso medido:
curl -X POST https://api.vipter.com/v1/subscriptions/sub_3c7a9e1f5b2d8c4e/usage_items \
-H "Authorization: Bearer vk_live_…" \
-H "Content-Type: application/json" \
-d '{
"meter": "mtr_4f8e2c1a9b7d6e5f3a2b1c0d",
"unit_amount": 0.4,
"included_units": 10000,
"rounding": "up",
"label": "Llamadas más allá de la franquicia"
}'{
"id": "usi_9d2e4f6a8b1c3d5e7f0a2b4c",
"object": "usage_item",
"meter": "mtr_4f8e2c1a9b7d6e5f3a2b1c0d",
"subscription": "sub_3c7a9e1f5b2d8c4e",
"offer": null,
"currency": "brl",
"unit_amount": 0.4,
"included_units": 10000,
"tiers": null,
"rounding": "up",
"billing_threshold": null,
"label": "Llamadas más allá de la franquicia",
"source": "api",
"livemode": true,
"created": 1791100900
}unit_amount es el precio de una unidad, en la menor unidad de la moneda, con fracciones: 0.4 es R$ 0,004 por llamada; 50 es R$ 0,50 por GB. La moneda es siempre la de la suscripción (otra recibe 400 currency_mismatch). Enviar de nuevo el mismo meter reemplaza el ítem; un ítem que definiste por la API nunca es sobrescrito por la herencia de la oferta. DELETE /v1/subscriptions/{id}/usage_items/{itemId} lo quita.
Paso 3: enviar los eventos
curl -X POST https://api.vipter.com/v1/billing/meter_events \
-H "Authorization: Bearer vk_live_…" \
-H "Content-Type: application/json" \
-d '{
"event_name": "api_calls",
"identifier": "req_01J9X3K7M2",
"payload": { "customer_id": "cust_9d2e4f6a8b1c3d5e", "value": 1 }
}'{
"id": "mev_1a5c9e3b7d2f6a8c0e4b2d6f",
"object": "billing.meter_event",
"event_name": "api_calls",
"identifier": "req_01J9X3K7M2",
"payload": { "customer_id": "cust_9d2e4f6a8b1c3d5e", "value": 1 },
"customer": "cust_9d2e4f6a8b1c3d5e",
"subscription": "sub_3c7a9e1f5b2d8c4e",
"value": 1,
"timestamp": 1791101000,
"livemode": true,
"created": 1791101000
}identifieres la idempotencia del evento, por medidor. Repetir el mismoidentifierdevuelve el evento ya guardado, con200, sin contar dos veces. Usa el ID de la solicitud, del job o del registro en tu base de datos. Sinidentifier, Vipter genera uno, y cada llamada cuenta.payload.valuees el valor sumado (sum) o guardado (last). En un medidorcount, el valor se ignora y cada evento vale 1.timestampes opcional, en segundos Unix: hasta 35 días atrás y hasta 5 minutos adelante, si no400 timestamp_out_of_range. Sin él, vale la hora de llegada. Eltimestampdecide en qué período cae el evento.subscriptionen la respuesta dice a qué se vinculó el evento.nulles un evento sin suscripción.
En volumen, usa el lote: hasta 100 eventos por llamada en POST /v1/billing/meter_events/batch. Cada ítem se acepta o se rechaza por su cuenta, y results[i] responde a events[i] con status accepted, duplicate o error. La respuesta es 200 siempre que al menos un ítem entró; 400 solo cuando ninguno entró.
const API = 'https://api.vipter.com/v1';
const headers = { Authorization: `Bearer ${process.env.VIPTER_API_KEY}`, 'Content-Type': 'application/json' };
// Un evento por consumo. `identifier` es el ID del registro de tu lado: reenviar nunca cuenta dos veces.
export async function reportUsage(record) {
const res = await fetch(`${API}/billing/meter_events`, {
method: 'POST',
headers,
body: JSON.stringify({
event_name: 'api_calls',
identifier: record.id,
timestamp: Math.floor(record.at.getTime() / 1000), // opcional; hasta 35 días atrás
payload: { customer_id: record.vipterCustomerId, value: record.calls },
}),
});
const json = await res.json();
if (res.ok) return json; // 201 nuevo, 200 repetido
// 400 con error.code: no_meter_found, meter_inactive, invalid_payload, timestamp_out_of_range. 404: el cliente no existe.
throw Object.assign(new Error(json.error.message), { code: json.error.code, status: res.status });
}
// En lote: hasta 100 por llamada. Trata `results` ítem por ítem; `duplicate` es normal en un reenvío.
export async function reportUsageBatch(records) {
const res = await fetch(`${API}/billing/meter_events/batch`, {
method: 'POST',
headers,
body: JSON.stringify({
events: records.map((r) => ({ event_name: 'api_calls', identifier: r.id, payload: { customer_id: r.vipterCustomerId, value: r.calls } })),
}),
});
const json = await res.json(); // { accepted, duplicates, errors, results[] }
json.results.forEach((r, i) => {
if (r.status === 'error') console.warn('evento rechazado', records[i].id, r.error.code, r.error.message);
});
return json;
}Guarda el cust_… de cada usuario cuando nace la suscripción: viene en customer de la sesión de checkout y de la suscripción. Un customer_id que no existe en la tienda recibe 404 resource_missing.
Paso 4: leer el uso del período
curl https://api.vipter.com/v1/subscriptions/sub_3c7a9e1f5b2d8c4e/usage \
-H "Authorization: Bearer vk_live_…"{
"object": "list",
"url": "https://api.vipter.com/v1/subscriptions/sub_3c7a9e1f5b2d8c4e/usage",
"has_more": false,
"data": [
{
"id": "usp_7b3e9f1c2a8d4e6f0b2d4f6a",
"object": "usage_period",
"subscription": "sub_3c7a9e1f5b2d8c4e",
"status": "open",
"close_reason": null,
"period_start": 1790186400,
"period_end": 1792778400,
"currency": "brl",
"lines": [
{ "meter": "mtr_4f8e2c1a9b7d6e5f3a2b1c0d", "event_name": "api_calls", "quantity": 12300, "included": 10000, "billable": 2300, "unit_amount": 0.4, "amount": 920 }
],
"amount_total": 920,
"charge": null,
"computed_at": 1791101100,
"closed_at": null,
"livemode": true,
"created": 1790187000
}
]
}La lista trae los 12 períodos más recientes, del más nuevo al más antiguo. El período abierto se calcula en el momento de la llamada, a partir de los eventos; los cerrados vienen como quedaron al cierre, con charge apuntando al cobro. Cada línea es un medidor: quantity es el agregado, included la franquicia, billable lo que la superó, amount el monto de la línea en centavos. Usa esta llamada para mostrar el consumo del mes en tu producto, en vez de sumar de tu lado.
Para el agregado de un cliente en cualquier intervalo, incluso por hora o por día, usa GET /v1/billing/meters/{id}/event_summaries?customer=&start_time=&end_time=&value_grouping_window=day. Suma todos los eventos del cliente en ese medidor, vinculados o no a una suscripción.
Qué pasa al final del ciclo
Una rutina corre cada 10 minutos y, para cada suscripción con ítems de uso:
- Abre el período del ciclo actual, si todavía no existe: de
current_period_startacurrent_period_endde la suscripción. Solo las suscripcionesactive,trialingopast_duetienen período abierto. - Recalcula el período abierto a partir de los eventos y guarda
linesyamount_total. - Cierra el período cuando termina el ciclo (
close_reason: "period_end"), cuando el monto acumulado alcanza el umbral (threshold) o cuando la suscripción deja de ser cobrable (subscription_ended). - Cobra el período cerrado con
amount_totalmayor que cero, una sola vez, en la tarjeta guardada de la suscripción: un cobro en la suscripción consource: "usage",Idempotency-Keyusage:{id del período}, descripciónUso 2026-09-23 a 2026-10-23y una línea por medidor con monto, con eldisplay_namedel medidor. Un período con monto cero cierra sin cobro y sin pedido.
El cobro sigue las reglas de los cobros en la suscripción:
- Aprobado: salen
invoice.paid, con el pedido (billing_reason: "usage",external_order_idcon elsch_…), ysubscription_charge.succeeded, con el cobro (metadata.kind: "usage"ymetadata.usage_periodcon elusp_…). El período quedaclosedconchargecompletado. - Rechazado por la tarjeta: salen
subscription_charge.failedy, cuando el proveedor registró un pedido,invoice.payment_failed. Vipter no reintenta por su cuenta; el período queda cerrado conchargeapuntando al cobro rechazado. Para cobrar el monto después de que el suscriptor cambie la tarjeta, usaPOST /v1/subscriptions/{id}/chargescon elamount_totaldel período. - En análisis: el cobro queda
pendingy los eventos salen cuando el proveedor decide. - Rechazado antes de llegar a la tarjeta (suscripción sin tarjeta guardada, tarjeta en un proveedor que no acepta cobros sin el cliente, monto menor a una unidad de la moneda, tienda con la mensualidad atrasada): el período cierra con
charge: nully el uso no se cobra después. Ensubscription_endedel uso ya consumido se cobra igual en la tarjeta guardada (es la factura final de la suscripción); solo queda sin cobro si la tarjeta ya no está disponible.
La renovación de la suscripción no cambia: la mensualidad se sigue cobrando en la fecha y por el monto de la oferta, y el uso viene en un cobro separado. Cada cobro de uso aprobado cuenta como un pedido pagado en la cuota del plan Vipter de la tienda; los eventos de uso no cuentan.
Ejemplos de precio
Los cálculos de abajo valen para una línea. billable es el agregado menos included_units; el precio se aplica solo a billable. El redondeo ocurre una vez, en el total de la línea, a centavos enteros: up redondea hacia arriba (predeterminado), nearest al más cercano.
Precio fijo con franquicia
{ "meter": "mtr_…", "unit_amount": 0.4, "included_units": 10000 }12.300 llamadas: billable 2.300 × 0,4 = 920 centavos, R$ 9,20. 9.000 llamadas: billable 0, línea con amount 0.
Tramos graduados
{
"meter": "mtr_…",
"unit_amount": 0,
"included_units": 0,
"tiers": [
{ "up_to": 1000, "unit_amount": 0 },
{ "up_to": 10000, "unit_amount": 0.5 },
{ "up_to": null, "unit_amount": 0.3 }
]
}Cada tramo cubre las unidades entre el up_to anterior y el suyo, y el último tiene que tener up_to: null. Con tiers, el unit_amount del ítem no entra en el cálculo (envía 0). 25.000 unidades: 1.000 × 0 + 9.000 × 0,5 + 15.000 × 0,3 = 4.500 + 4.500 = 9.000 centavos, R$ 90,00. Un tramo puede tener flat_amount, en centavos, cobrado una vez cuando alguna unidad cae en él. Los tramos cuentan a partir de billable: con included_units: 1000, el primer tramo empieza en la unidad 1.001 del consumo.
Redondeo
unit_amount: 0.4 y 23 unidades: 9,2 centavos. Con rounding: "up", la línea vale 10 centavos; con nearest, 9. El redondeo es por línea; el total del período es la suma de las líneas ya redondeadas.
Agregaciones
sum suma value (GB transferidos). count cuenta eventos e ignora value (llamadas, envíos). last se queda con el value del evento de timestamp más reciente del período (asientos activos, GB almacenados): manda el total actual, no la diferencia.
Umbral de cobro
billing_threshold, en centavos, cierra y cobra el período antes del fin del ciclo cuando el monto acumulado lo alcanza:
{ "meter": "mtr_…", "unit_amount": 0.4, "included_units": 10000, "billing_threshold": 20000 }Cuando amount_total llega a R$ 200,00, el período cierra con close_reason: "threshold", se cobra, y un período nuevo abre desde ese momento hasta el fin del ciclo. La verificación ocurre en cada corrida, así que el cobro puede pasarse un poco del umbral. Con varios ítems en la suscripción, vale el menor umbral entre ellos, sobre el total del período. Cada cierre es un cobro y un pedido.
Eventos sin suscripción
Al llegar, el evento se vincula a la suscripción del cliente que está active, trialing o past_due y tiene un ítem de uso para el medidor; con más de una, a la más reciente. payload.subscription_id elige una de ellas; si la elegida no tiene ítem para el medidor, el evento queda sin suscripción. La vinculación se hace una vez: un evento que llegó antes de que la suscripción tuviera el ítem no se cobra después.
Un evento sin suscripción se guarda (subscription: null), aparece en event_summaries y no se cobra. Mientras esto ocurre, Vipter manda billing.meter.error_report_triggered en el catálogo 2026-11-01, como máximo una vez por medidor y por hora, con reason.error_count eventos sin suscripción en la hora actual. Las causas más comunes:
- La suscripción es nueva y todavía no heredó el ítem de la oferta: la herencia ocurre en la corrida siguiente, en hasta 10 minutos. Empieza a mandar eventos después de que
GET /v1/subscriptions/{id}/usage_itemsdevuelva el ítem, o crea el ítem por la API en el momento. - El ítem existe solo en la oferta anterior: una suscripción hereda una vez; después de un cambio de oferta, revisa los ítems.
- El
customer_ides de otro cliente, o la suscripción paga por PIX y no tiene tarjeta guardada (recibe el evento, pero el cobro falla al cierre).
Desactivar un medidor
POST /v1/billing/meters/{id}/deactivate deja de aceptar eventos (400 meter_inactive) en el momento. Los períodos abiertos siguen mostrando la quantity del medidor, pero el precio pasa a cero. Los ítems de uso que apuntan a él siguen existiendo; quítalos si ya no quieres verlos. No hay reactivación por la API.
Límites
| Qué | Límite |
|---|---|
timestamp del evento | Hasta 35 días atrás y 5 minutos adelante. |
| Eventos por lote | 1 a 100. |
payload | Hasta 20 claves; valores texto, número o booleano. |
event_name, identifier | Hasta 100 caracteres. event_name: a-z, 0-9, _, ., -. |
display_name, label | Hasta 250 y 120 caracteres. |
tiers | 1 a 20 tramos, up_to creciente, el último null. |
Intervalo de event_summaries | Hasta un año. |
GET …/usage | Los 12 períodos más recientes. |
| Llamadas | Los límites de solicitudes de la API. |
Lo que ve el comprador
El cobro del período se convierte en un pedido con la descripción Uso <inicio> a <fin> y una línea por medidor, con el display_name del medidor y el monto de la línea. El suscriptor ve el pedido en el portal del cliente, junto a las renovaciones, y recibe el correo de confirmación de compra de la tienda, cuando está activo. En el panel, el cobro aparece en la página de la suscripción con el origen Uso, y la página de la suscripción muestra el uso del período abierto.
Si tu producto muestra el consumo en tiempo real, lee GET /v1/subscriptions/{id}/usage en vez de rehacer la cuenta: es el mismo cálculo que va al cobro.
Diferencias con Stripe
- No existe precio medido (
priceconrecurring.usage_type: "metered"). El precio vive en el ítem de uso de la oferta (por el panel) o de la suscripción (/usage_items), conunit_amountfraccionario,included_units,tiers,roundingybilling_thresholden el mismo objeto. - No hay factura. El período cerrado se convierte en un cobro en la suscripción con
source: "usage", cobrado en el momento en la tarjeta guardada, y un pedido conbilling_reason: "usage". Los eventos soninvoice.paidysubscription_charge.*, noinvoice.createdniinvoice.finalized. - La franquicia es un campo.
included_unitsreemplaza el tramo gratuito; lostierscuentan después de ella. - La vinculación con la suscripción se hace al llegar el evento, no al cierre. Un evento sin suscripción no se cobra después.
billing.meter.error_report_triggeredtiene un solo tipo de error (no_subscription_for_customer) y sale como máximo una vez por medidor y por hora.event_summariesdevuelve unalistsin paginación, solo con los intervalos que tienen eventos.- El
payloadaceptastripe_customer_idcomo alias decustomer_id; el valor tiene que ser elcust_…de Vipter. - No existen
meter_event_adjustmentsni reactivación de medidor.
Qué hacer después
- Consulta cada endpoint y cada objeto en Uso medido, en la referencia de la API.
- Recibe
invoice.paid,subscription_charge.*ybilling.meter.error_report_triggereden tu servidor: catálogo de eventos. - Para cobrar un monto que calculaste tú mismo, sigue Cobro adicional por uso.
SaaS: del registro al dashboard
Guía con código para cobrarle a un usuario de tu SaaS por Vipter: crear la sesión de checkout con el ID del usuario, redirigir, confirmar el pago por la API o por el webhook checkout.session.completed, tratar el PIX, la idempotencia, cobrar el uso del mes en la tarjeta guardada, cancelar y cambiar de plan, qué guardar y cómo convivir con Stripe.
Catálogo de eventos
Los tipos de evento que Vipter envía por webhook, cuándo se dispara cada uno y lo que viene en data.object, en los dos catálogos (los 21 nombres originales y los 19 al estilo Stripe, con las sesiones de checkout, los cobros en la suscripción y el uso medido), con ejemplos completos.