Skip to content

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:create and vouchers: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:

Shell
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
curl "https://nonox.app/api/v1/company" \
  -H "Authorization: Bearer $NONOX_TOKEN" \
  -H "Accept: application/json"
PHP (Laravel)
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');
PHP (Guzzle)
$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'];
JavaScript
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();
Python
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
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
    }
  ]
}'
PHP (Laravel)
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');
PHP (Guzzle)
$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'];
JavaScript
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();
Python
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
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)"
PHP (Laravel)
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');
PHP (Guzzle)
$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'];
JavaScript
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();
Python
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
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"
PHP (Laravel)
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');
PHP (Guzzle)
$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'];
JavaScript
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();
Python
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