Public API Reference
Read your receipts, and the raw text behind them, from your own code.
This is the public API. Bearer keys, the Pro Plus plan, read access to your own data.
Create and revoke keys in /settings.
Introduction
Base URL and Versioning
Every endpoint sits under /api/v1 and every path ends in a slash. Without the slash you get a 308 and pay for a second round trip — harmless if your client follows redirects, and broken if it does not. The version is in the path, so a breaking change arrives as a new one rather than as a field that quietly changes meaning.
https://skanota.app/api/v1/
# every path ends in a slash # without it you get a 308 and a second round trip GET https://skanota.app/api/v1/me -> 308 Location: /api/v1/me/ GET https://skanota.app/api/v1/me/ -> 200
Authentication
Send your key as a bearer token on every request. Keys are shown once when you create them and cannot be recovered afterwards. Each key carries its own scopes and its own rate-limit budget, so a leaked key can be revoked without disturbing your other integrations.
curl https://skanota.app/api/v1/me/ \ -H "Authorization: Bearer rs_live_..."
{
"success": true,
"data": {
"userId": "0c8f2b1e-4a77-4c1e-9a2b-6f4d1e9c33aa",
"tier": "pro_plus",
"scopes": ["receipts:read", "receipts:write"],
"scanCredits": {
"limit": 500,
"used": 128,
"purchased": 40,
"remaining": 412,
"resetsAt": "2026-09-01T00:00:00.000Z"
}
}
}Errors
Every refusal answers the same envelope: success is false, error is a sentence for a person, and code is a stable identifier for a program. Branch on code — the prose gets improved. Authentication failures are deliberately indistinguishable from one another, so a missing key, an unknown one and a revoked one all read the same.
curl -i https://skanota.app/api/v1/receipts/ \ -H "Authorization: Bearer rs_live_wrong"
HTTP/2 401
www-authenticate: Bearer realm="Skanota API"
{
"success": false,
"error": "Invalid or missing API key",
"code": "UNAUTHORIZED"
}
# codes you can branch on
# UNAUTHORIZED 401 no key, malformed, unknown, revoked or expired
# FORBIDDEN 403 valid key, account below the Pro Plus plan
# INSUFFICIENT_SCOPE 403 key does not carry the scope this route needs
# NOT_FOUND 404 no such receipt for this account
# INVALID_PARAMETER 400 a query parameter was not one of the accepted values
# RATE_LIMITED 429 budget spent; see Retry-After
# INTERNAL_ERROR 500Rate Limits
600 requests an hour, counted per key rather than per account — one runaway integration cannot throttle the others. Successful responses carry the remaining budget and a 429 carries Retry-After in seconds.
curl -i https://skanota.app/api/v1/receipts/ \ -H "Authorization: Bearer rs_live_..."
HTTP/2 200
x-ratelimit-limit: 600
x-ratelimit-remaining: 597
# and when the budget is spent
HTTP/2 429
retry-after: 2841
{ "success": false, "error": "Rate limit exceeded", "code": "RATE_LIMITED" }Receipts
The Receipt Object
One scanned receipt. Every endpoint that returns receipts returns this shape, and so does the archive export, so there is one format to write a parser against. It carries its own schema version on every object rather than only on the envelope — an exported line has to be able to say what it is once it has been split from the rest.
Attributes
schemastring- The format version of this object. Currently skanota.receipt.v1.
idstring- The receipt's identifier, and what you pass to the retrieve endpoints.
merchant.namestringnullable- The shop or restaurant name, as printed.
merchant.addressstringnullable- The address printed on the receipt, when there is one.
merchant.phonestringnullable- The phone number printed on the receipt, when there is one.
receiptNumberstringnullable- The transaction or receipt number the merchant printed.
transactionDatestringnullable- The date and time printed on the paper, with no timezone. It is a wall-clock label rather than an instant: the model can read the clock on the receipt but cannot know the shop's timezone, so do not attach one.
amounts.totalstringnullable- The final total. A decimal string, not a number — do not put it through a float.
amounts.taxstringnullable- Tax, when the receipt separates it.
amounts.subtotalstringnullable- The total before tax and service charge, when printed.
amounts.discountstringnullable- Any discount applied, when printed.
amounts.serviceChargestringnullable- Service charge, when printed.
amounts.currencystring- Always IDR. Stated so a consumer does not have to infer it from the digits.
category.builtInstringnullable- One of the built-in category keys, when the receipt has one.
category.customIdstringnullable- The identifier of a category the account created, when the receipt is filed under one instead.
notesstringnullable- Whatever the account owner typed against this receipt.
tagsarray- The receipt's tags. Always an array — an absent list and an empty one mean the same thing, so you never have to branch on which you got.
isVerifiedboolean- Whether somebody has checked the extracted fields against the paper.
ocr.confidencenumbernullable- How confident the model was overall, between 0 and 1.
ocr.rawTextstringnullable- Everything the model read off the receipt, unparsed. This is the field that makes the archive worth keeping.
ocr.fieldCountinteger- How many individual fields the model located on the image.
imagePathstringnullable- Where to fetch the image, or null when the receipt has none. It is a path on this API, never the underlying storage address.
createdAtstringnullable- When the receipt was scanned. A real instant, as an ISO string.
updatedAtstringnullable- When the receipt was last edited. A real instant, as an ISO string.
{
"schema": "skanota.receipt.v1",
"id": "8f14e45f-ceea-467a-9a36-dedd4bea2543",
"merchant": {
"name": "GRAND LUCKY SUPERSTORE",
"address": "Jl. Jend. Sudirman Kav. 52-53, Jakarta",
"phone": "+62215155678"
},
"receiptNumber": "TRX-20260812-0043",
"transactionDate": "2026-08-12T19:24:00",
"amounts": {
"total": "184500.00",
"tax": "18450.00",
"subtotal": "166050.00",
"discount": null,
"serviceCharge": null,
"currency": "IDR"
},
"category": { "builtIn": "grocery", "customId": null },
"notes": null,
"tags": [],
"isVerified": true,
"ocr": {
"confidence": 0.94,
"rawText": "GRAND LUCKY SUPERSTORE\n...",
"fieldCount": 11
},
"imagePath": "/api/v1/receipts/8f14e45f-ceea-467a-9a36-dedd4bea2543/image/",
"createdAt": "2026-08-12T12:31:07.412Z",
"updatedAt": "2026-08-12T12:31:07.412Z"
}List Receipts
GET/api/v1/receipts
Returns a page of receipts, newest first unless you ask otherwise. It accepts the same filters the Skanota app itself uses, so a filter saved in the product and a query written here mean the same thing.
Parameters
pageintegeroptionalWhich page to return, starting at 1.
Defaults to: 1
limitintegeroptionalHow many receipts to return per page. Anything above the ceiling is clamped to it rather than refused.
Defaults to: 20
sortenumoptionalThe order to return receipts in.
One of: newest · oldest · highest · lowest
Defaults to: newest
statusenumoptionalReturn only verified receipts, or only unverified ones. Omit for both.
One of: verified · pending
categorystringoptionalA built-in category key, or custom:<id> for one the account created. The prefix is what keeps the two apart. A value matching neither is ignored rather than refused.
monthstringoptionalA single calendar month, written YYYY-MM.
searchstringoptionalMatched against the merchant name and the receipt number. Anything past 255 characters is truncated, since a longer needle cannot match either column.
Returns
A list of receipt objects, and a pagination block giving the page, the page size, the total number of matches and whether more pages follow.
curl "https://skanota.app/api/v1/receipts/?limit=2&sort=highest" \ -H "Authorization: Bearer rs_live_..."
{
"success": true,
"data": [
{
"schema": "skanota.receipt.v1",
"id": "8f14e45f-ceea-467a-9a36-dedd4bea2543",
"merchant": {
"name": "GRAND LUCKY SUPERSTORE",
"address": "Jl. Jend. Sudirman Kav. 52-53, Jakarta",
"phone": "+62215155678"
},
"receiptNumber": "TRX-20260812-0043",
"transactionDate": "2026-08-12T19:24:00",
"amounts": {
"total": "184500.00",
"tax": "18450.00",
"subtotal": "166050.00",
"discount": null,
"serviceCharge": null,
"currency": "IDR"
},
"category": { "builtIn": "grocery", "customId": null },
"notes": null,
"tags": [],
"isVerified": true,
"ocr": {
"confidence": 0.94,
"rawText": "GRAND LUCKY SUPERSTORE\n...",
"fieldCount": 11
},
"imagePath": "/api/v1/receipts/8f14e45f-ceea-467a-9a36-dedd4bea2543/image/",
"createdAt": "2026-08-12T12:31:07.412Z",
"updatedAt": "2026-08-12T12:31:07.412Z"
}
],
"pagination": { "page": 1, "limit": 2, "total": 418, "hasMore": true }
}Retrieve a Receipt
GET/api/v1/receipts/{id}
Returns one receipt by id, including the raw text the model read. A receipt belonging to another account answers 404 rather than 403 — a 403 would confirm the identifier exists.
Parameters
This endpoint takes no query parameters.
Returns
A single receipt object.
curl https://skanota.app/api/v1/receipts/8f14e45f-ceea-467a-9a36-dedd4bea2543/ \ -H "Authorization: Bearer rs_live_..."
{ "success": true, "data": {
"schema": "skanota.receipt.v1",
"id": "8f14e45f-ceea-467a-9a36-dedd4bea2543",
"merchant": {
"name": "GRAND LUCKY SUPERSTORE",
"address": "Jl. Jend. Sudirman Kav. 52-53, Jakarta",
"phone": "+62215155678"
},
"receiptNumber": "TRX-20260812-0043",
"transactionDate": "2026-08-12T19:24:00",
"amounts": {
"total": "184500.00",
"tax": "18450.00",
"subtotal": "166050.00",
"discount": null,
"serviceCharge": null,
"currency": "IDR"
},
"category": { "builtIn": "grocery", "customId": null },
"notes": null,
"tags": [],
"isVerified": true,
"ocr": {
"confidence": 0.94,
"rawText": "GRAND LUCKY SUPERSTORE\n...",
"fieldCount": 11
},
"imagePath": "/api/v1/receipts/8f14e45f-ceea-467a-9a36-dedd4bea2543/image/",
"createdAt": "2026-08-12T12:31:07.412Z",
"updatedAt": "2026-08-12T12:31:07.412Z"
} }Retrieve a Receipt Image
GET/api/v1/receipts/{id}/image
Streams the image bytes. This is the address the imagePath field points at, and it is served through the API rather than from storage, so the underlying file address is never published. Ask for a resized copy when you are drawing a list — the stored image is up to 1500 pixels on its longest side.
Parameters
wenumoptionalReturn a resized copy instead of the stored image. The shorter side comes back at least this many pixels, whichever way up the receipt is. Only the listed values are accepted — anything else is refused rather than rounded to the nearest.
One of: 160 · 320
Returns
The image itself, not JSON. Cached privately for an hour, because the bytes for an identifier never change.
curl https://skanota.app/api/v1/receipts/8f14e45f-ceea-467a-9a36-dedd4bea2543/image/?w=320 \ -H "Authorization: Bearer rs_live_..." \ -o receipt.jpg
HTTP/2 200 content-type: image/jpeg cache-control: private, max-age=3600 referrer-policy: no-referrer <binary>
Archive
List the Archive
GET/api/v1/archive
Every receipt on the account, a page at a time and with no filters. This is the endpoint for mirroring an account; use List receipts when you want to search one.
Parameters
pageintegeroptionalWhich page to return, starting at 1.
Defaults to: 1
limitintegeroptionalHow many receipts to return per page. Higher than the list endpoint allows, because this one exists for mirroring an account rather than for filling a screen.
Defaults to: 50
Returns
A list of receipt objects, and a pagination block.
curl "https://skanota.app/api/v1/archive/?page=1&limit=100" \ -H "Authorization: Bearer rs_live_..."
{
"success": true,
"data": [ /* receipt objects */ ],
"pagination": { "page": 1, "limit": 100, "total": 418, "hasMore": true }
}Export the Archive
GET/api/v1/archive/export
The whole archive in one response, as one JSON object per line. Line-delimited rather than a single array so a consumer can read it a receipt at a time instead of holding an entire account in memory before the first byte is valid.
Parameters
formatenumoptionalThe response format.
One of: jsonl · json
Defaults to: jsonl
Returns
A line-delimited JSON body, or one JSON object containing a count and an array when format is json.
curl https://skanota.app/api/v1/archive/export/ \ -H "Authorization: Bearer rs_live_..." > archive.jsonl
content-type: application/x-ndjson; charset=utf-8
{"schema":"skanota.receipt.v1","id":"8f14e45f-...","merchant":{...}}
{"schema":"skanota.receipt.v1","id":"1c383cd3-...","merchant":{...}}
{"schema":"skanota.receipt.v1","id":"9f9d51bc-...","merchant":{...}}Analytics
Retrieve Analytics
GET/api/v1/analytics
Spending aggregated by month, by category and by merchant over a period. Deliberately narrower than the figures the Analytics page shows: several of those panels are shaped by what reads well on a chart rather than by what is true of the data, and a presentation decision has no business in an API contract.
Parameters
periodenumoptionalHow far back to aggregate.
One of: 1month · 3months · 6months · 1year · all
Defaults to: 6months
categorystringoptionalA built-in category key, or custom:<id> for one the account created. The prefix is what keeps the two apart. A value matching neither is ignored rather than refused.
Returns
The period that was applied, the instant it starts at, and three aggregates: monthly, categories and merchants.
curl "https://skanota.app/api/v1/analytics/?period=3months" \ -H "Authorization: Bearer rs_live_..."
{
"success": true,
"data": {
"period": "3months",
"since": "2026-05-24T00:00:00.000Z",
"monthly": [
{ "month": "2026-06", "total": "4812000.00", "count": 61 },
{ "month": "2026-07", "total": "5140500.00", "count": 74 }
],
"categories": [
{ "category": "grocery", "total": "3910000.00", "count": 52 }
],
"merchants": [
{ "merchant": "GRAND LUCKY SUPERSTORE", "total": "1284000.00", "count": 12 }
]
}
}Account
Retrieve the Account
GET/api/v1/me
Who the key belongs to, which plan the account is on, what the key is allowed to do and how much scan quota is left. Call it first: a 200 means the credential works, and a 401 or 403 tells you which problem you have without spending a real request to find out.
Parameters
This endpoint takes no query parameters.
Returns
The account identifier, its tier, the scopes on the key presented, and the scan credit balance with the date it resets.
curl https://skanota.app/api/v1/me/ \ -H "Authorization: Bearer rs_live_..."
{
"success": true,
"data": {
"userId": "0c8f2b1e-4a77-4c1e-9a2b-6f4d1e9c33aa",
"tier": "pro_plus",
"scopes": ["receipts:read", "receipts:write"],
"scanCredits": {
"limit": 500,
"used": 128,
"purchased": 40,
"remaining": 412,
"resetsAt": "2026-09-01T00:00:00.000Z"
}
}
}