---
title: "Record a sale"
method: POST
path: "/v1/sales"
tags: ["Sales"]
---

# Record a sale

`POST /v1/sales`

## Request

- Base URL: `https://api.affiliaterail.com`
- URL: `https://api.affiliaterail.com/v1/sales`
- Auth: HTTP bearer

Charge -> sale -> commission -> balance, atomically, through the same writer the processor webhooks use. Idempotent on `external_charge_id`: sending the same charge twice returns the same sale with a 200 instead of a 201. `amount_minor` is gross; tax is subtracted before the rate when the program commissions on net revenue. The response says explicitly whether money followed: `activity_outcome` is `commission_created` or `sale_recorded`, and a sale that paid nobody carries machine-readable `warnings` instead of failing the call. Customer auto-enrol runs on processor webhooks, not on this endpoint. Charge ids are unique per program, so another program using the same id never affects yours. A charge id already recorded in your program in the other mode (a test-mode id sent with a live key, or the reverse) answers 409 `external_charge_id_taken` with no sale fields.

**Scope:** `commissions:write` (or `write`)

## Request body

- SaleCreate
  - `program_id` string, required: Prefixed id (`prg_…`)
  - `customer_id` string, required: Prefixed id (`cus_…`)
  - `external_charge_id` string, required: Your charge id. One charge produces at most one sale, forever.
  - `amount_minor` integer, required: Integer minor units (cents, pence). Never a float, never a string.
  - `tax_minor` integer, nullable: Integer minor units (cents, pence). Never a float, never a string.
  - `currency` string, required: ISO-4217
  - `billing_type` 'one_time' | 'subscription'
  - `interval` 'day' | 'week' | 'month' | 'year' | 'null', nullable
  - `product_id` string, nullable
  - `product_name` string, nullable
  - `occurred_at` string, date-time: ISO-8601, UTC
  - `metadata` object, nullable: Your own key/value data, stored as given

## Response `200`

Already recorded: the existing sale

- SaleWithOutcome
  - `id` string, required: Prefixed id (`sale_…`)
  - `object` 'sale', required
  - `program_id` string, required: Prefixed id (`prg_…`)
  - `customer_id` string, required: Prefixed id (`cus_…`)
  - `partner_id` string, required: Prefixed id (`part_…`)
  - `external_charge_id` string, required
  - `external_payment_intent_id` string, nullable, required
  - `processor` 'stripe' | 'paddle' | 'chargebee' | 'manual' | 'api', required
  - `amount_minor` integer, required: Integer minor units (cents, pence). Never a float, never a string.
  - `tax_minor` integer, nullable, required: Integer minor units (cents, pence). Never a float, never a string.
  - `currency` string, required: ISO-4217
  - `billing_type` 'one_time' | 'subscription', required
  - `interval` 'day' | 'week' | 'month' | 'year' | 'null', nullable, required
  - `product_id` string, nullable, required
  - `product_name` string, nullable, required
  - `is_first_sale` boolean, required
  - `occurred_at` string, date-time, required: ISO-8601, UTC
  - `refunded_at` string, date-time, nullable, required: ISO-8601, UTC
  - `metadata` object, nullable, required: Your own key/value data, stored as given
  - `created_at` string, date-time, required: ISO-8601, UTC
  - `updated_at` string, date-time, required: ISO-8601, UTC
  - `activity_outcome` 'commission_created' | 'sale_recorded', required: `commission_created` when the sale produced a commission; `sale_recorded` when we accepted the sale and paid nobody (see `warnings` for why)
  - `warnings` WriteWarning[], required: Partial-success notes; empty when a commission was created
    - `code` 'no_partner_attributed' | 'no_commission_from_rules' | 'fixed_rate_currency_mismatch', required
    - `message` string, required

## Other responses

- `201`: The new sale
- `400`: Invalid request
- `401`: Missing or unknown API key
- `403`: Key lacks the required scope
- `404`: No such object in your organization
- `409`: external_charge_id_taken: the charge id is recorded in your program in the other mode
- `422`: The request was understood and refused
- `429`: Rate limited: see Retry-After

---

[API](https://skmtc.dev/affiliaterail/apis/affiliaterail-api.md) · [All operations](https://skmtc.dev/affiliaterail/apis/affiliaterail-api/llms.txt) · [OpenAPI document](https://skmtc.dev/affiliaterail/apis/affiliaterail-api/revisions/ea41f345c092?raw)
