Docs

TurboBulk API Reference

Authentication

All API endpoints require authentication via NetBox API token:

Authorization: Bearer <your-token>

NetBox v2 tokens (starting with nbt_) use Bearer authorization. Legacy tokens may use Token instead.

Models Endpoints

List Available Models

GET /api/plugins/turbobulk/models/

Returns a list of all available models with summary information.

Response:

[
  {
    "app_label": "dcim",
    "model_name": "device",
    "full_name": "dcim.device",
    "db_table": "dcim_device",
    "verbose_name": "device",
    "supports_custom_fields": true,
    "supports_tags": true
  },
  ...
]

Get Model Schema

GET /api/plugins/turbobulk/models/{app_label}.{model_name}/

Returns detailed schema information for a specific model.

Parameters:

  • {app_label} - Django app label (e.g., dcim)
  • {model_name} - Model name (e.g., device)

Response:

{
  "app_label": "dcim",
  "model_name": "device",
  "db_table": "dcim_device",
  "verbose_name": "device",
  "fields": [
    {
      "name": "id",
      "type": "BigAutoField",
      "db_type": "bigint",
      "arrow_type": "int64",
      "nullable": false,
      "primary_key": true,
      "unique": true,
      "foreign_key": null,
      "choices": null,
      "max_length": null,
      "default": null
    },
    {
      "name": "name",
      "type": "CharField",
      "db_type": "varchar(64)",
      "arrow_type": "string",
      "nullable": false,
      "primary_key": false,
      "unique": false,
      "foreign_key": null,
      "choices": null,
      "max_length": 64,
      "default": null
    },
    {
      "name": "site",
      "type": "ForeignKey",
      "db_type": "bigint",
      "arrow_type": "int64",
      "nullable": false,
      "primary_key": false,
      "unique": false,
      "foreign_key": "dcim.site",
      "choices": null,
      "max_length": null,
      "default": null
    }
  ],
  "primary_key_field": "id",
  "unique_constraints": [
    ["name", "tenant"]
  ],
  "supports_custom_fields": true,
  "supports_tags": true
}

Bulk Load Endpoint

Submit Bulk Load Job

POST /api/plugins/turbobulk/load/

Submits a bulk insert or upsert job.

Content-Type: multipart/form-data

Parameters:

ParameterTypeRequiredDescription
modelstringYesModel identifier (e.g., dcim.device)
modestringNoinsert (default) or upsert
filefileYesData file (JSONL, JSONL.gz, or Parquet)
formatstringNoFile format: auto (default), jsonl, or parquet
conflict_fieldsarrayNoFields for upsert conflict detection (simple columns)
conflict_constraintstringNoNamed constraint for expression-based conflicts (overrides conflict_fields)
dry_runbooleanNoValidate without committing (default: false)
create_changelogsbooleanNoGenerate ObjectChange records (default: true)
apply_save_hooksbooleanNoApply post-merge SQL fixups for computed fields (default: false). When enabled, uses bulk SQL to populate Device.location (from Rack), custom field defaults, unit normalizations (_abs_weight, _abs_length), scope cache fields, airflow/platform inheritance from DeviceType, and more. Near-zero overhead for known models. Falls back to per-object save() only for plugin models with custom save() overrides.
branchstringNoTarget branch name (requires netbox-branching)
post_hooksobjectNoHook configuration
dispatch_eventsbooleanNoOverride global event dispatch setting. true=dispatch events, false=skip events, null=use global config (default: null)

Example Request:

curl -X POST -H "Authorization: Bearer $TOKEN" \
  -F "model=dcim.device" \
  -F "mode=upsert" \
  -F "conflict_fields=name" \
  -F "conflict_fields=site_id" \
  -F "file=@devices.parquet" \
  "https://your-instance.cloud.netboxapp.com/api/plugins/turbobulk/load/"

Dry-Run Example:

curl -X POST -H "Authorization: Bearer $TOKEN" \
  -F "model=dcim.device" \
  -F "mode=insert" \
  -F "dry_run=true" \
  -F "file=@devices.parquet" \
  "https://your-instance.cloud.netboxapp.com/api/plugins/turbobulk/load/"

Load to Branch Example:

curl -X POST -H "Authorization: Bearer $TOKEN" \
  -F "model=dcim.device" \
  -F "mode=insert" \
  -F "branch=my-feature-branch" \
  -F "file=@devices.parquet" \
  "https://your-instance.cloud.netboxapp.com/api/plugins/turbobulk/load/"

Load without Changelogs (Performance Mode):

curl -X POST -H "Authorization: Bearer $TOKEN" \
  -F "model=dcim.device" \
  -F "mode=insert" \
  -F "create_changelogs=false" \
  -F "file=@devices.parquet" \
  "https://your-instance.cloud.netboxapp.com/api/plugins/turbobulk/load/"

Upsert with Expression-Based Constraint:

For models with expression-based unique constraints (e.g., case-insensitive name matching), use conflict_constraint instead of conflict_fields:

curl -X POST -H "Authorization: Bearer $TOKEN" \
  -F "model=dcim.device" \
  -F "mode=upsert" \
  -F "conflict_constraint=dcim_device_unique_name_site" \
  -F "file=@devices.parquet" \
  "https://your-instance.cloud.netboxapp.com/api/plugins/turbobulk/load/"

This enables upserts on constraints that include expressions like LOWER(name) or partial indexes. Contact NetBox Labs support if you need assistance identifying the correct constraint name for your use case.

Note: When conflict_constraint is specified, conflict_fields is ignored. The constraint must exist on the target table.

Response:

{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "pending",
  "status_url": "/api/plugins/turbobulk/jobs/550e8400-e29b-41d4-a716-446655440000/",
  "message": "Bulk insert job submitted for dcim.device",
  "dry_run": false
}

Dry-Run Response (Completed):

{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "completed",
  "data": {
    "model": "dcim.device",
    "mode": "insert",
    "dry_run": true,
    "valid": true,
    "rows": 1000,
    "errors": [],
    "warnings": []
  }
}

Bulk Delete Endpoint

Submit Bulk Delete Job

POST /api/plugins/turbobulk/delete/

Submits a bulk delete job.

Content-Type: multipart/form-data

Parameters:

ParameterTypeRequiredDescription
modelstringYesModel identifier
filefileYesData file with keys to delete (JSONL, JSONL.gz, or Parquet)
formatstringNoFile format: auto (default), jsonl, or parquet
key_fieldsarrayNoFields identifying rows (default: PK)
cascade_nullable_fksbooleanNoNullify nullable FK refs (default: true)
dry_runbooleanNoCount rows without deleting (default: false)
create_changelogsbooleanNoGenerate ObjectChange records (default: true)
branchstringNoTarget branch name (requires netbox-branching)
post_hooksobjectNoHook configuration
dispatch_eventsbooleanNoOverride global event dispatch setting. true=dispatch events, false=skip events, null=use global config (default: null)

Example Request:

curl -X POST -H "Authorization: Bearer $TOKEN" \
  -F "model=dcim.device" \
  -F "file=@delete_keys.parquet" \
  -F "cascade_nullable_fks=true" \
  "https://your-instance.cloud.netboxapp.com/api/plugins/turbobulk/delete/"

Response:

{
  "job_id": "550e8400-e29b-41d4-a716-446655440001",
  "status": "pending",
  "status_url": "/api/plugins/turbobulk/jobs/550e8400-e29b-41d4-a716-446655440001/",
  "message": "Bulk delete job submitted for dcim.device",
  "dry_run": false
}

Dry-Run Response (Completed):

{
  "job_id": "550e8400-e29b-41d4-a716-446655440001",
  "status": "completed",
  "data": {
    "model": "dcim.device",
    "dry_run": true,
    "valid": true,
    "rows": 500,
    "fks_would_nullify": 25
  }
}

Bulk Export Endpoint

Submit Bulk Export Job

POST /api/plugins/turbobulk/export/

Submits a bulk export job. Supports caching: if an identical export was recently completed and data hasn't changed, returns the cached file immediately (HTTP 200) instead of creating a new job (HTTP 202).

Content-Type: application/json

Parameters:

ParameterTypeRequiredDescription
modelstringYesModel identifier
filtersobjectNoDjango-style filter parameters
fieldsarrayNoSpecific fields to export
include_custom_fieldsbooleanNoInclude custom_field_data (default: true)
include_tagsbooleanNoInclude _tags column (default: true)
formatstringNoExport format: jsonl (default) or parquet
force_refreshbooleanNoBypass cache, generate fresh export (default: false)
check_cache_onlybooleanNoOnly check cache status, don't create job on miss (default: false)
client_cache_keystringNocache_key of the copy the client already holds. Send with client_cache_version to get a 304 while that copy is current
client_cache_versionstringNocache_version of the copy the client already holds, from a cached 200 response or a completed export job's data. Servers without cache_version support ignore it

Example Request:

curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "dcim.device",
    "filters": {"site__name": "NYC"},
    "fields": ["id", "name", "site", "status"],
    "include_custom_fields": true,
    "include_tags": true
  }' \
  "https://your-instance.cloud.netboxapp.com/api/plugins/turbobulk/export/"

Response (Cache Miss - New Job):

HTTP 202 Accepted
{
  "job_id": "550e8400-e29b-41d4-a716-446655440002",
  "status": "pending",
  "cached": false,
  "status_url": "/api/plugins/turbobulk/jobs/550e8400-e29b-41d4-a716-446655440002/",
  "message": "Bulk export job submitted for dcim.device"
}

Response (Cache Hit):

HTTP 200 OK
{
  "status": "completed",
  "cached": true,
  "cache_key": "abc123def456...",
  "cache_version": "1737712800000000",
  "cache_created_at": "2025-01-24T10:00:00Z",
  "download_url": "/api/plugins/turbobulk/cache/abc123def456.../download/",
  "file_size_bytes": 524288,
  "row_count": 1000,
  "model": "dcim.device"
}

Response (Client Cache Current - 304):

When client_cache_key and client_cache_version both match the current, still-valid cache entry:

HTTP 304 Not Modified
{
  "message": "Client cache is current"
}

Client caching and cache_version:

cache_key names the export request (a hash of model, filters, fields, options, format and the caller's authorization scope), so it stays the same every time the cache entry is rebuilt and cannot tell whether a client's copy is current. Each build of the entry also has an opaque cache_version. It is returned in the cached 200 response and, for an export that ran as a job, in the completed job's data together with cache_key and cache_stored: true (cache_stored is false, with no key or version, when the result could not be cached).

To skip a download, store both values with the file and send them back as client_cache_key and client_cache_version. The server returns 304 only when both match the current build. A request with client_cache_key but no client_cache_version gets the normal cached 200 (a re-download), never a 304, so a copy fetched before someone else rebuilt the entry can never be reported as current.

Compatibility: servers that predate cache_version do not return it and ignore client_cache_version; on those, client_cache_key alone still produces a 304 as before. The Python client sends client_cache_version only when it has one, so the same code works against both.

Response (Check Cache Only - Miss):

When check_cache_only=true and no valid cache exists:

HTTP 200 OK
{
  "cached": false,
  "data_changed": true,
  "message": "Cache invalidated or not found, call again to generate"
}

Force Refresh Example:

curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "dcim.device",
    "force_refresh": true
  }' \
  "https://your-instance.cloud.netboxapp.com/api/plugins/turbobulk/export/"

Download Cached Export

GET /api/plugins/turbobulk/cache/{cache_key}/download/

Downloads a cached export file.

Parameters:

  • {cache_key} - Cache key from export response

Response:

  • For cloud storage: Returns presigned URL
  • For local storage: Streams file content

Job Status Endpoint

Get Job Status

GET /api/plugins/turbobulk/jobs/{job_id}/

Returns the status and results of a bulk operation job.

Parameters:

  • {job_id} - UUID of the job

Response (Pending):

{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Bulk Load",
  "status": "pending",
  "created": "2024-01-15T10:00:00Z",
  "started": null,
  "completed": null,
  "user": "admin",
  "data": null,
  "error": null,
  "duration_seconds": null
}

Response (Completed - Success):

{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Bulk Load",
  "status": "completed",
  "created": "2024-01-15T10:00:00Z",
  "started": "2024-01-15T10:00:01Z",
  "completed": "2024-01-15T10:00:15Z",
  "user": "admin",
  "data": {
    "model": "dcim.device",
    "mode": "insert",
    "rows_processed": 10000,
    "rows_inserted": 10000,
    "tags_processed": 5000,
    "post_hooks": {
      "fix_denormalized": {"success": true, "rows_updated": 100},
      "fix_counters": {"success": true, "rows_updated": 50}
    }
  },
  "error": null,
  "duration_seconds": 14.5
}

Response (Failed):

{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Bulk Load",
  "status": "errored",
  "created": "2024-01-15T10:00:00Z",
  "started": "2024-01-15T10:00:01Z",
  "completed": "2024-01-15T10:00:02Z",
  "user": "admin",
  "data": {
    "model": "dcim.device",
    "mode": "insert",
    "rows_processed": 500,
    "success": false,
    "error": {
      "error_type": "foreign_key",
      "message": "Foreign key violation: site_id=999 references a dcim_site that does not exist",
      "column": "site_id",
      "value": "999",
      "referenced_table": "dcim_site",
      "suggestion": "Ensure referenced site exists before inserting"
    }
  },
  "error": "Foreign key violation",
  "duration_seconds": 1.2
}

Post Hooks Configuration

Post hooks can be configured per-request:

{
  "post_hooks": {
    "fix_denormalized": true,
    "rebuild_search_index": true,
    "fix_counters": true
  }
}
HookDefaultDescription
fix_denormalizedtrueFix denormalized site/location fields
rebuild_search_indextrueTrigger NetBox search reindex
fix_counterstrueUpdate counter cache fields

Error Responses

Validation Error (400)

{
  "model": ["Model must be in format 'app_label.model_name'"]
}

Model Not Found (400)

{
  "error": "Model not found: dcim.nonexistent",
  "error_type": "model_not_found"
}

Permission Denied (403)

{
  "detail": "User lacks permission: dcim.add_device"
}

Or, when write operations are disabled at the plugin level (load/delete only):

{
  "detail": "TurboBulk write operations are disabled. Set 'enable_writes' to True in PLUGINS_CONFIG to enable."
}

File Too Large (413)

{
  "error_type": "file_size_exceeded",
  "message": "File size (2000000000 bytes) exceeds maximum allowed size (1000000000 bytes)",
  "file_size": 2000000000,
  "max_size": 1000000000
}

Job Not Found (404)

{
  "detail": "Not found."
}

Data Format Requirements

TurboBulk supports two data formats:

JSONL Format (Default)

JSONL (JSON Lines) is the default and recommended format:

  • One JSON object per line
  • Supports gzip compression (.jsonl.gz)
  • Auto-detected from file extension or content

Example:

{"name": "device-001", "site_id": 1, "status": "active"}
{"name": "device-002", "site_id": 1, "status": "active"}

Parquet Format

Parquet is supported for high-performance operations:

  • Columnar format with built-in compression
  • Requires PyArrow or similar library to create
  • 25-50% faster throughput than JSONL, but gzipped JSONL files are 40-60% smaller

Field Type Mapping

Django FieldJSONL TypeParquet/Arrow Type
BigAutoField, BigIntegerFieldnumberint64
IntegerFieldnumberint32
CharField, TextFieldstringstring
BooleanFieldbooleanbool
DateFieldstring (ISO)date32
DateTimeFieldstring (ISO)timestamp
FloatFieldnumberfloat64
DecimalFieldstring/numberdecimal128
ForeignKeynumberint64 (PK of related)
JSONFieldobjectstring (serialized JSON)

Special Columns

ColumnTypeDescription
custom_field_datastringJSON object with custom field values
_tagslist<string>List of tag slugs for post-processing

Example Parquet Schema (PyArrow)

IMPORTANT: FK columns must use the _id suffix (database column name).

import pyarrow as pa

schema = pa.schema([
    ('name', pa.string()),
    ('site_id', pa.int64()),         # FK uses _id suffix
    ('device_type_id', pa.int64()),  # FK uses _id suffix
    ('role_id', pa.int64()),         # FK uses _id suffix
    ('status', pa.string()),
    ('serial', pa.string()),
    ('asset_tag', pa.string()),
    ('custom_field_data', pa.string()),
    ('_tags', pa.list_(pa.string())),
])

Changelog Support

TurboBulk generates ObjectChange records by default for full audit trail support in NetBox.

Behavior

OperationActionprechange_datapostchange_data
InsertcreatenullFull row data
UpsertupdateOriginal row dataUpdated row data
DeletedeleteFull row datanull

Disabling Changelogs

Set create_changelogs=false to skip changelog generation for better performance:

curl -X POST -H "Authorization: Bearer $TOKEN" \
  -F "model=dcim.device" \
  -F "mode=insert" \
  -F "create_changelogs=false" \
  -F "file=@devices.parquet" \
  "https://your-instance.cloud.netboxapp.com/api/plugins/turbobulk/load/"

Performance Impact

Changelog generation adds overhead that scales with the number of rows. At large scale (100K+ rows), enabling changelogs can significantly impact operation time.

Consider disabling changelogs for:

  • Initial data migrations where audit trail is not required
  • Large batch operations (>100K rows) where performance is critical
  • Ephemeral or test data

Job Result Data

When changelogs are enabled, job results include changelogs_created:

{
  "data": {
    "model": "dcim.device",
    "rows_inserted": 1000,
    "changelogs_created": 1000
  }
}

NetBox Branching Support

TurboBulk integrates with the netbox-branching plugin for branch-aware bulk operations.

Prerequisites

  • netbox-branching plugin installed and configured
  • Branch must exist and be in READY state

Usage

Specify the branch parameter to route operations to a branch schema:

curl -X POST -H "Authorization: Bearer $TOKEN" \
  -F "model=dcim.device" \
  -F "mode=insert" \
  -F "branch=feature-new-datacenter" \
  -F "file=@devices.parquet" \
  "https://your-instance.cloud.netboxapp.com/api/plugins/turbobulk/load/"

How It Works

When branch is specified:

  1. API validates branch exists and is in READY state
  2. Operation runs within the branch's PostgreSQL schema
  3. ObjectChange records are created (visible in branch changelog)
  4. ChangeDiff records are generated for merge conflict detection
  5. Data is isolated from main schema until branch is merged

Response with Branch

{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "pending",
  "message": "Bulk insert job submitted for dcim.device in branch feature-new-datacenter",
  "dry_run": false
}

Error Responses

Branch Not Found (400):

{
  "branch": ["Branch 'nonexistent-branch' not found"]
}

Branch Not Ready (400):

{
  "branch": ["Branch 'my-branch' is not in READY state (status: provisioning)"]
}

Plugin Not Installed (400):

{
  "branch": ["netbox-branching plugin not installed"]
}

Workflow Example

  1. Create a branch via NetBox UI or API
  2. Wait for branch to reach READY state
  3. Bulk load data to branch using branch parameter
  4. Review changes in NetBox Branch UI
  5. Merge branch to main when satisfied
  6. TurboBulk data is now in main schema

See Branching Documentation for detailed workflow examples.

On this page