BackAPI reference
- Overview
- Authentication
- Errors and limits
- Code examples
- Moving from Classic
- KPIs
- KPI Categories
- KPI Categories / KPIs
- Breakdown Items
- KPI Entries
- KPI Frequencies
- KPI Icons
- KPI Units
- Reports
- Users
SimpleKPI AI › API reference
Code examples
Every example below authenticates the same way — an API key as a bearer token. Create one at Settings → API Access first; seeAuthentication.
Read the key from an environment variable or your secret store rather than pasting it into source. A key committed to a repository has to be revoked and replaced, not just removed.
curl
List your KPIs
curl https://api.simplekpi.com/v1/kpis \ -H "Authorization: Bearer $SIMPLEKPI_TOKEN"
Push a value
curl https://api.simplekpi.com/v1/kpientries \
-X POST \
-H "Authorization: Bearer $SIMPLEKPI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"kpi_id":1234,"entry_date":"2026-08-03","actual":42,"setActual":true}'Push a day's worth in one request
curl https://api.simplekpi.com/v1/kpientries/list \
-X POST \
-H "Authorization: Bearer $SIMPLEKPI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"hasActuals": true,
"entries": [
{"kpi_id":1234,"entry_date":"2026-08-03","actual":42,"setActual":true},
{"kpi_id":1235,"entry_date":"2026-08-03","actual":17,"setActual":true}
]
}'On Windows, cmd needs the body wrapped in double quotes with the inner quotes escaped: -d "{\"kpi_id\":1234}". PowerShell and Git Bash take the single-quoted form above.
C#
One helper covering every call, in the spirit of theClassic example but against the new authentication scheme.
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
public sealed class SimpleKpiClient(string token) : IDisposable
{
private readonly HttpClient _http = new()
{
BaseAddress = new Uri("https://api.simplekpi.com/v1/"),
DefaultRequestHeaders = { Authorization = new AuthenticationHeaderValue("Bearer", token) }
};
public async Task<T?> SendAsync<T>(HttpMethod method, string path, object? body = null)
{
using var request = new HttpRequestMessage(method, path);
if (body is not null) request.Content = JsonContent.Create(body);
using var response = await _http.SendAsync(request);
// Honour Retry-After rather than hammering the endpoint that just said no.
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
var wait = response.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(30);
await Task.Delay(wait);
return await SendAsync<T>(method, path, body);
}
if (!response.IsSuccessStatusCode)
{
// Failures are problem+json, not the resource you asked for.
var problem = await response.Content.ReadAsStringAsync();
throw new HttpRequestException($"{(int)response.StatusCode} {path}: {problem}");
}
if (response.StatusCode == HttpStatusCode.NoContent) return default;
return await response.Content.ReadFromJsonAsync<T>();
}
public void Dispose() => _http.Dispose();
}Using it
using var client = new SimpleKpiClient(
Environment.GetEnvironmentVariable("SIMPLEKPI_TOKEN")!);
var kpis = await client.SendAsync<List<Kpi>>(HttpMethod.Get, "kpis");
await client.SendAsync<KpiEntry>(HttpMethod.Post, "kpientries", new
{
kpi_id = 1234,
entry_date = "2026-08-03",
actual = 42,
setActual = true,
});Python
import os
import time
import requests
BASE = "https://api.simplekpi.com/v1"
SESSION = requests.Session()
SESSION.headers["Authorization"] = f"Bearer {os.environ['SIMPLEKPI_TOKEN']}"
def call(method, path, body=None):
while True:
response = SESSION.request(method, f"{BASE}/{path}", json=body, timeout=30)
if response.status_code == 429:
time.sleep(int(response.headers.get("Retry-After", 30)))
continue
if not response.ok:
# Failures carry problem+json, so the detail is worth surfacing.
raise RuntimeError(f"{response.status_code} {path}: {response.text}")
return None if response.status_code == 204 else response.json()
kpis = call("GET", "kpis")
call("POST", "kpientries", {
"kpi_id": 1234,
"entry_date": "2026-08-03",
"actual": 42,
"setActual": True,
})Worth knowing
- Writes are upserts. Sending the same
kpi_id,source_idandentry_datetwice updates the entry rather than creating a second one, so a job that reruns is safe. - Batch rather than loop. One call to the batch endpoint beats hundreds of single writes, and keeps you well inside the rate limits.
- Check the batch counts. A batch where every row was rejected still returns
200. Readrows_rejected. - PUT replaces. Read the object, change what you need, send the whole thing back — anything omitted is reset rather than kept.