events

Stable. Describes behaviour that is built and covered by tests in this repository.

API reference

POST /v1/events #

Ingest usage events (1..500 per request, at most 1 MiB)

Answers 202 with one result per event, in request order, for any structurally valid batch. A bad event does not fail the batch: it comes back rejected with an error_code. Live keys must send an idempotency_key per event; a retry with the same key and payload returns duplicate with the original event_id. quantity may be a JSON string or number and is parsed as an exact decimal (never as a float). The ingest rate limit is charged per event.

Request body

FieldTypeDescription
events requiredarray of IncomingEvent
Example
{
  "events": [
    {
      "customer_external_id": "string",
      "meter_code": "string",
      "quantity": "1500",
      "event_time": "2026-11-03T10:15:00Z"
    }
  ]
}

Responses

StatusMeaningBody
202Accepted for processingIngestEventsResponse
400Error envelopeError
413Error envelopeError
429Error envelopeError
503Error envelopeError
Example response
{
  "results": [
    {
      "status": "accepted"
    }
  ]
}

Send usage events

# Nothing to install: any HTTP client works.
export PAYRUDDER_API_KEY=qk_test_
curl -sS https://YOUR-API-ADDRESS/v1/events \
  -H "Authorization: Bearer $PAYRUDDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"events":[{"customer_external_id":"cus_42","meter_code":"api_calls","quantity":"1","event_time":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","idempotency_key":"first-event-1"}]}'
# Not on npm yet: build the package from a checkout of the repository, then install the tarball it makes.
(cd /path/to/payrudder/sdk/typescript && npm ci && npm run build && npm pack)   # writes payrudder-sdk-0.0.0.tgz
npm install /path/to/payrudder/sdk/typescript/payrudder-sdk-0.0.0.tgz
import { Payrudder } from "@payrudder/sdk";

const payrudder = new Payrudder({ apiKey: process.env.PAYRUDDER_API_KEY, baseUrl: "https://YOUR-API-ADDRESS" }); // qk_test_...

await payrudder.customers.createCustomer({ external_id: "cus_42", name: "First customer", email: "first@example.com" });

const results = await payrudder.events.ingest([
  { customer_external_id: "cus_42", meter_code: "api_calls", quantity: "1", event_time: new Date(), idempotency_key: "first-event-1" },
]);
console.log(results[0].status); // accepted | duplicate | rejected
# Not on PyPI yet: install from a checkout of the repository.
pip install /path/to/payrudder/sdk/python
from datetime import datetime, timezone
from payrudder import Payrudder

client = Payrudder(api_key="qk_test_...", base_url="https://YOUR-API-ADDRESS")  # or PAYRUDDER_API_KEY / PAYRUDDER_BASE_URL

client.customers.create_customer({"external_id": "cus_42", "name": "First customer", "email": "first@example.com"})

results = client.events.ingest([
    {"customer_external_id": "cus_42", "meter_code": "api_calls", "quantity": "1",
     "event_time": datetime.now(timezone.utc).isoformat(), "idempotency_key": "first-event-1"},
])
print(results[0]["status"])  # accepted | duplicate | rejected
# Not on a module proxy yet: point your module at a checkout of the repository.
go mod edit -require=github.com/nafrucom/payrudder-sdks/go@v0.0.0
go mod edit -replace=github.com/nafrucom/payrudder-sdks/go=/path/to/payrudder/sdk/go
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	payrudder "github.com/nafrucom/payrudder-sdks/go"
)

func main() {
	client, err := payrudder.New(payrudder.WithAPIKey("qk_test_..."), payrudder.WithBaseURL("https://YOUR-API-ADDRESS"))
	if err != nil {
		log.Fatal(err)
	}
	ctx := context.Background()
	if _, err := client.Customers.CreateCustomer(ctx, &payrudder.CreateCustomerRequest{ExternalID: "cus_42", Name: "First customer", Email: "first@example.com"}); err != nil {
		log.Fatal(err)
	}
	results, err := client.Events.Ingest(ctx, []payrudder.IncomingEvent{{
		CustomerExternalID: "cus_42", MeterCode: "api_calls", Quantity: "1", EventTime: time.Now(), IdempotencyKey: "first-event-1",
	}})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(results[0].Status)
}
# Not on Packagist yet: use a checkout of the repository as a path repository.
composer config repositories.payrudder path /path/to/payrudder/sdk/php
composer require payrudder/payrudder-php:"*@dev"
use Payrudder\Client;

$client = new Client(apiKey: 'qk_test_...', baseUrl: 'https://YOUR-API-ADDRESS');

$client->customers->createCustomer(['external_id' => 'cus_42', 'name' => 'First customer', 'email' => 'first@example.com']);

$result = $client->events->ingest([
    ['customer_external_id' => 'cus_42', 'meter_code' => 'api_calls', 'quantity' => '1', 'event_time' => gmdate('c'), 'idempotency_key' => 'first-event-1'],
]);
var_dump($result[0]->status);

POST /v1/events/corrections #

Correct usage events (1..500 per request, at most 1 MiB)

A correction is a new event, never an edit: it names the idempotency_key of the event it corrects and the quantity that event should have had (0 cancels it), with a reason. The service works out the difference between that and what the event stands at now (its own quantity plus every earlier correction of it) and adds it to the period of the original event, by the rules every event follows: while that period is open, or closed but inside its grace period, the usage of the period changes exactly; once it has passed its cutoff or been rated, the difference becomes late usage of that period, a (negative, for less) LATE_USAGE line on the invoice of the period the correction was accepted in. A finalized invoice is never changed.

Only events of meters that add (SUM, COUNT) and have no property filter can be corrected (CORRECTION_UNSUPPORTED_METER otherwise). For a COUNT meter an event is one whatever its quantity, so the only corrections are 0 (cancel it) and any positive quantity (count it again). An event can be corrected as long as its idempotency key is remembered, which is at least 45 and less than 76 days after it was first seen (CORRECTION_UNKNOWN_EVENT after). A correction cannot be corrected: send the original's key again with the quantity it should have. customer_external_id and meter_code must be the original's.

The answer is 202 with one result per correction, in request order, like POST /v1/events; the correction has its own idempotency_key, and a retry with the same key and payload is a duplicate and changes nothing. A refused correction claims no key. Who made it (the signed-in person, or the API key) and the reason are kept. The ingest rate limit is charged per correction. A correction is not usage and is not metered as events.

Request body

FieldTypeDescription
corrections requiredarray of EventCorrection
Example
{
  "corrections": [
    {
      "corrects": "string",
      "reason": "string",
      "customer_external_id": "string",
      "meter_code": "string",
      "quantity": "1500"
    }
  ]
}

Responses

StatusMeaningBody
202Accepted for processingIngestEventsResponse
400Error envelopeError
413Error envelopeError
429Error envelopeError
503Error envelopeError
Example response
{
  "results": [
    {
      "status": "accepted"
    }
  ]
}
curl
curl -X POST https://YOUR-API-ADDRESS/v1/events/corrections \
  -H "Authorization: Bearer $PAYRUDDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"corrections":[{"corrects":"string","reason":"string","customer_external_id":"string","meter_code":"string","quantity":"1500"}]}'

Models

CorrectEventsRequest

FieldTypeDescription
corrections requiredarray of EventCorrection

Error

FieldTypeDescription
error requiredobject

EventCorrection

FieldTypeDescription
idempotency_keystringThe correction's own key. Required for live keys. Optional in sandbox (then no dedup).
corrects requiredstringThe idempotency_key of the event to correct, as it was sent
reason requiredstringWhy. Kept with the correction
customer_external_id requiredstringThe customer of the event being corrected
meter_code requiredstringThe meter of the event being corrected
quantity requiredstring | numberThe quantity the corrected event should have had, a non-negative decimal with at most the meter's quantity_scale decimal places. A string or a JSON number. 0 cancels the event.

EventResult

FieldTypeDescription
status requiredaccepted | duplicate | rejected
event_idstringSet for accepted and duplicate (the original id).
error_codeUNKNOWN_METER | UNKNOWN_CUSTOMER | EVENT_TOO_OLD | EVENT_IN_FUTURE | INVALID_EVENT_TIME | INVALID_QUANTITY | INVALID_QUANTITY_PRECISION | IDEMPOTENCY_KEY_REQUIRED | IDEMPOTENCY_KEY_INVALID | IDEMPOTENCY_KEY_REUSED | INVALID_PROPERTIES | INVALID_CORRECTION | CORRECTION_UNKNOWN_EVENT | CORRECTION_MISMATCH | CORRECTION_UNSUPPORTED_METER | CORRECTION_OF_CORRECTION
error_messagestring

IncomingEvent

FieldTypeDescription
idempotency_keystringRequired for live keys. Optional in sandbox (then no dedup).
customer_external_id requiredstring
meter_code requiredstring
quantity requiredstring | numberNon-negative decimal with at most the meter's quantity_scale decimal places. A string or a JSON number.
event_time requiredstring · date-timeNot older than 35 days, not more than 5 minutes ahead.
propertiesobjectAt most 32 entries and 4 KiB in total.

IngestEventsRequest

FieldTypeDescription
events requiredarray of IncomingEvent

IngestEventsResponse

FieldTypeDescription
results requiredarray of EventResultOne per request event, same order.