Bulk Enrol Devices via the API¶
Script the same bulk-enrolment flow the portal exposes. Useful for fleet-scale onboardings (1000+ devices) where the portal upload form isn't ergonomic.
Who can call these endpoints
Tenant administrators only — the same role gate as the portal. Authenticate with a bearer token issued to a tenant-admin user.
Endpoints¶
All paths are relative to https://dev-au-03.nnnco.io/api/v4/registry/.
| Method | Path | Purpose |
|---|---|---|
POST |
bulk-enrolments/ |
Submit a CSV |
GET |
bulk-enrolments/{batch_id}/ |
Status + per-row results |
GET |
bulk-enrolments/{batch_id}/failures/ |
Download the failed rows as CSV |
Submit¶
POST /api/v4/registry/bulk-enrolments/
A multipart upload with two fields:
| Field | Required | Description |
|---|---|---|
enterprise_id |
yes | UUID of the target enterprise. Must be in your accessible-enterprises tree. |
csv_file |
yes | UTF-8 CSV body. Max 5 MB. Format described below. |
Example¶
curl -X POST https://dev-au-03.nnnco.io/api/v4/registry/bulk-enrolments/ \
-H "Authorization: Bearer $TOKEN" \
-F "enterprise_id=00000000-0000-0000-0000-000000000000" \
-F "csv_file=@devices.csv"
CSV format¶
Each row enrols one device. Five columns, mixing OTAA and ABP
device types in the same file is allowed. The activation mode and
the meaning of each key column follow from the resolved device type
(its device_type_code).
| Column | OTAA device type | ABP device type |
|---|---|---|
dev_eui |
required (16 hex) | required (16 hex) |
dev_addr |
blank | required (8 hex) |
device_type_code |
required | required |
key_1 |
AppKey (32 hex) | NwkSKey (32 hex) |
key_2 |
blank | AppSKey (32 hex) |
Example:
AABBCCDDEEFF0001,,LS-300-OTAA,AABBCCDDEEFF00112233445566778899,
AABBCCDDEEFF0002,00ABC123,LS-300-ABP,AABBCCDDEEFF00112233445566778899,DDEEFF00112233445566778899AABBCC
Keys are forwarded directly to the network server. They are never stored on this platform and cannot be retrieved after enrolment.
Cellular and NB-IoT device types
Bulk enrolment supports LoRaWAN device types in this release. Cellular and NB-IoT device types will get their own enrolment flow once that adapter ships — enrol them via the single-device API for now.
Response¶
202 Accepted — the upload was validated and forwarded. The response
body is the batch in state=running; poll the status endpoint until
is_terminal is true.
{
"id": "11111111-2222-3333-4444-555555555555",
"enterprise_id": "00000000-0000-0000-0000-000000000000",
"enterprise_code": "acme.us-east",
"state": "running",
"is_terminal": false,
"total_rows": 250,
"succeeded_rows": 0,
"failed_rows": 0,
"submitted_at": "2026-05-28T01:23:45Z",
"last_polled_at": null,
"completed_at": null,
"rows": [...],
"row_count_truncated": false
}
The Location response header points at the status endpoint for this
batch.
Errors¶
| Status | When |
|---|---|
400 |
CSV body missing/malformed; unknown enterprise; duplicate or oversized DevEUI in CSV. |
403 |
Not a tenant admin, or enterprise_id outside your accessible tree. |
413 |
CSV exceeds 5 MB. Split into multiple uploads. |
502 |
Network server rejected the import (transient). |
Error bodies have a detail (human message) and code (machine token):
Get batch status¶
GET /api/v4/registry/bulk-enrolments/{batch_id}/
Returns the current state of the batch plus the per-row breakdown.
This endpoint lazy-polls the upstream network server on each call
when the batch is non-terminal — you don't need a separate poll
endpoint, calling this is the poll. Repeat the call until
is_terminal is true.
Polling tapers off automatically: once the batch is older than 30 minutes and still non-terminal, the server returns the last-known state without polling the network server again. Refresh manually later if needed.
Response shape¶
{
"id": "11111111-2222-3333-4444-555555555555",
"enterprise_id": "00000000-0000-0000-0000-000000000000",
"enterprise_code": "acme.us-east",
"state": "partial",
"is_terminal": true,
"total_rows": 250,
"succeeded_rows": 248,
"failed_rows": 2,
"submitted_at": "2026-05-28T01:23:45Z",
"last_polled_at": "2026-05-28T01:24:01Z",
"completed_at": "2026-05-28T01:24:01Z",
"rows": [
{
"row_index": 17,
"device_eui": "AABBCCDDEEFF0011",
"op_type": "OTAA",
"result": "error",
"error_code": "111",
"error_message": "duplicate DevEUI",
"created_device_id": null
}
],
"row_count_truncated": false
}
op_type is the device's activation mode (OTAA or ABP),
derived from the resolved device type. error_code is an opaque
token extracted from the upstream response — treat as text, not as
a number; values may be numeric (e.g. "111") or symbolic depending
on the upstream message format.
The rows array is capped at 500 entries to bound the response size.
If row_count_truncated is true, download the failures CSV (next
endpoint) for the full set.
Errors¶
| Status | When |
|---|---|
403 |
Not a tenant admin. |
404 |
Batch not found, or not in your accessible-enterprises tree. |
503 |
Database temporarily unavailable. Retry with exponential back-off. |
Download failures CSV¶
GET /api/v4/registry/bulk-enrolments/{batch_id}/failures/
Streams only the failed rows as CSV. Useful for the fix-and-resubmit loop: download, correct the failing rows (keys, DevEUIs, or device-type codes), and POST the corrected CSV.
Response¶
200 OK, Content-Type: text/csv, Content-Disposition: attachment.
row_index,op_type,device_eui,error_code,error_message
17,OTAA,AABBCCDDEEFF0011,111,duplicate DevEUI
42,ABP,AABBCCDDEEFF0042,222,invalid AppSKey length
- Capped at the same row limit as the portal export. If the failures
exceed that cap, the filename includes
-partial-{N}.csv. - Cell values starting with
=,+,-,@are prefixed with a single quote so spreadsheet applications don't interpret them as formulas.
Putting it together¶
Typical scripted flow:
# 1. Submit the CSV.
BATCH=$(curl -s -X POST https://dev-au-03.nnnco.io/api/v4/registry/bulk-enrolments/ \
-H "Authorization: Bearer $TOKEN" \
-F "enterprise_id=$ENTERPRISE_ID" \
-F "csv_file=@devices.csv" \
| jq -r '.id')
# 2. Poll until terminal (back off the calls — imports take seconds
# to minutes depending on size).
while true; do
STATE=$(curl -s -H "Authorization: Bearer $TOKEN" \
"https://dev-au-03.nnnco.io/api/v4/registry/bulk-enrolments/$BATCH/" \
| jq -r '.state')
case "$STATE" in
succeeded|partial|failed) break ;;
esac
sleep 5
done
# 3. If any rows failed, pull the failures CSV.
if [ "$STATE" != "succeeded" ]; then
curl -s -H "Authorization: Bearer $TOKEN" \
"https://dev-au-03.nnnco.io/api/v4/registry/bulk-enrolments/$BATCH/failures/" \
-o failures.csv
fi
Related¶
- Enrol devices through the portal — the same workflow with a UI.
- API reference — authentication, error envelopes, rate limits.