events
Stable. Describes behaviour that is built and covered by tests in this repository.
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
| Field | Type | Description |
|---|---|---|
events required | array of IncomingEvent |
Example
{
"events": [
{
"customer_external_id": "string",
"meter_code": "string",
"quantity": "1500",
"event_time": "2026-11-03T10:15:00Z"
}
]
}Responses
| Status | Meaning | Body |
|---|---|---|
202 | Accepted for processing | IngestEventsResponse |
400 | Error envelope | Error |
413 | Error envelope | Error |
429 | Error envelope | Error |
503 | Error envelope | Error |
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.tgzimport { 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/pythonfrom 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/gopackage 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
| Field | Type | Description |
|---|---|---|
corrections required | array of EventCorrection |
Example
{
"corrections": [
{
"corrects": "string",
"reason": "string",
"customer_external_id": "string",
"meter_code": "string",
"quantity": "1500"
}
]
}Responses
| Status | Meaning | Body |
|---|---|---|
202 | Accepted for processing | IngestEventsResponse |
400 | Error envelope | Error |
413 | Error envelope | Error |
429 | Error envelope | Error |
503 | Error envelope | Error |
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
| Field | Type | Description |
|---|---|---|
corrections required | array of EventCorrection |
Error
| Field | Type | Description |
|---|---|---|
error required | object |
EventCorrection
| Field | Type | Description |
|---|---|---|
idempotency_key | string | The correction's own key. Required for live keys. Optional in sandbox (then no dedup). |
corrects required | string | The idempotency_key of the event to correct, as it was sent |
reason required | string | Why. Kept with the correction |
customer_external_id required | string | The customer of the event being corrected |
meter_code required | string | The meter of the event being corrected |
quantity required | string | number | The 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
| Field | Type | Description |
|---|---|---|
status required | accepted | duplicate | rejected | |
event_id | string | Set for accepted and duplicate (the original id). |
error_code | UNKNOWN_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_message | string |
IncomingEvent
| Field | Type | Description |
|---|---|---|
idempotency_key | string | Required for live keys. Optional in sandbox (then no dedup). |
customer_external_id required | string | |
meter_code required | string | |
quantity required | string | number | Non-negative decimal with at most the meter's quantity_scale decimal places. A string or a JSON number. |
event_time required | string · date-time | Not older than 35 days, not more than 5 minutes ahead. |
properties | object | At most 32 entries and 4 KiB in total. |
IngestEventsRequest
| Field | Type | Description |
|---|---|---|
events required | array of IncomingEvent |
IngestEventsResponse
| Field | Type | Description |
|---|---|---|
results required | array of EventResult | One per request event, same order. |