> ## Documentation Index
> Fetch the complete documentation index at: https://docs.creem.io/llms.txt
> Use this file to discover all available pages before exploring further.

# 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.



## OpenAPI

````yaml post /v1/events/ingest
openapi: 3.0.0
info:
  title: Creem API
  description: >-
    Creem is an all-in-one platform for managing subscriptions and recurring
    revenue, tailored specifically for today's SaaS companies. It enables you to
    boost revenue, enhance customer retention, and scale your operations
    seamlessly.
  version: v1
  contact:
    name: Creem Support
    url: https://creem.io
    email: support@creem.io
  license:
    name: Commercial
    url: https://creem.io/terms
  termsOfService: https://creem.io/terms
servers:
  - url: https://api.creem.io
    description: Production — live API for processing real transactions and data.
    x-speakeasy-server-id: prod
  - url: https://test-api.creem.io
    description: Test — sandbox API for development and testing with no real charges.
    x-speakeasy-server-id: test
security: []
tags: []
externalDocs:
  description: Creem Documentation
  url: https://docs.creem.io
paths:
  /v1/events/ingest:
    post:
      tags:
        - Events
      summary: Ingest usage events
      description: >-
        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.
      operationId: ingestUsageEvents
      parameters: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IngestUsageEventsApiRequestDto'
      responses:
        '202':
          description: Batch accepted for processing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IngestUsageEventsApiResponseDto'
        '400':
          description: Bad Request - Invalid input parameters
        '401':
          description: Unauthorized - Invalid or missing API key
        '404':
          description: Not Found - Resource does not exist
        '422':
          description: >-
            An event in the batch is invalid — nothing was ingested; `param`
            identifies the event and field
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageMeteringErrorApiResponseDto'
      security:
        - ApiKey: []
components:
  schemas:
    IngestUsageEventsApiRequestDto:
      type: object
      properties:
        events:
          description: >-
            The batch of usage events to ingest (1–100). The whole batch is
            validated before anything is written.
          type: array
          items:
            $ref: '#/components/schemas/IngestUsageEventApiRequestDto'
      required:
        - events
    IngestUsageEventsApiResponseDto:
      type: object
      properties:
        accepted:
          type: number
          description: >-
            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.
          example: 100
        event_ids:
          description: >-
            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.
          example:
            - order-1
            - order-2
          type: array
          items:
            type: string
        warnings:
          description: >-
            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.
          type: array
          items:
            $ref: '#/components/schemas/IngestUsageEventWarningApiDto'
      required:
        - accepted
        - event_ids
    UsageMeteringErrorApiResponseDto:
      type: object
      properties:
        error:
          description: Error details
          allOf:
            - $ref: '#/components/schemas/UsageMeteringErrorDetailApiDto'
      required:
        - error
    IngestUsageEventApiRequestDto:
      type: object
      properties:
        name:
          type: string
          description: The usage event name; must match a meter to be aggregated
          example: image.generated
          maxLength: 128
        customer_id:
          type: string
          description: The customer this usage is attributed to
          example: cust_abc123
        event_id:
          type: string
          description: >-
            Your idempotency key for this event, unique within your store.
            Re-sending the same event_id is a no-op. Generated for you when
            omitted — supply your own if you want retries to be safe.
          example: order-1234
        timestamp:
          type: string
          description: >-
            When the usage occurred (ISO 8601), on your clock. Defaults to the
            time of ingestion.
          example: '2026-08-15T10:30:00.000Z'
        properties:
          type: object
          description: >-
            Top-level structured fields carried by the event — this is where the
            value an aggregation reduces (e.g. "tokens") lives. At most 50 keys,
            keys at most 40 characters, string values at most 500 characters.
          example:
            tokens: 1200
        metadata:
          type: object
          description: >-
            Free-form metadata. Addressable by meter filter clauses without a
            prefix. Same limits as `properties`.
          example:
            tier: pro
      required:
        - name
        - customer_id
    IngestUsageEventWarningApiDto:
      type: object
      properties:
        index:
          type: number
          description: Zero-based index of the event in the submitted batch
          example: 3
        code:
          type: string
          description: >-
            Warning code. `no_matching_meter`: no meter consumes this event
            name. `meter_archived`: only archived meters match — unarchive one
            to resume aggregation. `timestamp_outside_late_window`: the billing
            period the timestamp falls in is finalized, so the event will never
            change its totals.
          enum:
            - no_matching_meter
            - meter_archived
            - timestamp_outside_late_window
          example: no_matching_meter
        message:
          type: string
          description: Human-readable explanation
      required:
        - index
        - code
        - message
    UsageMeteringErrorDetailApiDto:
      type: object
      properties:
        type:
          type: string
          description: Error category
          enum:
            - invalid_request_error
            - api_error
            - authentication_error
            - rate_limit_error
          example: invalid_request_error
        code:
          type: string
          description: Machine-readable error code
          example: meter_name_taken
          nullable: true
        message:
          type: string
          description: Human-readable error message
          example: A meter named "Image generations" already exists in this store
        param:
          type: string
          description: The parameter related to the error, if applicable
          example: events[3].customer_id
          nullable: true
        details:
          type: object
          description: >-
            Structured, machine-readable context for errors that support an
            action, omitted entirely when there is none. Currently emitted for
            `meter_name_taken` when the colliding name is held by an ARCHIVED
            meter: `archived_holder` is true and `archived_holder_id` is that
            meter, which can be restored via POST /v1/meters/{id}/unarchive.
            Additive — treat unknown keys as ignorable.
          example:
            archived_holder: true
            archived_holder_id: mtr_abc123
        request_id:
          type: string
          description: Unique request identifier for support
          example: req_abc123def456
      required:
        - type
        - code
        - message
        - request_id
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: x-api-key
      description: >-
        API key for authentication. You can find your API key in the Creem
        dashboard under Settings > API Keys.

````