curl --request POST \
--url https://api.creem.io/v1/events/ingest \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"events": [
{
"name": "image.generated",
"customer_id": "cust_abc123",
"event_id": "order-1234",
"timestamp": "2026-08-15T10:30:00.000Z",
"properties": {
"tokens": 1200
},
"metadata": {
"tier": "pro"
}
}
]
}
'import requests
url = "https://api.creem.io/v1/events/ingest"
payload = { "events": [
{
"name": "image.generated",
"customer_id": "cust_abc123",
"event_id": "order-1234",
"timestamp": "2026-08-15T10:30:00.000Z",
"properties": { "tokens": 1200 },
"metadata": { "tier": "pro" }
}
] }
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
events: [
{
name: 'image.generated',
customer_id: 'cust_abc123',
event_id: 'order-1234',
timestamp: '2026-08-15T10:30:00.000Z',
properties: {tokens: 1200},
metadata: {tier: 'pro'}
}
]
})
};
fetch('https://api.creem.io/v1/events/ingest', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.creem.io/v1/events/ingest",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'events' => [
[
'name' => 'image.generated',
'customer_id' => 'cust_abc123',
'event_id' => 'order-1234',
'timestamp' => '2026-08-15T10:30:00.000Z',
'properties' => [
'tokens' => 1200
],
'metadata' => [
'tier' => 'pro'
]
]
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.creem.io/v1/events/ingest"
payload := strings.NewReader("{\n \"events\": [\n {\n \"name\": \"image.generated\",\n \"customer_id\": \"cust_abc123\",\n \"event_id\": \"order-1234\",\n \"timestamp\": \"2026-08-15T10:30:00.000Z\",\n \"properties\": {\n \"tokens\": 1200\n },\n \"metadata\": {\n \"tier\": \"pro\"\n }\n }\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.creem.io/v1/events/ingest")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"events\": [\n {\n \"name\": \"image.generated\",\n \"customer_id\": \"cust_abc123\",\n \"event_id\": \"order-1234\",\n \"timestamp\": \"2026-08-15T10:30:00.000Z\",\n \"properties\": {\n \"tokens\": 1200\n },\n \"metadata\": {\n \"tier\": \"pro\"\n }\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.creem.io/v1/events/ingest")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"events\": [\n {\n \"name\": \"image.generated\",\n \"customer_id\": \"cust_abc123\",\n \"event_id\": \"order-1234\",\n \"timestamp\": \"2026-08-15T10:30:00.000Z\",\n \"properties\": {\n \"tokens\": 1200\n },\n \"metadata\": {\n \"tier\": \"pro\"\n }\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"accepted": 100,
"event_ids": [
"order-1",
"order-2"
],
"warnings": [
{
"index": 3,
"code": "no_matching_meter",
"message": "<string>"
}
]
}{
"error": {
"type": "invalid_request_error",
"code": "meter_name_taken",
"message": "A meter named \"Image generations\" already exists in this store",
"request_id": "req_abc123def456",
"param": "events[3].customer_id",
"details": {
"archived_holder": true,
"archived_holder_id": "mtr_abc123"
}
}
}Ingest usage events
Send up to 100 usage events in one request.
Returns 202 Accepted with the number of events accepted and the event_id of each, in submission order — your own value where you supplied one, a generated value where you did not.
Ingestion is idempotent on (store, event_id): re-sending an event_id this store has already accepted records nothing and is not an error, so retrying a whole batch after a timeout or a partial failure is always safe. Duplicates are deduplicated silently rather than reported — a replayed batch returns the same 202 and the same event_ids as the original. Supply your own event_id to get that guarantee; when you omit it we generate one, which makes the event unique and a retry a second event.
Your event_id is stored trimmed, and the event_ids we return are the stored values — so " abc " is recorded and echoed as "abc", which is what reference= on GET /v1/events matches. Two entries in one batch whose ids differ only by surrounding whitespace are therefore the same event: the second is deduplicated against the first, both are reported accepted, and both carry the same id. An event_id that is entirely whitespace is rejected like an empty one (422) rather than being replaced with a generated id — an unusable key is worth telling you about.
The whole batch is validated before anything is accepted: if any event is invalid the request is rejected with 422 and param pointing at the offending event (e.g. events[3].customer_id), and no event in the batch is ingested.
Billing attribution uses the timestamp you supply (defaulting to the time of ingestion), bounded by the late-event window: once a billing period has been closed for longer than that window, an event arriving for it is still stored durably but is no longer folded into that period’s totals.
202, not 200, is deliberate. It means the batch has been accepted for processing, not that every downstream effect has completed: aggregation into meter totals is already asynchronous today, and the transport behind this endpoint may become queue-backed. A queue producer cannot know how many rows a consumer will ultimately insert, so this response reports acceptance rather than an insert/duplicate split — the contract you code against stays identical when that lands.
curl --request POST \
--url https://api.creem.io/v1/events/ingest \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"events": [
{
"name": "image.generated",
"customer_id": "cust_abc123",
"event_id": "order-1234",
"timestamp": "2026-08-15T10:30:00.000Z",
"properties": {
"tokens": 1200
},
"metadata": {
"tier": "pro"
}
}
]
}
'import requests
url = "https://api.creem.io/v1/events/ingest"
payload = { "events": [
{
"name": "image.generated",
"customer_id": "cust_abc123",
"event_id": "order-1234",
"timestamp": "2026-08-15T10:30:00.000Z",
"properties": { "tokens": 1200 },
"metadata": { "tier": "pro" }
}
] }
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
events: [
{
name: 'image.generated',
customer_id: 'cust_abc123',
event_id: 'order-1234',
timestamp: '2026-08-15T10:30:00.000Z',
properties: {tokens: 1200},
metadata: {tier: 'pro'}
}
]
})
};
fetch('https://api.creem.io/v1/events/ingest', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.creem.io/v1/events/ingest",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'events' => [
[
'name' => 'image.generated',
'customer_id' => 'cust_abc123',
'event_id' => 'order-1234',
'timestamp' => '2026-08-15T10:30:00.000Z',
'properties' => [
'tokens' => 1200
],
'metadata' => [
'tier' => 'pro'
]
]
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.creem.io/v1/events/ingest"
payload := strings.NewReader("{\n \"events\": [\n {\n \"name\": \"image.generated\",\n \"customer_id\": \"cust_abc123\",\n \"event_id\": \"order-1234\",\n \"timestamp\": \"2026-08-15T10:30:00.000Z\",\n \"properties\": {\n \"tokens\": 1200\n },\n \"metadata\": {\n \"tier\": \"pro\"\n }\n }\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.creem.io/v1/events/ingest")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"events\": [\n {\n \"name\": \"image.generated\",\n \"customer_id\": \"cust_abc123\",\n \"event_id\": \"order-1234\",\n \"timestamp\": \"2026-08-15T10:30:00.000Z\",\n \"properties\": {\n \"tokens\": 1200\n },\n \"metadata\": {\n \"tier\": \"pro\"\n }\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.creem.io/v1/events/ingest")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"events\": [\n {\n \"name\": \"image.generated\",\n \"customer_id\": \"cust_abc123\",\n \"event_id\": \"order-1234\",\n \"timestamp\": \"2026-08-15T10:30:00.000Z\",\n \"properties\": {\n \"tokens\": 1200\n },\n \"metadata\": {\n \"tier\": \"pro\"\n }\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"accepted": 100,
"event_ids": [
"order-1",
"order-2"
],
"warnings": [
{
"index": 3,
"code": "no_matching_meter",
"message": "<string>"
}
]
}{
"error": {
"type": "invalid_request_error",
"code": "meter_name_taken",
"message": "A meter named \"Image generations\" already exists in this store",
"request_id": "req_abc123def456",
"param": "events[3].customer_id",
"details": {
"archived_holder": true,
"archived_holder_id": "mtr_abc123"
}
}
}Authorizations
API key for authentication. You can find your API key in the Creem dashboard under Settings > API Keys.
Body
The batch of usage events to ingest (1–100). The whole batch is validated before anything is written.
Show child attributes
Show child attributes
Response
Batch accepted for processing
How many events were accepted. Always the size of the submitted batch — validation is all-or-nothing, so a 202 means every event in the batch was accepted.
100
The event_id each accepted event was recorded under, in the order they were submitted — your own value where you supplied one, the generated value where you did not. This is the stored id after normalisation (surrounding whitespace is trimmed), so it is exactly what reference on GET /v1/events will match. Use it to correlate a submitted event with the row it became.
["order-1", "order-2"]
Advisory warnings about accepted events that will not produce billable usage as sent — no active meter consumes the event name, only archived meters match, or the timestamp falls in an already-finalized billing period. Warnings never change what was accepted: the events are stored either way. Absent when every event will aggregate normally.
Show child attributes
Show child attributes