Developers
An API built around one job: getting invoices into your books.
Upload a document and get structured, human-reviewed data back — with the general-ledger accounts already resolved against the company's own chart. Five endpoints, one API key, no SDK required.
Quickstart
Authenticate with a bearer token on every request. If you already have an account, keys are issued from Settings → API keys and shown once, at creation. The full reference, including the incremental sync loop, is at /developers/api-reference.
Send a document in
curl -X POST https://invoices.coresrp.com/ingest/invoices \
-H "Authorization: Bearer $CI_API_KEY" \
-H "X-Request-ID: $(uuidgen)" \
-F "company=vbacc0107" \
-F "file=@invoice.pdf;type=application/pdf" import os, uuid, requests
API = "https://invoices.coresrp.com"
KEY = os.environ["CI_API_KEY"]
def upload(company: str, path: str) -> str:
with open(path, "rb") as fh:
response = requests.post(
f"{API}/ingest/invoices",
headers={
"Authorization": f"Bearer {KEY}",
"X-Request-ID": uuid.uuid4().hex,
},
data={"company": company},
files={"file": (os.path.basename(path), fh, "application/pdf")},
timeout=120,
)
if response.status_code == 402:
d = response.json()["detail"]
raise SystemExit(
f"Quota exhausted: {d['invoices_created']}/{d['invoice_quota']}"
)
response.raise_for_status()
return response.json()["invoice_id"] import { readFile } from "node:fs/promises";
import { basename } from "node:path";
import { randomUUID } from "node:crypto";
const API = "https://invoices.coresrp.com";
const KEY = process.env.CI_API_KEY;
export async function upload(company, path) {
const form = new FormData();
form.set("company", company);
form.set(
"file",
new Blob([await readFile(path)], { type: "application/pdf" }),
basename(path),
);
const res = await fetch(API + "/ingest/invoices", {
method: "POST",
headers: { Authorization: "Bearer " + KEY, "X-Request-ID": randomUUID() },
body: form,
});
if (res.status === 402) {
const { detail } = await res.json();
throw new Error(
"Quota exhausted: " + detail.invoices_created + "/" + detail.invoice_quota,
);
}
if (!res.ok) throw new Error("HTTP " + res.status + ": " + (await res.text()));
const { invoice_id } = await res.json();
return invoice_id;
} using System.Net.Http.Headers;
using System.Text.Json;
var api = "https://invoices.coresrp.com";
var key = Environment.GetEnvironmentVariable("CI_API_KEY")!;
using var http = new HttpClient
{
BaseAddress = new Uri(api),
Timeout = TimeSpan.FromSeconds(120),
};
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", key);
async Task<string> UploadAsync(string company, string path)
{
using var form = new MultipartFormDataContent();
form.Add(new StringContent(company), "company");
var file = new StreamContent(File.OpenRead(path));
file.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
form.Add(file, "file", Path.GetFileName(path));
using var request =
new HttpRequestMessage(HttpMethod.Post, "/ingest/invoices") { Content = form };
request.Headers.Add("X-Request-ID", Guid.NewGuid().ToString("N"));
using var response = await http.SendAsync(request);
if ((int)response.StatusCode == 402)
throw new InvalidOperationException("Invoice quota exhausted - stop uploading.");
response.EnsureSuccessStatusCode();
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
return doc.RootElement.GetProperty("invoice_id").GetString()!;
}
A 202 means the file is stored and queued for extraction — it is your
signal to move the file out of your outbound folder.
Read reviewed invoices out
curl -G https://invoices.coresrp.com/pull/invoices \
-H "Authorization: Bearer $CI_API_KEY" \
--data-urlencode "company=vbacc0107" \
--data-urlencode "updated_since=2026-08-01T00:00:00Z" Results are cursor-paginated and ordered so that concurrent edits never cause a document to be skipped or duplicated across pages.
The endpoint surface
Deliberately small. Everything an integration needs, and nothing that exposes the rest of the platform.
| Endpoint | Purpose | Rate limit |
|---|---|---|
POST /ingest/invoices | Upload a PDF or scan for extraction and review. | 60/min |
GET /pull/companies | List the companies your key can reach. | 120/min |
POST /pull/companies | Register a company. Create-only and idempotent. | 120/min |
GET /pull/invoices | Read reviewed invoices incrementally, with GL coding. | 120/min |
POST /pull/invoices/{invoice_id}/error | Report that a document could not be posted. | 120/min |
Rate limits are keyed by source IP and request path, so the two
/pull/companies verbs share one budget. Responses carry an
x-request-id you should log — it lets us trace any individual call.
Full request and response detail for every endpoint is in the
API reference.
What you get back
Extraction is only half of it. Each invoice arrives with the debit and credit accounts already chosen for the company that owns it, so your side posts codes rather than inferring them.
- Keys are organization-scoped
- One key reaches every company in its organization, so each request names the company by slug. There is no per-company key — if you need hard isolation, ask for a separate organization.
- Money travels as strings
- Every amount, quantity and rate is a JSON string such as "1100.00". Parse them as decimals. Parsing as floats will eventually misstate a voucher by a cent.
- Post from gl_mapping
- Account codes are resolved server-side against each company's own chart of accounts, so you post the codes you are given rather than deriving them. Codes are editable per company — never hard-code them.
- Reviewed means human-approved
- The pull feed serves documents a person has checked, not raw extraction output. That is what makes the GL coding trustworthy enough to post automatically.
{
"id": "01a02806-feea-7c33-b89f-390b59c0950f",
"vendor_name": "LED HOUSE SARL",
"invoice_number": "INV-88213",
"invoice_date": "2026-08-14",
"description": "LED Panel 60x60 40W +2 more",
"currency": "USD",
"subtotal": "630.63",
"tax_total": "69.37",
"grand_total": "700.00",
"vat_applicable": true,
"gl_mapping": {
"debit_code": "60111", "debit_name": "Purchases of Goods Taxable VAT",
"credit_code": "4011", "credit_name": "Suppliers - Invoices",
"vat_code": "44210", "vat_name": "Input VAT (Purchases)"
},
"cost_center_id": "01a03e11-7c40-7a02-9f31-6b1d0c4e2b88",
"cost_center_name": "Boiler consumption",
"line_items": [ /* ... */ ]
} Three rules for a correct sync
If you poll for invoices, these are the details that decide whether your integration is reliable. They are easy to miss and expensive to discover in production.
-
Deduplicate on invoice id
The updated_since filter is inclusive, so the boundary invoice is re-served on every poll. Record the ids you have posted and skip them on sight, or you will double-post.
-
Never advance past a failure
Your watermark is the highest updated_at you successfully handled. If anything failed during a pass, leave the watermark where it was so the next poll re-pulls it.
-
Run one poller per company
Deduplication alone does not make two pollers safe: the second can advance the watermark past a document the first is still working on. Give each worker its own watermark, or run a single poller.
Building an integration?
We will send you the full endpoint reference and issue a sandbox key against a separate environment, so you never develop against live books.
Request developer access