Skip to main content

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 — your customer_number, derived from the connector instance.
  • Content-Type: application/json is 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

MethodWhen 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:

ExceptionModuleWhen it fires
ApiErrorlib.api_clientThe HTTP call reached Nuntiq and the server returned a non-2xx response (or the call couldn't reach the server at all).
ValidationErrorlib.objects.exceptionsA Load class rejected your payload before the HTTP call — required fields missing, wrong type.
NotFoundErrorlib.objects.exceptionsA Load class lookup found no matching record.
AuthErrorlib.objects.exceptionsReserved 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:

AttributeTypeMeaning
e.status_codeintHTTP status, or 0 when the request couldn't reach the server
e.response_bodystrRaw 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.
  • 4xx except 429 → 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.

What's next

  • BaseLoad — read/write patterns shared by every entity.
  • Logging — how to surface what your client calls did.