context.api — the API client
The framework gives you an authenticated HTTP client to the Nuntiq customer
API as context.api. Two ways to use it.
Two styles, same backend
1. Typed entity Loads — preferred for CRUD
from lib.objects.supplier import SupplierLoad
load = SupplierLoad(context)
for s in load.get_all():
print(s.supplier_number, s.supplier_name)
new_supplier = load.new()
new_supplier.supplier_number = 'SUP-100'
new_supplier.supplier_name = 'Acme Corp'
load.save_all()
You get pagination, dirty-tracking, batching at 1000 rows, and field accessors for free. See BaseLoad for the full pattern and entity reference for per-object field lists.
2. Low-level — for endpoints without a Load wrapper, or one-off calls
# GET
resp = context.api.get('/v1/supplier', params={'page': 1, 'limit': 100})
# {"items": [...], "count": 248, "page": 1}
# POST
context.api.post('/v1/invoices/7f3a.../lifecycle-messages', data={
'level': 'info',
'message': 'Posted to ERP',
})
# PUT, DELETE — same shape
context.api.put('/v1/some-endpoint', data={...})
context.api.delete('/v1/some-endpoint', data={...})
Every method returns the parsed JSON response, raises on 4xx/5xx, and adds the auth + customer headers automatically.
What's already authenticated
The framework constructs the client with:
Authorization: Bearer <job_token>— your short-lived job token, scoped to this run and this customer.X-Customer-Number— yourcustomer_number, derived from the connector instance.Content-Type: application/jsonis set automatically on POST/PUT/DELETE calls that carry a body.
You don't manage any of these. The token expires when the job ends — it's useless to capture and re-use it elsewhere.
Methods on context.api
| Method | When to use |
|---|---|
get(path, params=None, extra_headers=None) | Any GET. Returns parsed JSON. |
post(path, data=None, extra_headers=None) | POST with JSON body. |
put(path, data=None, extra_headers=None) | PUT with JSON body. |
delete(path, data=None, extra_headers=None) | DELETE (Nuntiq's DELETEs take a body for action+parameters filters). |
download_attachment(attachment_token) | Stream an attachment file. Returns raw bytes. |
upload_source_documents(files, ...) | Multipart upload — creates a new source document. |
upload_invoice_attachments(files) | Multipart upload — adds attachments to an existing invoice. |
The full path list is in the Customer API reference —
every endpoint your context.api.get(...) calls hits is documented there with
request/response schemas.
Error handling
Two exception families to know about:
| Exception | Module | When it fires |
|---|---|---|
ApiError | lib.api_client | The HTTP call reached Nuntiq and the server returned a non-2xx response (or the call couldn't reach the server at all). |
ValidationError | lib.objects.exceptions | A Load class rejected your payload before the HTTP call — required fields missing, wrong type. |
NotFoundError | lib.objects.exceptions | A Load class lookup found no matching record. |
AuthError | lib.objects.exceptions | Reserved for SDK-detected auth failures. Rare today — most auth issues surface as ApiError(401). |
All four inherit from a common base — ConnectFxError for ValidationError /
NotFoundError / AuthError, Exception for ApiError. There is no single
catch-all class that covers both families.
ApiError
Raised by every method on context.api when the response status is 4xx/5xx,
or when the request can't reach the server (connection refused, timeout). Has
two attributes:
| Attribute | Type | Meaning |
|---|---|---|
e.status_code | int | HTTP status, or 0 when the request couldn't reach the server |
e.response_body | str | Raw response body as decoded UTF-8. Often JSON — parse with json.loads() if you need fields out of it. |
str(e) gives "API error {status}: {message}" for logging.
import json
from lib.api_client import ApiError
try:
resp = context.api.post('/v1/supplier', data=payload)
except ApiError as e:
context.logger.warn(
f"supplier POST failed: {e.status_code}",
detail={'body': e.response_body[:500]},
)
if e.status_code == 0:
# Couldn't reach Nuntiq at all (DNS, TCP, TLS). Probably transient.
raise
if e.status_code == 400:
# Bad payload. Parse the JSON body to see which field upset the
# server, log it, and DON'T retry — the same body will fail again.
body = json.loads(e.response_body) if e.response_body else {}
context.logger.error(
f"Validation failed: {body.get('message', 'unknown')}",
step='supplier_post',
)
raise
if e.status_code in (502, 503, 504):
# Transient. Retry once after backoff if your runner supports it.
raise
raise
ValidationError
Raised by Load.save_all() and obj.save() when an entity is missing a
required field or has a wrong type. Carries a field_errors dict that maps
field names to error messages — useful when you're loading a batch and want
to log exactly which row was bad.
from lib.objects.exceptions import ValidationError
load = SupplierLoad(context)
for row in external_rows:
s = load.new()
s.supplier_number = row['number']
# NOTE: forgot to set supplier_name
try:
load.save_all()
except ValidationError as e:
context.logger.error(
f"Batch rejected: {e}",
detail={'field_errors': e.field_errors},
)
# field_errors looks like {'supplier_name': 'Required field is missing'}
raise
Validation runs at save_all() time, not at field assignment — so the whole
batch is rejected atomically. No partial writes.
NotFoundError
Currently reserved — most Load.get_*() methods return None for not-found
rather than raising. Catch it defensively if you're using a SDK helper that
documents raising it.
Retry vs. re-raise — quick rules
status_code == 0→ reach failure, retry-safe.4xxexcept429→ client-side problem, do not retry the same payload.429→ rate-limited. Back off.5xx→ server-side, usually transient, safe to retry.
If you wrap your run in delta_run(), an unhandled ApiError will fail the
delta cursor cleanly — the next scheduled run starts from the same lower
bound and naturally retries the window. You don't need an in-loop retry
unless the run is too long to throw away and restart.
When to drop to low-level
- The endpoint doesn't have a Load (lifecycle messages, delta state, custom data tables — all of which are documented here too).
- You're hitting a future endpoint the SDK doesn't know about yet.
- You want raw JSON instead of typed objects (debugging, ad-hoc scripts).
Otherwise, prefer the Load classes. They batch, they paginate, they enforce the right idioms, and your code reads like domain logic instead of HTTP plumbing.