Skip to main content

Endpoint

POST https://app.quivly.ai/api/v1/usage/events

Authentication

Send your organization API key as a bearer token. Quivly derives your organization from the key.
Authorization: Bearer YOUR_API_KEY
A missing or invalid key returns 401.

Request body

Send a batch of events. 1–1000 events per request.
{
  "events": [
    {
      "external_customer_id": "001gL00000XR5jlQAD",
      "metric_key": "api_calls",
      "value": 18420,
      "granularity": "daily",
      "timestamp": "2026-06-11T00:00:00Z",
      "properties": { "region": "us-east-1", "plan": "growth" }
    }
  ]
}

Event fields

FieldTypeRequiredDescription
external_customer_idstringYesAn ID Quivly already recognizes for the customer (e.g. a CRM ID). Max 255 chars. Unrecognized IDs are dropped and listed in errors.
metric_keystringYesThe metric name, e.g. api_calls. Max 128 chars. New metric keys are accepted automatically.
valuenumberYesThe measured quantity. Must be a finite number.
timestampstringNoISO 8601 timestamp of the usage (e.g. 2026-06-11T00:00:00Z). Defaults to the time of ingestion when omitted.
granularitystringNoOne of hourly, daily, weekly, monthly. Defaults to daily.
propertiesobjectNoFree-form metadata stored with the event. Must be ≤ 4 KB.

Example request

curl -X POST https://app.quivly.ai/api/v1/usage/events \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "external_customer_id": "001gL00000XR5jlQAD",
        "metric_key": "api_calls",
        "value": 18420,
        "granularity": "daily",
        "timestamp": "2026-06-11T00:00:00Z"
      },
      {
        "external_customer_id": "001gL00000YBlhZQAT",
        "metric_key": "seats_active",
        "value": 42
      }
    ]
  }'

Idempotency

Sending the Idempotency-Key header makes retries safe. If a request with the same key has already been processed, Quivly replays the stored response instead of processing again, and adds the header Idempotency-Replayed: true.
Idempotency-Key: 7f3c1e2a-... (a unique UUID per request)
This header is optional but recommended — use it whenever a network error might cause your client to retry.

Rate limits

Up to 1,000 requests per minute per organization. Exceeding the limit returns 429 with a Retry-After header (seconds to wait).

Verify mode and dry runs

Before you publish the configuration, every call is verify-only — events are resolved against your customers and reported, but nothing is stored. After publishing, you can still force a verify-only call by adding ?dry_run=true:
POST https://app.quivly.ai/api/v1/usage/events?dry_run=true

Response

A successful call returns 200 with a summary of what happened.
{
  "success": true,
  "data": {
    "mode": "live",
    "stored": true,
    "inserted": 2,
    "skipped": 0,
    "summary": { "total": 2, "matched": 2, "unmatched": 0, "rejected": 0 },
    "results": [
      {
        "external_customer_id": "001gL00000XR5jlQAD",
        "metric_key": "api_calls",
        "value": 18420,
        "matched": true,
        "customer_id": "a1b2c3d4-...",
        "customer_name": "Acme Corp",
        "metric_allowed": true,
        "written": true
      }
    ],
    "errors": []
  }
}
Before publishing (or with ?dry_run=true), the same call returns mode: "test", stored: false, inserted: 0, and each result’s written is false — confirming the events resolved but were not saved.

Response fields

FieldDescription
modelive when events were stored, test for a verify-only call.
storedWhether events were written to product usage.
insertedNumber of events written.
summary.totalEvents received.
summary.matchedEvents whose external_customer_id matched a customer.
summary.unmatchedEvents dropped because the customer ID wasn’t recognized.
resultsPer-event detail (first 100).
errorsEvents that were dropped, with the reason (first 100).
A dropped (unmatched) event appears in errors like this:
{ "index": 2, "external_customer_id": "unknown-id", "reason": "No customer matches this external_customer_id" }

Errors

StatusWhenBody
401Missing or invalid API key{ "success": false, "error": "Invalid or missing API key", "code": "UNAUTHORIZED" }
422The payload failed validationSee Validation errors below
422A live call where every event was dropped (none matched a customer)error: "No events could be ingested…", with the full summary under details
429Rate limit exceeded{ "success": false, "error": "Too many requests", "code": "RATE_LIMITED" } + Retry-After header

Validation errors

When the payload is malformed, Quivly returns 422 and points to the exact field paths that failed:
{
  "success": false,
  "error": "Invalid usage events payload",
  "code": "VALIDATION_ERROR",
  "details": {
    "issues": [
      { "path": "events.0.value", "message": "Expected number, received string" },
      { "path": "events.1.metric_key", "message": "String must contain at least 1 character(s)" }
    ]
  }
}