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

# Push FOCUS data

> Send your own FOCUS billing records to CostGraph over the API

Most integrations are **pull**: you hand CostGraph a read-only credential and it
fetches your bill once a day. **Push** is the other direction. You send
[FOCUS 1.2](https://focus.finops.org) records to our API yourself, and no vendor
credential ever leaves your infrastructure.

Use it for billing data CostGraph cannot reach: an internal chargeback system, a
data warehouse job, a vendor whose export you already normalise, or a private cloud
nobody else bills for. Whatever you send lands beside every other provider in Cost
Overview, anomaly detection, and the MCP tools.

## Create the connection

1. Open **Settings -> Integrations** and choose **FOCUS push**.
2. Pick the tenant the charges belong to and name the connection.
3. Copy the **connection id**. It looks like `fpc_...` and identifies this stream.

A connection is created in push mode and stays that way. Posting to a connection
CostGraph syncs itself is refused with a `409`.

## Create the API key

1. Open **Settings -> API keys** and create a key.
2. Grant it the **`focus:write`** scope. Nothing else is required.

<Warning>
  Install tokens are rejected on this endpoint. Use an ordinary API key.
</Warning>

## Send the records

```bash theme={null}
curl -X POST https://api.costgraph.ai/api/v1/tenant/billing/focus \
  -H "Authorization: Bearer $COSTGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "connection_id": "fpc_...",
    "complete": true,
    "focus_version": "1.2",
    "charge_period_start": "2026-07-01T00:00:00Z",
    "charge_period_end": "2026-08-01T00:00:00Z",
    "rows": [
      {
        "BillingAccountId": "acct-1",
        "BillingCurrency": "USD",
        "BillingPeriodStart": "2026-07-01T00:00:00Z",
        "BillingPeriodEnd": "2026-08-01T00:00:00Z",
        "ChargePeriodStart": "2026-07-01T00:00:00Z",
        "ChargePeriodEnd": "2026-07-02T00:00:00Z",
        "ChargeCategory": "Usage",
        "ChargeDescription": "Internal platform, shared cluster",
        "Provider": "acme",
        "ServiceName": "Internal Platform",
        "BilledCost": "12.50",
        "EffectiveCost": "12.50",
        "ListCost": "15.00"
      }
    ]
  }'
```

Every row is validated, and a row that fails validation rejects the whole batch, so
nothing lands half-applied. The `422` response names the offending row index.

## Request body

| Field                 | Required | Meaning                                                                               |
| --------------------- | -------- | ------------------------------------------------------------------------------------- |
| `connection_id`       | **yes**  | The `fpc_...` id of the push connection.                                              |
| `charge_period_start` | **yes**  | Start of the window this batch describes, RFC 3339.                                   |
| `charge_period_end`   | **yes**  | End of the window. Must be after the start, and no more than 366 days later.          |
| `rows`                | **yes**  | The FOCUS records.                                                                    |
| `complete`            | no       | `true` replaces the window. Omitted or `false` appends.                               |
| `focus_version`       | no       | The FOCUS version the rows conform to, for example `1.2`. Recorded against the batch. |

## Row fields

Costs and quantities are **strings**, not numbers, so no precision is lost in JSON.
Timestamps are RFC 3339.

### Required on every row

| Field                                     | Notes                                                                          |
| ----------------------------------------- | ------------------------------------------------------------------------------ |
| `BillingAccountId`                        | Non-empty. Becomes the account the spend is attributed to.                     |
| `BillingCurrency`                         | Uppercase ISO 4217, for example `USD`. Lowercase is rejected.                  |
| `BillingPeriodStart` / `BillingPeriodEnd` | End must be after start. The invoice period the charge belongs to.             |
| `ChargePeriodStart` / `ChargePeriodEnd`   | End must be after start. Start must fall inside the window the batch declares. |
| `ChargeCategory`                          | One of `Usage`, `Purchase`, `Tax`, `Credit`, `Adjustment`.                     |
| `ChargeDescription`                       | Non-empty.                                                                     |
| `Provider`                                | Non-empty. Every row in a batch must agree, and must match the connection.     |
| `ServiceName`                             | Non-empty.                                                                     |
| `BilledCost`                              | Decimal string.                                                                |
| `EffectiveCost`                           | Decimal string. Amortised cost; this is what CostGraph reports on.             |
| `ListCost`                                | Decimal string.                                                                |

### Constrained when present

| Field                              | Constraint                                                                                                                                                                                                                                                                                          |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ChargeClass`                      | Only `Correction` is accepted.                                                                                                                                                                                                                                                                      |
| `ServiceCategory`                  | One of `AI and Machine Learning`, `Analytics`, `Business Applications`, `Compute`, `Databases`, `Developer Tools`, `Identity`, `Integration`, `Internet of Things`, `Management and Governance`, `Media`, `Migration`, `Mobile`, `Multicloud`, `Networking`, `Security`, `Storage`, `Web`, `Other`. |
| `ContractedCost`                   | Must parse as a decimal.                                                                                                                                                                                                                                                                            |
| `SubAccountName`, `SubAccountType` | Require `SubAccountId` to be set too.                                                                                                                                                                                                                                                               |

### Optional

Everything else in FOCUS 1.2 is accepted and stored, and omitting it is fine:

`AvailabilityZone`, `BillingAccountName`, `BillingAccountType`, `CapacityReservationId`,
`CapacityReservationStatus`, `ChargeFrequency`, `CommitmentDiscountCategory`,
`CommitmentDiscountId`, `CommitmentDiscountName`, `CommitmentDiscountQuantity`,
`CommitmentDiscountStatus`, `CommitmentDiscountType`, `CommitmentDiscountUnit`,
`ConsumedQuantity`, `ConsumedUnit`, `ContractedUnitPrice`, `InvoiceId`, `InvoiceIssuer`,
`ListUnitPrice`, `PricingCategory`, `PricingCurrency`,
`PricingCurrencyContractedUnitPrice`, `PricingCurrencyEffectiveCost`,
`PricingCurrencyListUnitPrice`, `PricingQuantity`, `PricingUnit`, `Publisher`,
`RegionId`, `RegionName`, `ResourceId`, `ResourceName`, `ResourceType`,
`ServiceSubcategory`, `SkuId`, `SkuMeter`, `SkuPriceId`, `SkuPriceDetails`,
`SubAccountId`, `Tags`.

<Note>
  Send `ResourceId` wherever you have one. Without it a charge can be totalled but not
  attributed to a thing, so it will not appear in resource-level breakdowns.
</Note>

Any key CostGraph does not recognise is kept as a provider extension rather than
rejected, so a dialect that carries extra columns round-trips.

## Multi-tenancy with `SubAccountId`

One connection can carry charges for many of your own customers. Set
`SubAccountId` on each row to the identifier of the customer the charge belongs
to, and optionally `SubAccountName` for a human label and `SubAccountType` to say
what kind of thing that identifier names.

```json theme={null}
{
  "BillingAccountId": "acct-1",
  "SubAccountId": "cust-4711",
  "SubAccountName": "Northwind Traders",
  "SubAccountType": "Customer",
  "ChargeDescription": "Shared cluster, allocated",
  "BilledCost": "12.50",
  "EffectiveCost": "12.50",
  "ListCost": "15.00"
}
```

`SubAccountId` is part of the identity of a row, alongside the billing account,
provider, resource and charge period. Two customers billed for the same SKU over
the same period stay separate records rather than collapsing into one, and a
replayed window updates each customer's rows in place.

<Note>
  `SubAccountName` and `SubAccountType` are only accepted when `SubAccountId` is
  set. A name without an id has nothing to attach to and the batch is rejected.
</Note>

Send the same `SubAccountId` values on every push. Changing the identifier for a
customer starts a new sub account rather than renaming the old one; the name is
free to change.

## The window and the `complete` flag

Each push declares the charge period it covers, and what you are claiming about it.

`complete: true` deletes everything previously ingested for that connection inside
the window and replaces it with this batch. That is what makes re-sending a month
idempotent, and it is how you correct a restatement: send the corrected month again.

<Warning>
  `complete: true` with an empty `rows` array is refused, because it would silently
  delete the window. Omit `complete` if you mean to append nothing.
</Warning>

Every row's `ChargePeriodStart` must fall inside the declared window, and all rows in
a batch must carry the same provider. The first push sets the connection's provider;
later pushes must agree with it.

## Batch size

The endpoint accepts **32 MB** per request by default. A larger payload is rejected
with a `413` telling you to split the batch. Narrow the window rather than dropping
rows - a month is usually the natural unit.

Only one ingest runs per connection at a time. A concurrent push gets a `409`, so
retry rather than posting two batches to one connection in parallel.

## A daily loop

Send yesterday once the day closes, with `complete: true` so a re-run is harmless:

```
POST window [yesterday 00:00Z, today 00:00Z)  complete: true
```

Then re-send the whole month after your own books close, again with `complete: true`.
The month replaces the days it covers, so late charges and restatements correct
themselves without any deletion on your side.

## What CostGraph does with it

Records land as raw billing rows, are normalised into line items, then roll up into
daily cost and reconcile against the invoice totals for each month the window
touches.

## Responses

| Status | Meaning                                                                                               |
| ------ | ----------------------------------------------------------------------------------------------------- |
| `200`  | Ingested. The body echoes the connection id, provider, row count, and whether the batch was complete. |
| `400`  | Malformed payload, or the window or rows failed validation.                                           |
| `403`  | The key is missing the `focus:write` scope.                                                           |
| `404`  | No such connection for this organization and tenant.                                                  |
| `409`  | The connection is pull-mode, or another ingest is already running.                                    |
| `413`  | Payload above the size limit. Split the batch.                                                        |
| `422`  | A row is not valid FOCUS. The message names the row index.                                            |
