Documentation

Events

API & other languages

Send product events, insights and expenses from any backend: the Python SDK, or plain HTTP — the API is described by an OpenAPI 3.1 spec.

OpenAPI spec

The write API (POST /api/v1/events, POST /api/v1/insights and POST / PATCH /api/v1/expenses) is described by an OpenAPI 3.1 document, generated from the same validation the server runs, so it can't drift from what the endpoints accept:

https://metricneed.com/api/v1/openapi.json

Import it in Postman, Insomnia or Bruno, browse it in any OpenAPI viewer, or generate a client with openapi-generator. Authentication is a bearer mne_… key from Settings → Events API — server-side only; the field reference and error codes are on Events & insights.

Python SDK

pip install metricneed
import os

from metricneed import MetricNeed

mn = MetricNeed(api_key=os.environ["METRICNEED_API_KEY"], base_url=os.environ.get("METRICNEED_URL", "https://metricneed.com"))

mn.track("signups", "New signup", user_id="usr_42", tags={"plan": "pro"})
mn.insight.increment("Signups today")

mn.shutdown()  # flush before a short-lived process exits

Same API as the Node.js SDK, in Python: track() never raises and sends in a background thread; track_sync() waits for the answer and raises a MetricNeedError; batch=True queues events and sends them in batches; retries cover network errors, 429 and 5xx. Zero dependencies, Python 3.9+. Call shutdown() before a serverless function or a script exits.

Plain HTTP

No SDK needed: one JSON POST per event (or up to 100 in { "events": [...] }). Each snippet reads METRICNEED_API_KEY, and METRICNEED_URL for a self-hosted or local instance.

Go

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	base := os.Getenv("METRICNEED_URL")
	if base == "" {
		base = "https://metricneed.com"
	}
	body, _ := json.Marshal(map[string]any{
		"channel": "signups", "event": "New signup", "user_id": "usr_42",
		"tags": map[string]string{"plan": "pro"},
	})
	req, _ := http.NewRequest(http.MethodPost, base+"/api/v1/events", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("METRICNEED_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	out, _ := io.ReadAll(res.Body)
	fmt.Println(res.StatusCode, string(out))
}

Ruby

require "json"
require "net/http"
require "uri"

uri = URI("#{ENV.fetch("METRICNEED_URL", "https://metricneed.com")}/api/v1/events")
request = Net::HTTP::Post.new(uri, "Authorization" => "Bearer #{ENV.fetch("METRICNEED_API_KEY")}", "Content-Type" => "application/json")
request.body = { channel: "signups", event: "New signup", user_id: "usr_42", tags: { plan: "pro" } }.to_json

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(request) }
puts response.code, response.body

PHP

<?php
$ch = curl_init((getenv('METRICNEED_URL') ?: 'https://metricneed.com') . '/api/v1/events');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('METRICNEED_API_KEY'), 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode(['channel' => 'signups', 'event' => 'New signup', 'user_id' => 'usr_42', 'tags' => ['plan' => 'pro']]),
]);
$body = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $body, "\n";

Swift (server-side)

import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

let env = ProcessInfo.processInfo.environment
var request = URLRequest(url: URL(string: "\(env["METRICNEED_URL"] ?? "https://metricneed.com")/api/v1/events")!)
request.httpMethod = "POST"
request.setValue("Bearer \(env["METRICNEED_API_KEY"]!)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONSerialization.data(withJSONObject: [
    "channel": "signups", "event": "New signup", "user_id": "usr_42", "tags": ["plan": "pro"]
])

let (data, response) = try await URLSession.shared.data(for: request)
print((response as! HTTPURLResponse).statusCode, String(decoding: data, as: UTF8.self))

Swift on a server (Vapor, Hummingbird) — never from an iOS app: the key would ship in the app bundle.

Insights

Set a gauge with value, or add to it atomically with increment:

curl -X POST https://metricneed.com/api/v1/insights \
  -H "Authorization: Bearer $METRICNEED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Signups today", "increment": 1 }'

Expenses

Add one of your expenses — a recurring subscription with intervalUnit + intervalCount, or a one-off without them — from a script or when a vendor invoice arrives. Same fields and rules as the Expenses form: amounts in major units, dates as YYYY-MM-DD, startedAt is the first charge. The Free plan's limit of 5 expenses applies (402 expense_limit).

curl -X POST https://metricneed.com/api/v1/expenses \
  -H "Authorization: Bearer $METRICNEED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Vercel Pro", "vendor": "vercel", "category": "hosting", "amount": 20, "currency": "USD", "intervalUnit": "month", "intervalCount": 1, "startedAt": "2026-09-01" }'

The response carries the expense's id. Keep it to edit the expense later — only the fields you send change; endedAt cancels a subscription, null reactivates it:

curl -X PATCH https://metricneed.com/api/v1/expenses/$EXPENSE_ID \
  -H "Authorization: Bearer $METRICNEED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "endedAt": "2026-09-30" }'

Keys stay write-only: they can't list or delete expenses — do that on the Expenses page.