Guides
Getting started
This guide takes you from nothing to a posted voucher through the API. You need a company in NONOX where you are owner or administrator, and a passkey on your account. Try it in a company of your own first; there is no separate sandbox yet.
1. Turn on NONOX Connect
The API is a module and is off by default. Open Inställningar → Moduler (Settings → Modules) and turn on NONOX Connect. While the module is off, every call for the company answers 404, as if the API did not exist.
In the module's settings a company can also limit which apps may connect and which IP addresses may call the API.
2. Create an API key
For a script of your own, an API key is the simplest. Open Anslutningar (Connections) and create a key with:
- a name, for example "Test script";
- the scopes
vouchers:view,vouchers:createandvouchers:post; - an expiry date (required, at most a year ahead).
Confirm with your passkey. The key starts with nnx_ and is shown once; copy it right away. Keep it in an environment variable so it never ends up in your code:
export NONOX_TOKEN="nnx_..."If a program used by many companies is connecting, use OAuth instead: Authentication (OAuth).
3. See which company the key belongs to
GET /company needs no particular scope and answers with the company and the key. It is a good first call to see that everything works.
curl "https://nonox.app/api/v1/company" \
-H "Authorization: Bearer $NONOX_TOKEN" \
-H "Accept: application/json"use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.nonox.token'))
->acceptJson()
->get('https://nonox.app/api/v1/company');
$response->throw();
$data = $response->json('data');$client = new \GuzzleHttp\Client(['timeout' => 30]);
$response = $client->request('GET', 'https://nonox.app/api/v1/company', [
'headers' => [
'Authorization' => 'Bearer '.getenv('NONOX_TOKEN'),
'Accept' => 'application/json',
],
]);
$data = json_decode((string) $response->getBody(), true, flags: JSON_THROW_ON_ERROR)['data'];const response = await fetch('https://nonox.app/api/v1/company', {
headers: {
Authorization: `Bearer ${process.env.NONOX_TOKEN}`,
Accept: 'application/json',
},
});
if (!response.ok) {
const problem = await response.json();
throw new Error(`${problem.code}: ${problem.detail}`);
}
const { data } = await response.json();import os
import requests
response = requests.get(
"https://nonox.app/api/v1/company",
headers={
"Authorization": f"Bearer {os.environ['NONOX_TOKEN']}",
"Accept": "application/json",
},
timeout=30,
)
response.raise_for_status()
data = response.json()["data"]4. Create a draft
A voucher starts as a draft. Amounts are whole öre: 125000 is SEK 1,250.00. The rows are checked as in the app (accounts that exist and are active, debit or credit on each row); the balance is checked when the draft is posted.
Every POST needs an Idempotency-Key, so a retry after a network error never creates two drafts. More in Idempotency.
curl -X POST "https://nonox.app/api/v1/vouchers" \
-H "Authorization: Bearer $NONOX_TOKEN" \
-H "Accept: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"voucher_date": "2026-03-02",
"description": "Kontantförsäljning",
"series": "A",
"rows": [
{
"account": 1930,
"debit_ore": 125000
},
{
"account": 3001,
"credit_ore": 100000
},
{
"account": 2611,
"credit_ore": 25000
}
]
}'use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
$response = Http::withToken(config('services.nonox.token'))
->acceptJson()
->withHeaders(['Idempotency-Key' => (string) Str::uuid()])
->post('https://nonox.app/api/v1/vouchers', [
'voucher_date' => '2026-03-02',
'description' => 'Kontantförsäljning',
'series' => 'A',
'rows' => [
[
'account' => 1930,
'debit_ore' => 125000,
],
[
'account' => 3001,
'credit_ore' => 100000,
],
[
'account' => 2611,
'credit_ore' => 25000,
],
],
]);
$response->throw();
$data = $response->json('data');$client = new \GuzzleHttp\Client(['timeout' => 30]);
$response = $client->request('POST', 'https://nonox.app/api/v1/vouchers', [
'headers' => [
'Authorization' => 'Bearer '.getenv('NONOX_TOKEN'),
'Accept' => 'application/json',
'Idempotency-Key' => bin2hex(random_bytes(16)),
],
'json' => [
'voucher_date' => '2026-03-02',
'description' => 'Kontantförsäljning',
'series' => 'A',
'rows' => [
[
'account' => 1930,
'debit_ore' => 125000,
],
[
'account' => 3001,
'credit_ore' => 100000,
],
[
'account' => 2611,
'credit_ore' => 25000,
],
],
],
]);
$data = json_decode((string) $response->getBody(), true, flags: JSON_THROW_ON_ERROR)['data'];const response = await fetch('https://nonox.app/api/v1/vouchers', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NONOX_TOKEN}`,
Accept: 'application/json',
'Idempotency-Key': crypto.randomUUID(),
'Content-Type': 'application/json',
},
body: JSON.stringify({
"voucher_date": "2026-03-02",
"description": "Kontantförsäljning",
"series": "A",
"rows": [
{
"account": 1930,
"debit_ore": 125000
},
{
"account": 3001,
"credit_ore": 100000
},
{
"account": 2611,
"credit_ore": 25000
}
]
}),
});
if (!response.ok) {
const problem = await response.json();
throw new Error(`${problem.code}: ${problem.detail}`);
}
const { data } = await response.json();import os
import uuid
import requests
response = requests.post(
"https://nonox.app/api/v1/vouchers",
headers={
"Authorization": f"Bearer {os.environ['NONOX_TOKEN']}",
"Accept": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"voucher_date": "2026-03-02",
"description": "Kontantförsäljning",
"series": "A",
"rows": [
{
"account": 1930,
"debit_ore": 125000,
},
{
"account": 3001,
"credit_ore": 100000,
},
{
"account": 2611,
"credit_ore": 25000,
},
],
},
timeout=30,
)
response.raise_for_status()
data = response.json()["data"]The answer is 201 Created with the draft. Keep data.id.
5. Post the draft
Posting gives the voucher the next number in its series with no gaps, checks that it balances and that its date is in an open fiscal year outside locked periods, and writes the audit trail. It cannot be undone: a posted voucher can only be corrected with a correcting voucher.
curl -X POST "https://nonox.app/api/v1/vouchers/0b9f3c1e-6a2d-4f7b-9c8e-1d2a3b4c5d6e/post" \
-H "Authorization: Bearer $NONOX_TOKEN" \
-H "Accept: application/json" \
-H "Idempotency-Key: $(uuidgen)"use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
$response = Http::withToken(config('services.nonox.token'))
->acceptJson()
->withHeaders(['Idempotency-Key' => (string) Str::uuid()])
->post('https://nonox.app/api/v1/vouchers/0b9f3c1e-6a2d-4f7b-9c8e-1d2a3b4c5d6e/post');
$response->throw();
$data = $response->json('data');$client = new \GuzzleHttp\Client(['timeout' => 30]);
$response = $client->request('POST', 'https://nonox.app/api/v1/vouchers/0b9f3c1e-6a2d-4f7b-9c8e-1d2a3b4c5d6e/post', [
'headers' => [
'Authorization' => 'Bearer '.getenv('NONOX_TOKEN'),
'Accept' => 'application/json',
'Idempotency-Key' => bin2hex(random_bytes(16)),
],
]);
$data = json_decode((string) $response->getBody(), true, flags: JSON_THROW_ON_ERROR)['data'];const response = await fetch('https://nonox.app/api/v1/vouchers/0b9f3c1e-6a2d-4f7b-9c8e-1d2a3b4c5d6e/post', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NONOX_TOKEN}`,
Accept: 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
});
if (!response.ok) {
const problem = await response.json();
throw new Error(`${problem.code}: ${problem.detail}`);
}
const { data } = await response.json();import os
import uuid
import requests
response = requests.post(
"https://nonox.app/api/v1/vouchers/0b9f3c1e-6a2d-4f7b-9c8e-1d2a3b4c5d6e/post",
headers={
"Authorization": f"Bearer {os.environ['NONOX_TOKEN']}",
"Accept": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
timeout=30,
)
response.raise_for_status()
data = response.json()["data"]6. Read it back
List posted vouchers, or fetch only what changed since last time with updated_since. See Pagination and sync.
curl "https://nonox.app/api/v1/vouchers?limit=50&updated_since=2026-03-01T00%3A00%3A00%2B01%3A00" \
-H "Authorization: Bearer $NONOX_TOKEN" \
-H "Accept: application/json"use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.nonox.token'))
->acceptJson()
->get('https://nonox.app/api/v1/vouchers', [
'limit' => 50,
'updated_since' => '2026-03-01T00:00:00+01:00',
]);
$response->throw();
$data = $response->json('data');$client = new \GuzzleHttp\Client(['timeout' => 30]);
$response = $client->request('GET', 'https://nonox.app/api/v1/vouchers', [
'headers' => [
'Authorization' => 'Bearer '.getenv('NONOX_TOKEN'),
'Accept' => 'application/json',
],
'query' => [
'limit' => 50,
'updated_since' => '2026-03-01T00:00:00+01:00',
],
]);
$data = json_decode((string) $response->getBody(), true, flags: JSON_THROW_ON_ERROR)['data'];const url = new URL('https://nonox.app/api/v1/vouchers');
url.searchParams.set('limit', '50');
url.searchParams.set('updated_since', '2026-03-01T00:00:00+01:00');
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${process.env.NONOX_TOKEN}`,
Accept: 'application/json',
},
});
if (!response.ok) {
const problem = await response.json();
throw new Error(`${problem.code}: ${problem.detail}`);
}
const { data } = await response.json();import os
import requests
response = requests.get(
"https://nonox.app/api/v1/vouchers",
headers={
"Authorization": f"Bearer {os.environ['NONOX_TOKEN']}",
"Accept": "application/json",
},
params={
"limit": 50,
"updated_since": "2026-03-01T00:00:00+01:00",
},
timeout=30,
)
response.raise_for_status()
data = response.json()["data"]When something goes wrong
Every error is application/problem+json with a stable code and a Swedish explanation in detail. Always branch on code. A key that lacks a scope gets 403 insufficient_scope with required_scopes. Every code is listed in Errors.
Next steps
- Scopes: which scopes open which endpoints.
- The API reference: every endpoint with parameters, schemas and examples.
- Rate limits: how many calls per minute are allowed.