Skip to content
PROTOTYPE — MOCK DATAFigures are generated fixtures derived from the registry. Not ACF actuals.
Menu (13)
FROZEN NOW · BUILT IN S6

One envelope, three doors

The console, the CSV upload and the future SDK all speak this contract. Freezing it now is what makes them one system instead of three — and it is why the CSV door needed no separate validation path, no separate audit trail and no separate set of rules.

ENDPOINTS
Push API endpoints
METHODPATHPURPOSE
POST/v1/oauth/tokenSigned JWT assertion → 60-minute access token
GET/v1/projects/{id}/datasets/{ds}/schemaThe contract, versioned
POST/v1/projects/{id}/datasets/{ds}:pushHeaders Idempotency-Key, X-ACF-Period. Returns 202 + receipt_id
GET/v1/receipts/{receiptId}Status, per-row errors
GET/v1/projects/{id}/deliveries?from=&to=The expectation calendar — a client can check its own history
THE ENVELOPE
{
  "dataset": "telesales_calls_daily",
  "period": "2026-09-22", "grain": "day", "org_unit": "SALES-TS",
  "submitted_at": "2026-09-23T02:04:11+07:00",
  "producer": "stringeex-exporter/1.4.0",
  "rows": [
    { "metric_id": "TS-01", "org_unit": "SALES-TS",
      "period": "2026-09-22", "value": 738769, "unit": "count" }
  ],
  "row_count": 1,
  "checksum": "sha256:4c1e…"
}

A CSV row becomes one rows[] entry. There is one validation path, one audit trail, one set of rules — which door the data came through is provenance, not a separate pipeline.

ERROR CODES ARE PART OF THE CONTRACT
  • 400schema_invalid

    The payload does not match the dataset version. The whole batch is refused.

  • 409period_closed

    That period is locked. Reopen it through the metric owner, not through a retry.

  • 409restatement_requires_reason

    A value already exists and differs. Send `reason` — this raises a trend break marker.

  • 422value_implausible

    Outside the plausibility range in the registry. Quarantined, with an alert.

  • 423project_suspended

    The circuit breaker tripped. A human must resume it — retrying will not.

  • 429rate_limited

    Back off. Retry-After is set.

  • 503retry_after

    We are down, not you. The idempotency key makes your retry safe.

TYPESCRIPT SDK — GENERATED FROM THE OPENAPI SPEC
import { AcfKpi } from '@acf/kpi-sdk'

const client = new AcfKpi({ keyFile: './acf-kpi-service.json' })

// signs the assertion, retries idempotently, and validates against the
// cached schema BEFORE the network call — so a bad batch fails here,
// on your laptop, not in our quarantine.
const receipt = await client
  .dataset('telesales_calls_daily')
  .push({ period: '2026-09-22', rows })

if (receipt.state === 'rejected') {
  console.table(receipt.errors)   // your file rows, not batch offsets
}
PYTHON SDK
from acf_kpi import Client

client = Client.from_key_file("acf-kpi-service.json")

receipt = client.dataset("finance_monthly_upload").push(
    period="2026-09", rows=rows,
)
receipt.raise_for_status()
WHY AN SDK EARNS ITS PLACE OVER RAW REST

Three things the REST contract cannot do on its own: it signs the JWT assertion, it retries idempotently, and it validates against the cached schema before the network call — so a malformed batch fails on the department's laptop rather than in our quarantine, and the person who can fix it is the person looking at the error. Both are generated from the OpenAPI spec, so an SDK cannot drift from the contract. A .NET SDK only if a source team asks for one.

Versioning: a breaking dataset change creates v2 and v1 keeps being accepted for 90 days, with the countdown on the project page. Nobody's exporter breaks overnight because we tightened a type.