SimpleKPI Logo

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_id and entry_date twice 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. Read rows_rejected.
  • PUT replaces. Read the object, change what you need, send the whole thing back — anything omitted is reset rather than kept.