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

# Send purchase-refunded

> Reports that a previously reported purchase was refunded.

`amount` is the positive magnitude of the refund in the minor unit of
`currency`. The direction is carried by the event type, so a negative
amount is a validation failure rather than an encoding. `invoiceId`
identifies the payment being reversed.

Part of the [v2 events contract](/api-reference/generic-events/send-event). See also: [signup created](/api-reference/generic-events/signup-created-v2), [purchase created](/api-reference/generic-events/purchase-created-v2), [demo attended](/api-reference/generic-events/demo-attended-v2).


## OpenAPI

````yaml api-reference/openapi-v2.json POST /v2/events/purchase-refunded
openapi: 3.1.0
info:
  title: Cello Events API
  version: 2.0.0
  contact:
    name: Cello Support
    url: https://docs.cello.so
  license:
    name: UNLICENSED
    identifier: LicenseRef-Proprietary
  summary: Ingestion of referral lifecycle events from customer products.
  description: |-
    Reports referral lifecycle events to Cello: a referred user signing up,
    attending a demo, paying an invoice, or having a charge refunded.

    This document is the typed v2 contract. Each business event has its own
    endpoint under `/v2/events/`, each with a closed schema, and requests are
    validated strictly: undeclared fields are refused, enums are closed,
    timestamps require an explicit UTC offset, and monetary amounts are
    integers in the currency's minor unit. Every violation in a request is
    reported at once, each with a JSON Pointer to the offending field, so a
    first integration takes one round trip rather than five.

    `POST /events` remains a live frozen alias served by a separate
    implementation. It is not part of this document. New integrations should
    use `v2`.
servers: []
security:
  - BearerAuth: []
tags:
  - name: Events v2
    description: The typed v2 event endpoints. One endpoint per business event.
paths:
  /v2/events/purchase-refunded:
    post:
      tags:
        - Events v2
      summary: Report a refunded purchase
      description: |-
        Reports that a previously reported purchase was refunded.

        `amount` is the positive magnitude of the refund in the minor unit of
        `currency`. The direction is carried by the event type, so a negative
        amount is a validation failure rather than an encoding. `invoiceId`
        identifies the payment being reversed.
      operationId: createPurchaseRefundedEvent
      parameters:
        - $ref: '#/components/parameters/Parameters.IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PurchaseRefundedRequest'
            examples:
              A full refund of a 49.00 EUR purchase:
                summary: A full refund of a 49.00 EUR purchase
                value:
                  ucc: ucc_7f3d9a
                  occurredAt: '2026-08-06T09:00:00Z'
                  newUser:
                    id: usr_1042
                  amount: 4900
                  currency: EUR
                  invoiceId: inv_456
      responses:
        '202':
          description: >-
            The event was validated and durably written to the event stream.


            This says the event was accepted, not that attribution or reward
            accrual

            has run. Those happen asynchronously afterwards.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AcceptedResponse'
        '422':
          description: >-
            The body parsed but broke the contract. Every violation is listed,
            each

            with a pointer to the field, so one round trip is enough to fix it.
            No

            event is written.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
components:
  parameters:
    Parameters.IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: |-
        An opaque key identifying this submission attempt, so that a retry can
        be recognised as the same event.

        A repeat of the same request, for the same tenant and event type, within
        72 hours returns the original `202` — the same `eventId`, `receivedAt`,
        and `warnings` — and does not create a second event. A repeat that uses
        the same key with a different body is refused with `409` and
        `IDEMPOTENCY_CONFLICT`. After 72 hours the same key is a new submission.

        The same key may be sent in the `idempotencyKey` body field instead. If
        both are sent they must be equal after trim, otherwise the request is
        refused with `IDEMPOTENCY_KEY_CONFLICT` — there is no precedence rule,
        because guessing which one the caller meant is worse than saying so.

        Leading and trailing whitespace is trimmed on both forms before the key
        is stored or compared. The 1–255 character limit applies to that trimmed
        value; a key that is empty after trim is refused.
      schema:
        type: string
        minLength: 1
        maxLength: 255
  schemas:
    PurchaseRefundedRequest:
      type: object
      required:
        - ucc
        - occurredAt
        - newUser
        - currency
        - invoiceId
        - amount
      properties:
        ucc:
          type: string
          minLength: 1
          maxLength: 255
          description: The unique Cello code that attributes this event to a referral.
          examples:
            - ucc_7f3d9a
        occurredAt:
          $ref: '#/components/schemas/Timestamp'
        newUser:
          $ref: '#/components/schemas/NewUser'
        idempotencyKey:
          type: string
          minLength: 1
          maxLength: 255
          description: >-
            The body form of the `Idempotency-Key` header. See that header for
            the

            full description. Leading and trailing whitespace is trimmed; the
            1–255

            character limit applies after trim.
          examples:
            - purchase-inv_456
        currency:
          $ref: '#/components/schemas/Currency'
        invoiceId:
          type: string
          minLength: 1
          maxLength: 255
          description: |-
            The payment being reported or reversed. Any stable identifier from
            your billing system is acceptable, as long as the refund for a
            purchase carries the same one the purchase did. Required on both
            money endpoints, because without it a refund cannot be matched to
            what it reverses.
          examples:
            - inv_456
        subscription:
          $ref: '#/components/schemas/Subscription'
        amount:
          $ref: '#/components/schemas/MinorUnitAmount'
          minimum: 1
          description: |-
            The positive magnitude of the refund. The event type already
            says this is a reversal, so a negative amount is a mistake
            rather than a direction, and a zero-value refund is not an
            event.
      unevaluatedProperties: false
      title: Purchase refunded request
    AcceptedResponse:
      type: object
      required:
        - eventId
        - receivedAt
      properties:
        eventId:
          type: string
          format: uuid
          description: >-
            A UUIDv7 assigned by Cello. It is the identifier of the stored event

            (`id` on the stream record). Quote it in support conversations.
            Never

            accepted from a request body.
          examples:
            - 019205ff-8e00-7cc0-9f2e-2b4c1d5a6e70
        receivedAt:
          type: string
          format: date-time
          description: |-
            When Cello received the event, from our clock. Compare it with your
            `occurredAt` to see the skew we saw. Never accepted from a request
            body.
          examples:
            - '2026-08-05T14:14:35.123Z'
        warnings:
          type: array
          items:
            $ref: '#/components/schemas/Warning'
          minItems: 1
          description: |-
            Non-fatal observations about an accepted event. Absent entirely when
            there is nothing to report, rather than present and empty.

            A warning never changes the status code and never changes how the
            event is processed: an event carrying warnings is stored, attributed
            and rewarded exactly like one without.

            New codes may appear at any time without a version bump, so treat
            an unrecognised code as informational rather than failing on it.
      unevaluatedProperties: false
      title: Accepted event
    Problem:
      type: object
      required:
        - type
        - title
        - status
        - detail
        - code
      properties:
        type:
          type: string
          format: uri
          description: |-
            A stable identifier for the class of problem. It doubles as a
            documentation URL, but treat it as an identifier: compare it, do not
            fetch it.
          examples:
            - https://docs.cello.so/api-reference/errors/validation-error
        title:
          type: string
          description: A short, unchanging summary of the problem class.
          examples:
            - Validation error
        status:
          type: integer
          description: The HTTP status code, repeated here so a logged body stands alone.
          examples:
            - 422
        detail:
          type: string
          description: What went wrong with this specific request.
        instance:
          type: string
          description: |-
            The request identifier. Quote it in a support ticket and we can find
            the exact request in our logs.
          examples:
            - Rk8xMjM0NTY3ODkw
        code:
          type: string
          enum:
            - MALFORMED_JSON
            - UNAUTHORIZED
            - UNKNOWN_EVENT_TYPE
            - PAYLOAD_TOO_LARGE
            - VALIDATION_ERROR
            - IDEMPOTENCY_CONFLICT
            - IDEMPOTENCY_KEY_CONFLICT
            - INTERNAL_ERROR
            - SERVICE_UNAVAILABLE
          description: >-
            The machine-readable Cello error code. Branch on this rather than on

            `title` or `detail`, which are written for people and may be
            reworded.
          examples:
            - VALIDATION_ERROR
        errors:
          type: array
          items:
            $ref: '#/components/schemas/Violation'
          description: |-
            Every violation in the request, not just the first, so one round
            trip is enough to fix an integration. Capped at 20 entries, with
            `detail` saying so when the list was truncated.
      unevaluatedProperties: false
      description: |-
        An RFC 9457 problem document, served as `application/problem+json`. Read
        `code` to branch in software and `detail` to show a human what to fix.
      title: Problem
    Timestamp:
      type: string
      format: rfc3339-offset
      description: |-
        When the event happened in your system, as an RFC 3339 date-time with an
        explicit UTC offset: either `Z` or `±HH:MM`.

        The offset is mandatory and the `T` separator is mandatory. A local time
        with no offset is ambiguous by exactly the amount that makes referral
        attribution wrong, and `2022-10-05 14:14:34` is refused even though v1
        tolerated it.

        A syntactically valid timestamp is never refused for being implausible,
        however far it is from our clock. If it looks wrong we accept the event
        and tell you so in `warnings`, because dropping a real purchase over a
        misconfigured clock is the worse failure.
      title: RFC 3339 timestamp with an explicit offset
      examples:
        - '2026-08-05T16:14:34+02:00'
        - '2026-08-05T14:14:34Z'
    NewUser:
      type: object
      required:
        - id
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 255
          description: |-
            Your own identifier for the user. Stable across events, because it
            is how a later purchase is tied to an earlier signup.
          examples:
            - usr_1042
        email:
          type: string
          minLength: 1
          maxLength: 320
          format: email
          description: |-
            Optional. Omit the key entirely if you do not have it or cannot
            share it; do not send an empty string or `null`.
          examples:
            - ada@example.com
        name:
          type: string
          minLength: 1
          maxLength: 255
          examples:
            - Ada Lovelace
        organizationId:
          type: string
          minLength: 1
          maxLength: 255
          description: Your identifier for the organisation the user belongs to.
          examples:
            - org_88
      unevaluatedProperties: false
      description: |-
        The referred user this event is about. Only `id` is required; the rest
        improves what Cello can show in the portal.
      title: Referred user
    Currency:
      type: string
      format: iso4217
      description: |-
        An uppercase ISO 4217 alphabetic code, validated against the code list
        rather than against a three-letter pattern, so `EUR` is accepted and
        `eur` and `XYZ` are both refused with a message saying which rule they
        broke.

        The same code selects the minor-unit exponent that interprets `amount`
        in documentation (JPY is ¥1580, not 158000). The exponent is not used
        to rescale v1 `payload.price`; v1 is already in minor units.
      title: ISO 4217 currency code
      examples:
        - JPY
        - USD
        - EUR
      x-cello-data-table: ./iso4217.json#/codes
    Subscription:
      type: object
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 255
          examples:
            - sub_99
        interval:
          type: string
          enum:
            - one-time
            - weekly
            - bi-weekly
            - monthly
            - quarterly
            - semi-annual
            - yearly
            - biennial
            - triennial
            - lifetime
          description: |-
            The billing interval. Required on purchase-created. Optional on
            purchase-refunded. A closed list; unrecognised values are refused.
          examples:
            - monthly
        seatsUsed:
          type: integer
          minimum: 0
          description: Seats occupied under the subscription at the time of the event.
          examples:
            - 3
        productKey:
          type: string
          minLength: 1
          maxLength: 255
          description: Your identifier for the plan or product purchased.
          examples:
            - team-plan
      unevaluatedProperties: false
      description: |-
        Detail about the subscription the payment belongs to. Required on
        purchase-created, where `interval` is required. Optional on
        purchase-refunded, where every field is optional.
      title: Subscription detail
    MinorUnitAmount:
      type: integer
      maximum: 9007199254740991
      description: |-
        The amount as a whole number of the currency's minor unit, never a float
        and never a string. 49.00 EUR is `4900`; 1580 JPY is `1580`, because the
        yen has no minor unit.

        Integers are required because a float cannot represent every decimal
        amount exactly, and a rounding error in a purchase amount becomes a
        rounding error in someone's commission. Nothing is coerced: `49.00` and
        `"4900"` are both refused rather than converted.
      title: Amount in minor units
      examples:
        - 4900
    Warning:
      type: object
      required:
        - code
        - detail
      properties:
        code:
          type: string
          description: |-
            `OCCURRED_AT_IN_FUTURE` when `occurredAt` is more than five minutes
            ahead of our clock, `OCCURRED_AT_BACKDATED` when it is older than
            the backdating window, and `FIELD_NOT_YET_HONOURED` for a field that
            is part of the contract but not yet processed. The idempotency key
            is honoured and does not produce that warning.
          examples:
            - OCCURRED_AT_BACKDATED
        detail:
          type: string
          description: A human-readable sentence explaining the warning.
        pointer:
          type: string
          description: |-
            A JSON Pointer to the field the warning concerns, when it concerns
            one.
          examples:
            - '#/occurredAt'
      unevaluatedProperties: false
      title: Warning
    Violation:
      type: object
      required:
        - pointer
        - code
        - detail
      properties:
        pointer:
          type: string
          description: A JSON Pointer to the offending field, relative to the request body.
          examples:
            - '#/currency'
        code:
          type: string
          enum:
            - REQUIRED
            - INVALID_TYPE
            - INVALID_FORMAT
            - INVALID_ENUM
            - OUT_OF_RANGE
            - UNKNOWN_FIELD
          examples:
            - INVALID_FORMAT
        detail:
          type: string
          description: |-
            A sentence naming what was wrong and what would be right. For an
            unknown field that looks like a v1 field name, it names the v2 field
            you probably meant.
          examples:
            - currency must be an uppercase ISO 4217 code, for example EUR.
      unevaluatedProperties: false
      title: Violation
  securitySchemes:
    BearerAuth:
      type: http
      scheme: Bearer

````