Revenue Expansion beta
Send product usage events that Revenue Linter can score.
Product events tell Revenue Linter which paying accounts are active, dormant, or pushing against plan limits. They unlock silent-churn and expansion-ready cohorts without writing to your app, Stripe, or CRM.
Endpoint
POST product events with an ingest token.
Use the same ingest token you created for app-state snapshots. Include a stable app user ID and Stripe customer ID when available.
POST /api/v1/events/product
Authorization: Bearer <ingest_token>
Content-Type: application/json
{
"events": [{
"externalUserId": "user_123",
"stripeCustomerId": "cus_123",
"type": "PREMIUM_FEATURE_USED",
"occurredAt": "2026-05-29T15:00:00.000Z",
"properties": {
"feature": "audit_export",
"plan": "starter"
}
}]
}Backend examples
Send events from your server, not the browser.
Keep REVLINT_INGEST_TOKEN in server-side environment variables. These examples are intentionally one-way and zero-write.
Next.js
Call Revenue Linter from a server action, route handler, or trusted server-side job.
const endpoint = process.env.REVLINT_PRODUCT_EVENTS_URL ?? "https://revenuelinter.com/api/v1/events/product";
await fetch(endpoint, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.REVLINT_INGEST_TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
events: [{
externalUserId: user.id,
stripeCustomerId: user.stripeCustomerId,
type: "API_LIMIT_USED",
occurredAt: new Date().toISOString(),
properties: {
usage: apiUsageThisMonth,
limit: apiLimit,
unit: "api_calls"
}
}]
})
});Laravel
Send events from a controller, queued job, listener, or scheduled command.
use Illuminate\Support\Facades\Http;
Http::withToken(config('services.revlint.ingest_token'))
->post(config('services.revlint.product_events_url'), [
'events' => [[
'externalUserId' => (string) $user->id,
'stripeCustomerId' => $user->stripe_customer_id,
'type' => 'API_LIMIT_USED',
'occurredAt' => now()->toIso8601String(),
'properties' => [
'usage' => $usageThisMonth,
'limit' => $planLimit,
'unit' => 'api_calls',
],
]],
]);Rails
Post events from a service object, ActiveJob worker, or subscription usage job.
require "net/http"
require "json"
uri = URI(ENV.fetch("REVLINT_PRODUCT_EVENTS_URL", "https://revenuelinter.com/api/v1/events/product"))
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('REVLINT_INGEST_TOKEN')}"
request["Content-Type"] = "application/json"
request.body = {
events: [{
externalUserId: user.id.to_s,
stripeCustomerId: user.stripe_customer_id,
type: "SEAT_LIMIT_USED",
occurredAt: Time.current.iso8601,
properties: {
seatsUsed: workspace.users.count,
seatLimit: workspace.plan.seat_limit
}
}]
}.to_json
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
http.request(request)
endNode
Use from a backend worker, API server, cron job, or billing reconciliation task.
const endpoint = process.env.REVLINT_PRODUCT_EVENTS_URL || "https://revenuelinter.com/api/v1/events/product";
await fetch(endpoint, {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.REVLINT_INGEST_TOKEN,
"Content-Type": "application/json"
},
body: JSON.stringify({
events: [{
externalUserId: String(user.id),
stripeCustomerId: user.stripeCustomerId,
type: "PREMIUM_FEATURE_USED",
occurredAt: new Date().toISOString(),
properties: {
feature: "audit_export",
plan: user.plan
}
}]
})
});Event types
Start with events that prove activity or pressure.
Revenue Linter accepts any product event type string, but these names make the Revenue Expansion intent clear.
Expansion-ready accounts
Send usage and limit values when an account nears a cap.
Revenue Linter marks lower-tier accounts as expansion-ready when recent usage is high or usage reaches at least 80% of a reported limit.
POST /api/v1/events/product
Authorization: Bearer <ingest_token>
{
"events": [{
"externalUserId": "user_123",
"stripeCustomerId": "cus_123",
"type": "API_LIMIT_USED",
"occurredAt": "2026-05-29T15:05:00.000Z",
"properties": {
"usage": 91000,
"limit": 100000,
"metric": "api_requests",
"window": "monthly"
}
}, {
"externalUserId": "user_456",
"stripeCustomerId": "cus_456",
"type": "SEAT_LIMIT_USED",
"occurredAt": "2026-05-29T15:06:00.000Z",
"properties": {
"seatsUsed": 9,
"seatLimit": 10
}
}]
}Properties Revenue Linter reads
Use one usage key and one limit key.
These fields are enough for Revenue Linter to calculate a usage ratio and label the exported signal.
Silent churn
Any recent product event can prove activity.
Silent churn is flagged when Stripe billing is active but Revenue Linter has no recent app activity or product usage events. Send login, feature, export, dashboard, or API usage events for accounts that should count as active.
{
"events": [{
"externalUserId": "user_789",
"stripeCustomerId": "cus_789",
"type": "EXPORT_CREATED",
"occurredAt": "2026-05-29T15:10:00.000Z",
"properties": {
"exportType": "audit_report"
}
}]
}Safety notes
Do not send secrets
The product event API rejects secret-like payloads. Do not include API keys, tokens, passwords, cookies, or raw credentials.
Prefer IDs over personal data
Use externalUserId and stripeCustomerId. Email is optional and hashed before storage.
Keep follow-up team-approved
Revenue Expansion exports advisory cohorts. It does not write to your CRM or trigger marketing campaigns.