Skip to main content

nmbrs-rest-api

Python SDK for the Nmbrs public REST API, with cached debtor, company and employee objects.

PyPI Python versions License CI

Status: usable, pre-1.0. Every read endpoint is implemented and tested; writes are not. Responses come back as plain dicts for now — typed models are next. The API may still change before 1.0.

Install · Quickstart · Authentication · Reading data · Errors · Scopes · Example project · Reporting bugs

Install

pip install nmbrs-rest-api

The distribution is nmbrs-rest-api; the import name is nmbrs_rest. See Relationship to the SOAP SDK for why they differ.

Python 3.10+. You will need OAuth credentials and a subscription key from the Nmbrs Developer Portal.

Quickstart

from nmbrs_rest import Nmbrs

api = Nmbrs(client_id, client_secret, subscription_key)

# 1. Send the user here to approve. Nmbrs supports only the authorization code
#    flow, so this needs a human — once.
url = api.login("https://yourapp.example/callback")

# 2. Hand back the code from the redirect.
api.authenticate(code)

# 3. Persist these. The refresh token is good for 30 days.
api.access_token
api.refresh_token

Next time, skip the browser entirely:

api = Nmbrs(client_id, client_secret, subscription_key, refresh_token=saved)

for company in api.companies():
    for employee in api.employees(company.id):
        print(employee.full_name, employee.contracts())

There is a complete, runnable version of this in example/.

Authentication

Nmbrs offers the OAuth 2.0 authorization code flow and nothing else — no client credentials, no API-key-only mode. The first token always requires a person to approve in a browser.

First run

api = Nmbrs(
    client_id, client_secret, subscription_key,
    redirect_uri="http://localhost:4000",     # must be registered on your app
    on_token_refresh=save_tokens,             # see below
)

url = api.login(scopes=["offline_access", "company.info.read", "employee.info.read"])
# ... user approves, browser redirects to your redirect_uri with ?code=...
api.authenticate(code, state=state_from_the_redirect)

Include offline_access or no refresh token is issued and everything stops working in an hour. The default scope set includes it.

Refresh tokens rotate

Nmbrs invalidates the old refresh token every time it issues a new one. access_token and refresh_token are live properties rather than snapshots, so they always reflect the current values — but if your app persists them, ask to be told:

def save_tokens(token):
    db.store(token.access_token, token.refresh_token, token.expires_at)

api = Nmbrs(client_id, client_secret, subscription_key,
            refresh_token=saved, on_token_refresh=save_tokens)

Persisting a stale refresh token is the single most common way a Nmbrs integration dies overnight. The SDK refreshes before expiry, refreshes again if the server rejects a token, replays the request, and persists the rotated token before using it.

Web apps

login() remembers the CSRF state, the PKCE verifier and the redirect URI on the client, which is all a script needs. A web app handles the redirect in a different request, so pass them back:

url = api.login(redirect_uri, scopes, pkce=True)
session["state"] = api.pending_login.state
session["verifier"] = api.pending_login.code_verifier

# ... later, in the callback handler ...
api.authenticate(code, state=session["state"], code_verifier=session["verifier"])

Also available: api.refresh() to force a refresh, api.logout() to revoke upstream and clear local state, and api.scopes for what was actually granted (which can be narrower than what you asked for).

Full contract: docs/authentication.md.

Reading data

Getting around

api.debtors()                          # -> list[Debtor]
api.companies()                        # -> list[Company]
api.employees(company_id)              # -> list[Employee]
api.employee(company_id, employee_id)  # -> Employee, one request

api.company(company_id)                # -> Company, no request yet
api.debtor(debtor_id)                  # -> Debtor,  no request yet

Collections are eager — every page is fetched before the list comes back.

api.company(id) returns the same object each time, so its cache survives across your own code.

Both ids are required for an employee. Nmbrs exposes no employee-to-company lookup, so an employee id on its own cannot reach its own data.

Reads are methods

33 of the 38 employee endpoints take a filter — year, period, created_from — which an attribute cannot express. So every read is a method, and the parentheses mark where I/O happens:

employee = api.employee(company_id, employee_id)

employee.contracts()
employee.salaries()
employee.addresses()
employee.fixed_hours(year=2026, period=3)
employee.leave_requests(year=2026, status="Approved", request_type="Holiday")
employee.contracts(created_from=date(2020, 1, 1))    # pass real dates

employee.details()   # personalInfo, manager, department, function, address — 1 request

Identity fields come free with the company listing, so reading names costs nothing:

employee.id, employee.company_id
employee.first_name, employee.last_name, employee.full_name
employee.employee_number, employee.employee_type

Company and debtor reads work the same way:

company.period()                 # current payroll period
company.wage_taxes(year=2026)
company.cost_centers()
company.leave_groups()

debtor.companies()               # -> list[Company]
debtor.managers()
debtor.tags()

There are 60 generated read methods in total — 34 on Employee, 19 on Company, 7 on Debtor — derived from the official OpenAPI spec, with CI failing if they drift from it.

Caching

Per object, keyed on the filter arguments, not just the endpoint:

employee.contracts()                              # request
employee.contracts()                              # cached
employee.contracts(created_from=date(2026, 1, 1)) # different question, own entry

employee.cached      # how many distinct reads are held
employee.refresh()   # drop them

Two identical calls make one request; a different filter is a different question and is never served a stale answer.

Anything not surfaced

company.raw          # the listing row, exactly as the API sent it
employee.raw

api.transport.get_json("/api/countries")   # any endpoint, authenticated

Errors

Every failure is a subclass of NmbrsError, and messages never contain tokens — redaction happens when the exception is built, so they are safe to log and safe to paste into an issue.

from nmbrs_rest.errors import (
    InsufficientScopeError,
    NmbrsError,
    RateLimitError,
    ReauthorizationRequired,
    ResourceNotFoundError,
)

try:
    employee.salaries()
except InsufficientScopeError as exc:
    print("re-consent with:", exc.missing)
except ResourceNotFoundError:
    ...
except RateLimitError as exc:
    print("retry after", exc.retry_after)
except ReauthorizationRequired:
    ...   # refresh token dead — send the user through consent again
except NmbrsError as exc:
    ...   # everything else

A 403 names the scope you are missing. The spec declares required scopes per operation, so the SDK can tell you which one to re-consent with instead of saying "Forbidden":

InsufficientScopeError: [403/40303] GET /api/companies/ad3562fc/employees/salaries: no detail provided

  This operation accepts any of:  employee.employment, employee.employment.read
  Your token was granted:         company.info.read, employee.info.read

  Scope is fixed at consent time and cannot be widened for an existing
  token. Re-run the authorization flow including one of the scopes above.

Rate limits and transient 5xx are retried automatically with jittered backoff. Quota exhaustion (QuotaExceededError) is not retried, because it would not help.

Every error code and what to do about it: docs/errors.md.

Scopes

from nmbrs_rest import ALL_READ_SCOPES, DEFAULT_SCOPES

api.login(redirect_uri, DEFAULT_SCOPES)              # small starter set
api.login(redirect_uri, sorted(ALL_READ_SCOPES))     # every read scope + offline_access

Ask for what you use. Requesting sixteen scopes, or a write scope you will never call, looks bad on a customer's consent screen — and scope cannot be widened later without re-consent.

Scope names do not reliably track resource names: GET /api/debtors requires company.info*, not debtor.info.read. When you get it wrong, the 403 tells you exactly which scope to add.

What this hides

Nmbrs' REST API is company-scoped and inconsistent in ways that leak into every integration written against it. The SDK absorbs that:

  • Responses are wrapped twice. Everything arrives as {"data": [...]}, even a single record, and employee reads are wrapped again as {"employeeId": ..., "contracts": [...]} — with a payload key that differs per endpoint. You get the payload.
  • Three endpoints ignore employeeId. privateInfos, extraFields and useraccounts return the whole company whatever you ask for. The SDK filters them client-side so employee.private_info() means what it says.
  • /api/companies refuses multi-debtor consent. If your token spans several debtors that endpoint returns a 403; api.companies() catches it and walks debtors instead, transparently.
  • Paging is manual. Every list read follows pages until they run out.
  • Dates need ISO-8601 strings. Pass date/datetime objects instead.

Example project

example/ is a small runnable project:

File What it shows
main.py Consent in the browser, exchange the code, save the refresh token
resume.py Running headless from the saved token — a cron job or backend service
explore.py Filters, caching, errors worth catching, raw access
callback.py A tiny HTTP server that catches the OAuth redirect
cd example
pip install -r requirements.txt
cp .env.example .env      # then fill in your credentials
python main.py

Features

  • Automatic pagination — every list read follows pages until they run out.
  • Per-object caching — keyed on the filter arguments, so the same question is asked once and a different question is not served a stale answer.
  • OAuth 2.0 handled end to endlogin(), authenticate(), refresh-on-401 with replay, and rotation persisted before use.
  • Meaningful errors — a 403 tells you which scope is missing for which operation, not just "Forbidden".
  • Typed — ships py.typed; read methods generated from the official OpenAPI spec, with CI failing if they drift from it.

Reporting bugs

Please open an issue. This SDK is new and the API it wraps is unversioned and occasionally surprising, so real-world reports are how the rough edges get found. Fixes have already shipped because someone hit a 403 the SDK described badly.

→ Open an issue

Useful to include:

  • What you called, and what you expected to happen.
  • The full exception. Tokens are redacted before the message is built, so exception text is safe to paste. Do not paste your .env, a raw token, or your subscription key.
  • nmbrs_rest.__version__ and your Python version.

Feature requests and questions are welcome in the same place. If you would rather fix it yourself, see CONTRIBUTING.md.

Documentation

Upstream API reference: Nmbrs Public REST API.

Scope of this release

Reads only. All 84 GET operations are implemented: 34 employee reads, 19 company reads, 7 debtor reads, plus the listings. The 39 write operations are planned for a later release.

Nmbrs offers no sandbox. Development and testing need a Nmbrs demo environment, which does not expire.

Relationship to the SOAP SDK

Nmbrs has two APIs, and this project covers one of them:

Package Import Covers
SOAP nmbrs (repo) nmbrs The legacy SOAP API, retiring 2027-03-01
REST nmbrs-rest-api (this one) nmbrs_rest The current REST API

Both are by the same author. The import names differ deliberately so the two can be installed side by side while you migrate. The REST API does not yet cover everything SOAP does, so a period of running both is expected.

A note on upstream stability

The Nmbrs REST API is unversioned. Nmbrs ships additive changes without notice and without a version bump. New fields simply appear — reads return dicts, so they arrive without breaking anything. A field removed or retyped upstream is a real break, and regenerating from the spec surfaces it as a failing check rather than a silent change.

License

Apache-2.0. See LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nmbrs_rest_api-0.0.3.tar.gz (142.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nmbrs_rest_api-0.0.3-py3-none-any.whl (101.2 kB view details)

Uploaded Python 3

File details

Details for the file nmbrs_rest_api-0.0.3.tar.gz.

File metadata

  • Download URL: nmbrs_rest_api-0.0.3.tar.gz
  • Upload date:
  • Size: 142.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for nmbrs_rest_api-0.0.3.tar.gz
Algorithm Hash digest
SHA256 fac966af4969e47181801a9608f3d8e3863b1c824cdcac1a987880603d045e7e
MD5 3a2005eeb969d9f2ae8846aedd23bb31
BLAKE2b-256 1153fb4fb8ecba5221ed760311a7b85fb11a073eef058a5ed03203e3bc243db2

See more details on using hashes here.

File details

Details for the file nmbrs_rest_api-0.0.3-py3-none-any.whl.

File metadata

  • Download URL: nmbrs_rest_api-0.0.3-py3-none-any.whl
  • Upload date:
  • Size: 101.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for nmbrs_rest_api-0.0.3-py3-none-any.whl
Algorithm Hash digest
SHA256 cff44cc3327a9fcd13ee2908303ec54d25639e2e1e12de027dd67f02d4aa8c7e
MD5 0642c934ead1abfea8018abfe31bb44b
BLAKE2b-256 c58d817932e58595955f14e6c519b3621892183dc86ca4dc45d459a13f666f76

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.0.3 This release

2 files

0.0.2

2 files

0.0.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page