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

# Report usage

> Sends up to 100 usage events, each naming the subscription it is billed to and the meter it counts against. **Every event carries an `event_id` of your own, unique across your organisation, and that is what makes a resend safe**: an event that was already stored with the same details is counted once and reported as a `duplicate`, so retrying a batch that timed out cannot double count. The same `event_id` with different details is rejected as `event_id_conflict`. An event that is malformed fails the whole request, with nothing stored. **An event that is well formed but cannot be accepted — a subscription that no longer exists, a meter the subscription's plan does not bill for — is rejected on its own and the rest are stored. The response is then 207, and `results` says why for each: a 2xx does not mean every event was stored, so read `rejected`.** Usage is billed in arrears, when the subscription's period ends.

<Info>Requires the `usage:write` scope. See [Authentication](/api-reference/authentication#scopes). For how usage is priced and invoiced, see [Usage-based billing](/api-reference/guides/usage-based-billing).</Info>


## OpenAPI

````yaml /api-reference/openapi.json post /v1/usage_events
openapi: 3.1.0
info:
  title: Saasybill API
  version: 1.0.0
  description: >-
    Create and manage subscriptions in Saasybill from your own platform.


    **Authentication.** `Authorization: Bearer saasybill_live_…`. Create keys
    under Developer Settings.


    **Idempotency.** Send an `Idempotency-Key` on writes. It is required to
    create a subscription.


    **Webhooks.** Saasybill calls your URL when something changes. Events:
    `customer.created`, `customer.updated`, `customer.deleted`,
    `subscription.created`, `subscription.updated`, `subscription.ended`,
    `subscription.restored`, `subscription.deleted`, `invoice.created`,
    `invoice.updated`, `invoice.paid`, `invoice.voided`, `invoice.deleted`,
    `credit_note.created`, `credit_note.updated`, `credit_note.voided`,
    `credit_note.deleted`. Each request is signed: `Saasybill-Signature: t=<unix
    seconds>,v1=<hex>`, where `v1` is `HMAC-SHA256(secret, "<t>.<raw body>")`.
    Reject a `t` more than five minutes old. Deliveries are at-least-once,
    retried for about 20 hours, and unordered: de-duplicate on the event `id`.
    Changes are detected about once a minute, so an event can arrive up to a
    minute late, and changes inside one minute are combined.


    **Testing.** Create a sandbox under Developer Settings → Configuration,
    connect it to a Xero Demo Company, and use a test key (`saasybill_test_…`)
    created inside it. A test key works only in its sandbox, which sends no
    emails and is never billed. Your plans, customers and subscriptions can be
    copied into the sandbox when it is created or reset, and its test clock can
    be advanced to see renewals without waiting. `GET /v1/organisation` returns
    `livemode: false` for a sandbox.
servers:
  - url: https://api.saasybill.com
    description: Production
security: []
tags:
  - name: Organisation
    description: The organisation an API key belongs to.
  - name: Customers
    description: >-
      Customers are synced from Xero. Create one only where the organisation
      allows it.
  - name: Plans
    description: Read the pricing plans you can sell.
  - name: Charges
    description: Read the one-off fees that can be added to a new subscription.
  - name: Subscriptions
    description: Create subscriptions, change units, and end or restore them.
  - name: Invoices
    description: Read the invoices Saasybill raises in Xero.
  - name: Credit notes
    description: >-
      Read the credit notes Saasybill raises in Xero, and where their credit was
      applied.
  - name: Usage
    description: Report the usage that is billed in arrears against a subscription.
paths:
  /v1/usage_events:
    post:
      tags:
        - Usage
      summary: Report usage
      description: >-
        Sends up to 100 usage events, each naming the subscription it is billed
        to and the meter it counts against. **Every event carries an `event_id`
        of your own, unique across your organisation, and that is what makes a
        resend safe**: an event that was already stored with the same details is
        counted once and reported as a `duplicate`, so retrying a batch that
        timed out cannot double count. The same `event_id` with different
        details is rejected as `event_id_conflict`. An event that is malformed
        fails the whole request, with nothing stored. **An event that is well
        formed but cannot be accepted — a subscription that no longer exists, a
        meter the subscription's plan does not bill for — is rejected on its own
        and the rest are stored. The response is then 207, and `results` says
        why for each: a 2xx does not mean every event was stored, so read
        `rejected`.** Usage is billed in arrears, when the subscription's period
        ends.
      operationId: post_v1_usage_events
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: >-
            1 to 255 printable characters. A retry with the same key and body
            returns the first response.
          schema:
            type: string
            minLength: 1
            maxLength: 255
            example: usage_batch_2026-10-12T10
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                events:
                  minItems: 1
                  maxItems: 100
                  type: array
                  items:
                    type: object
                    properties:
                      event_id:
                        type: string
                        description: >-
                          Your own id for this event, unique across your
                          organisation. Sending the same event again with the
                          same id is safe: it is counted once.
                        minLength: 1
                        maxLength: 255
                      subscription:
                        type: string
                        minLength: 1
                        maxLength: 100
                        description: The Saasybill subscription id the usage is billed to.
                      meter:
                        description: The Saasybill meter id. Give this or `meter_code`.
                        type: string
                        minLength: 1
                        maxLength: 100
                      meter_code:
                        type: string
                        description: >-
                          The meter's ID as shown in Saasybill, such as
                          `api_calls`.
                      quantity:
                        description: >-
                          How much was used. Greater than zero, to four decimal
                          places.
                        anyOf:
                          - type: string
                            minLength: 1
                            maxLength: 20
                          - type: number
                      timestamp:
                        type: string
                        format: date-time
                        description: >-
                          When the usage happened, ISO 8601 with a time zone.
                          Not in the future.
                    required:
                      - event_id
                      - subscription
                      - quantity
                      - timestamp
                    additionalProperties: false
              required:
                - events
              additionalProperties: false
            examples:
              Two meters, by code:
                summary: Two meters, by code
                description: >-
                  The usual report: name each meter by its ID in Saasybill. Each
                  event_id is your own and must be unique.
                value:
                  events:
                    - event_id: evt_20261012_0001
                      subscription: 9d4e2b7a-13c5-4f8e-b6a0-2e1d7c9f5b33
                      meter_code: api_calls
                      quantity: '1250'
                      timestamp: '2026-10-12T03:15:00Z'
                    - event_id: evt_20261012_0002
                      subscription: 9d4e2b7a-13c5-4f8e-b6a0-2e1d7c9f5b33
                      meter_code: storage_gb
                      quantity: '12.5'
                      timestamp: '2026-10-12T03:15:00Z'
              Meter by id:
                summary: Meter by id
                description: >-
                  Give meter instead of meter_code to name the meter by its
                  Saasybill id.
                value:
                  events:
                    - event_id: evt_20261012_0003
                      subscription: 9d4e2b7a-13c5-4f8e-b6a0-2e1d7c9f5b33
                      meter: c4d8e2f6-7a19-4b30-95e1-3f6a8b0d2c55
                      quantity: 300
                      timestamp: '2026-10-12T04:00:00+11:00'
      responses:
        '200':
          description: Success.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    const: usage_event_batch
                  created:
                    type: integer
                    description: Events stored by this request.
                  duplicates:
                    type: integer
                    description: Events already stored with the same details, counted once.
                  rejected:
                    type: integer
                    description: >-
                      Events not stored. `results` says why for each. Resend
                      them once fixed.
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        event_id:
                          type: string
                        status:
                          type: string
                          enum:
                            - created
                            - duplicate
                            - rejected
                        error:
                          description: Present only when `status` is `rejected`.
                          type: object
                          properties:
                            code:
                              type: string
                              enum:
                                - subscription_not_found
                                - meter_not_found
                                - meter_inactive
                                - meter_not_on_plan
                                - timestamp_in_future
                                - before_subscription_start
                                - after_subscription_end
                                - event_id_conflict
                              description: Stable. Branch on this, never on `message`.
                            message:
                              type: string
                          required:
                            - code
                            - message
                          additionalProperties: false
                      required:
                        - event_id
                        - status
                      additionalProperties: false
                    description: One entry per event, in the order they were sent.
                required:
                  - object
                  - created
                  - duplicates
                  - rejected
                  - results
                additionalProperties: false
              example:
                object: usage_event_batch
                created: 2
                duplicates: 0
                rejected: 0
                results:
                  - event_id: evt_20261012_0001
                    status: created
                  - event_id: evt_20261012_0002
                    status: created
          headers:
            Request-Id:
              $ref: '#/components/headers/Request-Id'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimit-Limit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimit-Remaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimit-Reset'
        '207':
          description: >-
            At least one event was rejected. The others were stored. Read
            `rejected` and each entry in `results`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    const: usage_event_batch
                  created:
                    type: integer
                    description: Events stored by this request.
                  duplicates:
                    type: integer
                    description: Events already stored with the same details, counted once.
                  rejected:
                    type: integer
                    description: >-
                      Events not stored. `results` says why for each. Resend
                      them once fixed.
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        event_id:
                          type: string
                        status:
                          type: string
                          enum:
                            - created
                            - duplicate
                            - rejected
                        error:
                          description: Present only when `status` is `rejected`.
                          type: object
                          properties:
                            code:
                              type: string
                              enum:
                                - subscription_not_found
                                - meter_not_found
                                - meter_inactive
                                - meter_not_on_plan
                                - timestamp_in_future
                                - before_subscription_start
                                - after_subscription_end
                                - event_id_conflict
                              description: Stable. Branch on this, never on `message`.
                            message:
                              type: string
                          required:
                            - code
                            - message
                          additionalProperties: false
                      required:
                        - event_id
                        - status
                      additionalProperties: false
                    description: One entry per event, in the order they were sent.
                required:
                  - object
                  - created
                  - duplicates
                  - rejected
                  - results
                additionalProperties: false
              example:
                object: usage_event_batch
                created: 1
                duplicates: 1
                rejected: 1
                results:
                  - event_id: evt_20261012_0001
                    status: created
                  - event_id: evt_20261012_0002
                    status: duplicate
                  - event_id: evt_20261012_0004
                    status: rejected
                    error:
                      code: meter_not_on_plan
                      message: This subscription's plan does not bill for this meter.
          headers:
            Request-Id:
              $ref: '#/components/headers/Request-Id'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimit-Limit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimit-Remaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimit-Reset'
        '401':
          description: The key is missing, malformed, revoked or expired.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  type: authentication_error
                  code: invalid_api_key
                  message: 'Provide a valid API key as `Authorization: Bearer <key>`.'
                  request_id: req_4f1a9c2e7b8d3a605e1c9f02
          headers:
            Request-Id:
              $ref: '#/components/headers/Request-Id'
        '403':
          description: >-
            The key lacks the scope, is a test key, or the organisation has not
            allowed this.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  type: permission_error
                  code: insufficient_scope
                  message: This API key does not have the `usage:write` scope.
                  request_id: req_4f1a9c2e7b8d3a605e1c9f02
          headers:
            Request-Id:
              $ref: '#/components/headers/Request-Id'
        '404':
          description: No such object.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  type: not_found
                  code: unknown_endpoint
                  message: There is no such endpoint.
                  request_id: req_4f1a9c2e7b8d3a605e1c9f02
          headers:
            Request-Id:
              $ref: '#/components/headers/Request-Id'
        '409':
          description: >-
            The request conflicts with the current state, or with an earlier
            request using the same `Idempotency-Key`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  type: conflict
                  code: idempotency_conflict
                  message: >-
                    This `Idempotency-Key` was used with a different request.
                    Use a new key for a different request.
                  request_id: req_4f1a9c2e7b8d3a605e1c9f02
          headers:
            Request-Id:
              $ref: '#/components/headers/Request-Id'
        '422':
          description: A field is missing or invalid. `error.param` names it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  type: invalid_request_error
                  code: invalid_parameter
                  message: >-
                    `events.3.quantity`: A quantity is a plain number greater
                    than zero, with at most 4 decimal places and no sign or
                    exponent.
                  param: events.3.quantity
                  request_id: req_4f1a9c2e7b8d3a605e1c9f02
          headers:
            Request-Id:
              $ref: '#/components/headers/Request-Id'
        '429':
          description: 'Rate limited: 120 requests a minute per key. See `Retry-After`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  type: rate_limit_error
                  code: rate_limited
                  message: Too many requests. Try again in 12 seconds.
                  request_id: req_4f1a9c2e7b8d3a605e1c9f02
          headers:
            Request-Id:
              $ref: '#/components/headers/Request-Id'
        '500':
          description: >-
            Something went wrong on our side. Retry; quote `error.request_id` if
            it persists.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  type: api_error
                  code: internal_error
                  message: >-
                    Something went wrong on our side. Retry the request; if it
                    keeps failing, quote the request id.
                  request_id: req_4f1a9c2e7b8d3a605e1c9f02
          headers:
            Request-Id:
              $ref: '#/components/headers/Request-Id'
      security:
        - bearerAuth:
            - usage:write
components:
  headers:
    Request-Id:
      description: A unique id for the request. Quote it when you contact support.
      schema:
        type: string
        example: req_4f1a9c2e7b8d3a605e1c9f02
    RateLimit-Limit:
      description: Requests allowed per minute for this key.
      schema:
        type: integer
        example: 120
    RateLimit-Remaining:
      description: Requests left in the current window.
      schema:
        type: integer
        example: 117
    RateLimit-Reset:
      description: Seconds until the oldest counted request stops counting.
      schema:
        type: integer
        example: 41
  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            type:
              type: string
              enum:
                - authentication_error
                - permission_error
                - invalid_request_error
                - not_found
                - conflict
                - rate_limit_error
                - api_error
            code:
              type: string
              description: Stable. Branch on this, never on `message`.
            message:
              type: string
            param:
              type: string
            request_id:
              type: string
          required:
            - type
            - code
            - message
            - request_id
          additionalProperties: false
      required:
        - error
      additionalProperties: false
      description: >-
        Every failed request answers with this shape. Branch on `error.code`,
        never on `error.message`.
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        An API key. Its scopes decide what it may call: `customers:read` (List
        and look up customers.) `customers:write` (Create customers, and Xero
        contacts where the organisation allows it.) `catalogue:read` (List plans
        and charges.) `subscriptions:read` (List and look up subscriptions.)
        `subscriptions:write` (Create subscriptions, change units, end and
        restore them.) `invoices:read` (List and look up invoices.)
        `credit_notes:read` (List and look up credit notes, and where their
        credit was applied.)

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.