Skip to content

Stripe integration

Unimeter records and aggregates usage events in real time. Stripe handles invoicing and payments. Together they give you a complete usage-based billing pipeline without building aggregation logic yourself.

This guide walks through a working example: your application ingests usage events into Unimeter, and when Stripe creates an invoice it queries the usage total and adds a line item automatically.

The integration has two sides. On the recording side, your application sends usage events to Unimeter every time a billable action happens. Unimeter aggregates them in real time and keeps a running total per customer per billing period.

On the invoicing side, Stripe sends your server a webhook when it creates a new invoice. Your webhook handler takes the customer ID from the invoice, maps it to a Unimeter account, queries the usage total for the last completed billing period, and creates a line item on that invoice with the metered amount. Stripe then finalizes the invoice and charges the customer as usual.

Unimeter owns the usage data. Stripe owns the billing. Your webhook handler is the bridge between the two, and it only runs when Stripe asks for the numbers.

  • A Stripe account with an API key and webhook signing secret.
  • A running Unimeter cluster (single-node is fine for development).
  • The Unimeter SDK installed:
Terminal window
go get github.com/unimeter/go-unimeter@latest

Register a metric that counts API calls per calendar month. A BillingCycleDay of 1 means each period starts on the first of the month, matching the default Stripe subscription cycle.

client, _ := billing.New([]string{"localhost:7001"})
err := client.Metrics.Create(ctx, billing.MetricSchema{
Code: "api_calls",
AggType: billing.AggCount,
PeriodType: billing.PeriodCalendar,
BillingCycleDay: 1,
})

You only need to do this once. The metric definition persists across restarts.

Call Ingest every time a billable action happens. A common pattern is an HTTP middleware that fires after each request.

func usageMiddleware(next http.Handler, client *billing.Client) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r)
accountID, _ := strconv.ParseUint(r.Header.Get("X-Account-ID"), 10, 64)
if accountID == 0 {
return
}
client.Ingest(r.Context(), []billing.Event{{
AccountID: accountID,
MetricCode: "api_calls",
Value: 1,
}})
})
}

Events are deduplicated by their idempotency key, so retries are safe.

When Stripe finalizes an invoice it sends an invoice.created webhook. Your handler queries Unimeter for the customer’s usage during the last billing period and creates a line item on the invoice.

package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
billing "github.com/unimeter/go-unimeter"
"github.com/stripe/stripe-go/v82"
"github.com/stripe/stripe-go/v82/invoiceitem"
"github.com/stripe/stripe-go/v82/webhook"
)
// Map Stripe customer IDs to Unimeter account IDs.
// In production, store this in your database or in Stripe customer metadata.
var customerToAccount = map[string]uint64{
"cus_demo_alice": 1,
"cus_demo_bob": 2,
}
func main() {
stripe.Key = os.Getenv("STRIPE_SECRET_KEY")
webhookSecret := os.Getenv("STRIPE_WEBHOOK_SECRET")
client, err := billing.New([]string{os.Getenv("BILLING_NODES")})
if err != nil {
log.Fatal(err)
}
defer client.Close()
http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
event, err := webhook.ConstructEvent(
body, r.Header.Get("Stripe-Signature"), webhookSecret,
)
if err != nil {
http.Error(w, "invalid signature", http.StatusBadRequest)
return
}
if event.Type != "invoice.created" {
w.WriteHeader(http.StatusOK)
return
}
var invoice stripe.Invoice
json.Unmarshal(event.Data.Raw, &invoice)
accountID, ok := customerToAccount[invoice.Customer.ID]
if !ok {
w.WriteHeader(http.StatusOK)
return
}
// Query last completed billing period.
usage, err := client.Query(r.Context(), billing.QueryRequest{
AccountID: accountID,
MetricCode: "api_calls",
Period: billing.LastBillingPeriod(1),
})
if err != nil {
http.Error(w, "query failed", http.StatusInternalServerError)
return
}
// Create a line item on the Stripe invoice.
period := billing.LastBillingPeriod(1)
idempotencyKey := fmt.Sprintf(
"unimeter-%d-api_calls-%s", accountID, period.Start.Format("2006-01"),
)
_, err = invoiceitem.New(&stripe.InvoiceItemParams{
Customer: stripe.String(invoice.Customer.ID),
Invoice: stripe.String(invoice.ID),
Description: stripe.String("API calls"),
Quantity: stripe.Int64(int64(usage.Value.Count)),
UnitAmount: stripe.Int64(10), // $0.001 per call
Currency: stripe.String("usd"),
Params: stripe.Params{
IdempotencyKey: stripe.String(idempotencyKey),
},
})
if err != nil {
log.Printf("stripe error: %v", err)
}
w.WriteHeader(http.StatusOK)
})
log.Println("listening on :4242")
log.Fatal(http.ListenAndServe(":4242", nil))
}

The important mapping is between Stripe customer IDs and Unimeter account IDs. In the example this is a hardcoded map; in production, store the Unimeter account ID in Stripe customer metadata or in your database.

The idempotency key includes the account, metric, and period start date. If Stripe retries the webhook, the invoice item creation is skipped because Stripe has already seen that key.

Set the required environment variables and start the server:

Terminal window
export STRIPE_SECRET_KEY="sk_test_..."
export STRIPE_WEBHOOK_SECRET="whsec_..."
export BILLING_NODES="localhost:7001"
go run main.go

In a separate terminal, forward Stripe test webhooks to your local server using the Stripe CLI:

Terminal window
stripe listen --forward-to localhost:4242/webhook

Create a test invoice in the Stripe dashboard or via the API and watch the webhook handler query Unimeter and attach a line item.