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

# Bulk Record Revenue

> Record multiple revenue attributions in a single request.

Record up to 1,000 revenue events in a single API call. Each record follows the same schema as the single revenue endpoint. The response includes per-record error details so you can identify and retry failures individually.

## Authentication

<ParamField header="X-Api-Key" type="string" required>
  Your Trackly SMS API key. Format: `trk_[32-char-hex]`.
</ParamField>

## Body Parameters

<ParamField body="records" type="array" required>
  An array of revenue records to process. Maximum of 1,000 records per request. Each record accepts the following fields:

  <Expandable title="Record fields">
    <ParamField body="message_id" type="string" required>
      An opaque, variable-length message identifier. This is the same value passed via the `{{sendId}}` macro. Do not assume a fixed length or format.
    </ParamField>

    <ParamField body="revenue" type="float" required>
      Revenue amount. Must be greater than or equal to `0`.
    </ParamField>

    <ParamField body="attribution_type" type="string" required>
      One of: `sale`, `click`, or `send`.
    </ParamField>

    <ParamField body="offer_id" type="string">
      The offer ID this revenue is associated with. Can be either the `externalId` or the offer ID generated by TracklySMS. If omitted, the system will attempt to resolve the offer from the message's short link.
    </ParamField>

    <ParamField body="timestamp" type="datetime">
      ISO 8601 timestamp. Assumed to be UTC if no timezone is provided. Defaults to the current time.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="imported" type="boolean">
  Accepted for forward compatibility but not currently used — the value is ignored and has no effect on how records are stored or attributed.
</ParamField>

## Response Fields

<ResponseField name="success_count" type="integer">
  Number of revenue records successfully created.
</ResponseField>

<ResponseField name="error_count" type="integer">
  Number of records that failed validation or processing.
</ResponseField>

<ResponseField name="total_revenue" type="float">
  Sum of all successfully processed revenue amounts.
</ResponseField>

<ResponseField name="errors" type="array">
  Array of error objects for failed records. Each object contains:

  * `index` (integer) -- Position of the failed record in the input array.
  * `code` (string) -- Machine-readable error code.
  * `error` (string) -- Human-readable error description.
</ResponseField>

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST https://api.tracklysms.com/api/v2/revenue/bulk \
      -H "X-Api-Key: trk_your_api_key_here" \
      -H "Content-Type: application/json" \
      -d '{
        "records": [
          {
            "message_id": "a1b2c3d4",
            "revenue": 24.99,
            "offer_id": "offer_123",
            "attribution_type": "sale"
          },
          {
            "message_id": "e5f6g7h8",
            "revenue": 12.50,
            "offer_id": "offer_456",
            "attribution_type": "click"
          }
        ],
        "imported": false
      }'
    ```

    <Note>
      `message_id` is an opaque identifier — do not assume a fixed length or format. Store them as variable-length strings.
    </Note>

    ```python Python theme={null}
    import requests

    response = requests.post(
        "https://api.tracklysms.com/api/v2/revenue/bulk",
        headers={
            "X-Api-Key": "trk_your_api_key_here",
            "Content-Type": "application/json",
        },
        json={
            "records": [
                {
                    "message_id": "a1b2c3d4",
                    "revenue": 24.99,
                    "offer_id": "offer_123",
                    "attribution_type": "sale",
                },
                {
                    "message_id": "e5f6g7h8",
                    "revenue": 12.50,
                    "offer_id": "offer_456",
                    "attribution_type": "click",
                },
            ],
            "imported": False,
        },
    )

    print(response.json())
    ```

    ```javascript Node.js theme={null}
    const response = await fetch("https://api.tracklysms.com/api/v2/revenue/bulk", {
      method: "POST",
      headers: {
        "X-Api-Key": "trk_your_api_key_here",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        records: [
          {
            message_id: "a1b2c3d4",
            revenue: 24.99,
            offer_id: "offer_123",
            attribution_type: "sale",
          },
          {
            message_id: "e5f6g7h8",
            revenue: 12.5,
            offer_id: "offer_456",
            attribution_type: "click",
          },
        ],
        imported: false,
      }),
    });

    const data = await response.json();
    console.log(data);
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 201 - Success theme={null}
  {
    "success_count": 2,
    "error_count": 0,
    "total_revenue": 37.49,
    "errors": []
  }
  ```

  ```json 201 - Partial Success theme={null}
  {
    "success_count": 1,
    "error_count": 1,
    "total_revenue": 24.99,
    "errors": [
      {
        "index": 1,
        "code": "message_not_found",
        "error": "Message ID not found"
      }
    ]
  }
  ```

  ```json 400 - Validation Error theme={null}
  {
    "code": "missing_records",
    "error": "records array is required"
  }
  ```

  ```json 413 - Payload Too Large theme={null}
  {
    "code": "too_many_records",
    "error": "Maximum 1000 records per request"
  }
  ```
</ResponseExample>

## Error Codes

Only three conditions reject the whole request. Every other error is reported **per record**: the request still returns `201`, and each failed record appears in the response's `errors[]` array with a `code` (see the Partial Success example above).

### Request errors (HTTP status)

| HTTP Status | Error Code         | Description                                                    |
| ----------- | ------------------ | -------------------------------------------------------------- |
| 400         | `missing_records`  | The `records` field is required and must be a non-empty array. |
| 400         | `invalid_body`     | The request body must be a JSON object.                        |
| 413         | `too_many_records` | Exceeded the maximum of 1,000 records per request.             |

Authenticated requests can also fail with `401 invalid_credentials`, `403 account_suspended`, or `429 rate_limited` — see [Error Codes](/api-reference/v2/error-codes).

### Per-record errors (`errors[].code`, returned with HTTP 201)

| Error Code                 | Description                                                                                                          |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `missing_message_id`       | A record is missing the `message_id` field.                                                                          |
| `missing_revenue`          | A record is missing the `revenue` field.                                                                             |
| `missing_attribution_type` | A record is missing the `attribution_type` field.                                                                    |
| `invalid_message_id`       | A record's `message_id` is not a string.                                                                             |
| `invalid_record`           | A record is not a JSON object.                                                                                       |
| `invalid_revenue`          | Revenue must be a number greater than or equal to 0.                                                                 |
| `invalid_attribution_type` | Must be one of: `sale`, `click`, or `send`.                                                                          |
| `message_not_found`        | No message with the given ID exists in your account (also returned when the message belongs to a different account). |

## Next Steps

<CardGroup cols={2}>
  <Card title="Revenue Tracking" icon="chart-line" href="/guides/offers/revenue-tracking">
    Track and attribute revenue
  </Card>

  <Card title="Send Message" icon="paper-plane" href="/api-reference/v2/messages/send-single">
    Send messages to drive revenue
  </Card>
</CardGroup>
