REST API pull
The most common connector: read records that changed in the customer's
system since the last run and write them into Nuntiq. This page builds
get_supplier for an ERP with a REST API. The erp_rest_*
templates contain this pattern for every
master-data entrypoint; start there for a real connector.
The scenario
The ERP lists vendors at GET /api/v1/vendors?modifiedSince=...&page=N,
100 per page, oldest change first, with an API key in the X-API-Key
header. Each vendor has an id, a number, a name, an address and bank
accounts. The ERP answers 429 when called too often.
Settings and secrets
| Name | Where | Example |
|---|---|---|
connector_name | setting | acme_erp (names the delta cursor; unique per connector) |
source_system | setting | ACME_ERP (the identity written on the suppliers) |
base_url | setting | https://erp.example.com/api/v1 |
lookback_days | setting | 30 (first run only) |
api_key | secret | the ERP's API key |
The connector
"""get_supplier: ERP vendors -> Nuntiq suppliers (changes since the last run)."""
from lib.helpers import Settings, RunSummary, HttpClient, ApiKeyAuth, incremental, country_code, utc_iso
from lib.objects.supplier import SupplierLoad
def run(context):
s = Settings(context)
summary = RunSummary(context)
erp = HttpClient(s.str('base_url'), auth=ApiKeyAuth(s.secret('api_key')), context=context, name='ERP')
load = SupplierLoad(context, source_system=s.source_system)
with incremental(context, 'suppliers') as run:
for v in erp.paginate('vendors', params={'modifiedSince': run.since}, items='data',
style='page', page_size=100):
sup = load.new()
sup.source_record_id = str(v['id'])
sup.supplier_number = v['number']
sup.supplier_name = v['name']
loc = sup.new_location()
loc.location_code = 'MAIN'
addr = loc.new_address()
addr.street = v.get('street')
addr.city = v.get('city')
addr.country_code = country_code(v.get('country'))
run.seen(utc_iso(v['modifiedAt']))
summary.add('suppliers')
load.save_all()
return summary.result(cursor=run.new_cursor)
What the helpers do for you
- Settings:
s.str('base_url')fails the run with "base_url is required" when it is empty;s.secret('api_key')the same for the secret. - Retries: a 429 waits as long as the ERP's
Retry-Aftersays and tries again; 502, 503, 504 and dropped connections are retried with growing waits. A GET is always safe to repeat. - Paging:
paginate()followspage=1,2,3until a short or empty page. Other APIs usestyle='offset','cursor','next_url'(for exampleitems='value', next_url='@odata.nextLink'for Microsoft APIs) or'link_header'. - The cursor:
run.sinceis where this run starts (the job parametersince_override, else the cursor of the last successful run, else now minuslookback_days). The next run starts at the highest value passed torun.seen(). When the run fails, the cursor stays where it was and the next run reads the same changes again. Saving is idempotent, so that is safe. - Big backlogs:
incremental(context, 'suppliers', max_records=5000)and checkrun.fullin the loop; the next run continues where this one stopped. The source must list oldest first for that.
A login other than an API key
Only the auth= changes. For OAuth 2 client credentials:
auth = OAuth2ClientCredentials(s.str('token_url'), s.secret('client_id'), s.secret('client_secret'),
scope=s.str('scope', None), cache=TokenCache(context))
Every login method is in Helpers and in its own template. See also OAuth 2.0.
Testing it locally
Put the settings and secrets in a settings file of the
toolkit, point base_url at the ERP's sandbox and run
python nuntiq.py <settings> get_supplier. The suppliers land in the
toolkit's fake Nuntiq (fixtures/supplier.json); nothing reaches a real
tenant.
Deployment notes
- The ERP must accept calls from Nuntiq's outbound IP address on port 443.
- Schedule
get_supplierwith a scheduler task of typeconnector_dispatch. For a full reload, set the job parametersince_overrideon a one-off run. - Give every record its id in the source system (
source_record_id): the next run updates the same supplier instead of creating a second one.