Skip to main content

Device Onboarding

Automated device enrollment, bootstrap key management, and self-provisioning for IoT devices.

The Onboarding Service (beacontower-onboarding) manages the lifecycle between manufacturing a device and it connecting to your Beacon Tower platform. It handles enrollment groups, bootstrap key distribution, device registration, and provider provisioning — so devices can self-configure on first boot.

Overview

The onboarding flow works like this:

  1. You create an enrollment group that defines how a class of devices authenticates and where they connect
  2. You register devices (individually or in bulk) into the enrollment group
  3. The device calls GET /get_settings with its bootstrap key to receive connection settings
  4. You or automation calls POST /provision_device to register the device with the provider

After onboarding completes, the device connects to the MQTT broker using its provisioned credentials and begins publishing telemetry. See MQTT Device Connectivity for the full protocol details.

Certificate-based onboarding

For higher-security deployments, Beacon Tower also supports certificate-based onboarding using EST (Enrollment over Secure Transport) and the Glaze CA. Certificate-based flows replace username/password authentication with mTLS, supporting three attestation tiers from shared keys to manufacturer birth certificates. See Certificate Onboarding for the full guide.

Key Concepts

Enrollment Groups

An enrollment group defines a class of devices that share:

  • Authentication method — Currently shared_key (bootstrap keys)
  • Routing target — Where devices should connect (MQTT broker + provider API)
  • Response template — JSON template rendered when a device fetches its settings
  • Statusenabled or disabled (disabled groups reject all device requests)

Bootstrap Keys

Each enrollment group has a primary and secondary bootstrap key, encrypted at rest with AES-256-GCM. Devices authenticate using either key, enabling zero-downtime key rotation.

Routing Targets

A routing target defines the infrastructure a device connects to:

  • Brokers — MQTT endpoint(s) the device will use (host:port)
  • Provider base URL — The provider API that manages device credentials
  • Provider API key — Encrypted credential for provider communication
  • Webhook secret — For receiving provider callback events

Response Templates

Enrollment groups define a JSON template with {{variable}} placeholders that gets rendered when a device calls GET /get_settings:

VariableSourceType
{{device.id}}Device recordstring
{{device.primary_key}}Provider credentialsstring
{{device.secondary_key}}Provider credentialsstring
{{target.host}}Routing target brokerstring
{{target.port}}Routing target brokerinteger

{{target.port}} renders as a JSON integer, not a quoted string.

Authentication

The onboarding service uses three authentication methods:

MethodEndpointsUsed By
JWT BearerAll /api/* endpointsAdministrators and automation
Basic AuthGET /get_settingsDevices (deviceId:bootstrapKey)
HMAC-SHA256POST /api/webhooks/provider-eventsProvider callbacks

Setup Walkthrough

Prerequisites

  • A running Beacon Tower instance with a configured provider (e.g., MQTT Provider)
  • The onboarding service deployed and accessible
  • A JWT access token with appropriate privileges
export ONBOARDING_URL="https://onboarding.beacontower.ai"
export TOKEN="your-jwt-token"

Step 1: Create a Routing Target

A routing target tells devices where to connect and tells the onboarding service how to reach the provider API.

curl -X POST $ONBOARDING_URL/api/routing-targets \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Production IoT Hub",
"brokers": [
{ "host": "your-iot-hub.azure-devices.net", "port": 8883 }
],
"provider_base_url": "https://api.beacontower.ai/providers/iothub-primary",
"provider_api_key": "your-provider-api-key",
"webhook_secret": "your-webhook-secret"
}'

Response:

{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"name": "Production IoT Hub",
"brokers": [{ "host": "your-iot-hub.azure-devices.net", "port": 8883 }],
"created_at": "2025-01-15T10:00:00Z"
}

Save the routing target id for the next step.

Step 2: Create an Enrollment Group

An enrollment group ties together authentication, routing, and the response template that devices receive.

export ROUTING_TARGET_ID="f47ac10b-58cc-4372-a567-0e02b2c3d479"

curl -X POST $ONBOARDING_URL/api/enrollment-groups \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Temperature Sensors",
"auth_method": "shared_key",
"routing_target_id": "'$ROUTING_TARGET_ID'",
"status": "enabled",
"response_template": {
"mqtt_host": "{{target.host}}",
"mqtt_port": {{target.port}},
"device_id": "{{device.id}}",
"primary_key": "{{device.primary_key}}",
"secondary_key": "{{device.secondary_key}}"
}
}'

Response:

{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Temperature Sensors",
"auth_method": "shared_key",
"status": "enabled",
"routing_target_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"created_at": "2025-01-15T10:05:00Z"
}

Step 3: Generate Bootstrap Keys

Generate the primary bootstrap key for the enrollment group:

export GROUP_ID="a1b2c3d4-e5f6-7890-abcd-ef1234567890"

curl -X POST $ONBOARDING_URL/api/enrollment-groups/$GROUP_ID/keys/generate \
-H "Authorization: Bearer $TOKEN"

Response:

{
"primary_key": "bk_a3f8e2d1c4b5...",
"secondary_key": null
}

The first call fills primary_key. Subsequent calls fill secondary_key, enabling zero-downtime rotation. Distribute the key to devices during manufacturing or initial configuration. You can retrieve current keys at any time via GET .../keys.

Step 4: Register Devices

Single Device

curl -X POST $ONBOARDING_URL/api/enrollment-groups/$GROUP_ID/devices \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"device_id": "temp-sensor-001"
}'

Bulk Import (NDJSON)

For large batches (up to 10,000 devices per request), use the bulk endpoint with NDJSON format:

curl -X POST $ONBOARDING_URL/api/enrollment-groups/$GROUP_ID/devices/bulk \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/x-ndjson" \
-d '{"device_id": "temp-sensor-001"}
{"device_id": "temp-sensor-002"}
{"device_id": "temp-sensor-003"}'

Response:

{
"added": 3,
"errors": [],
"skipped": 0
}

Each device is validated against the provider in parallel (max 20 concurrent). Devices that already exist are skipped, and validation failures are reported in the errors array.

Step 5: Device Bootstrap

On first boot, the device calls GET /get_settings with Basic Auth (deviceId:bootstrapKey):

# This is what the device firmware does:
curl $ONBOARDING_URL/get_settings \
-u "temp-sensor-001:bk_a3f8e2d1c4b5..."

Response (rendered from the enrollment group's template):

{
"mqtt_host": "your-iot-hub.azure-devices.net",
"mqtt_port": 8883,
"device_id": "temp-sensor-001",
"primary_key": "device-specific-key-from-provider",
"secondary_key": "device-specific-secondary-key"
}

The device now has everything it needs to connect to the MQTT broker. The onboarding service fetches the device's credentials from the provider behind the scenes.

Step 6: Provision the Device

Provisioning registers the device with the provider infrastructure (e.g., creates it on IoT Hub):

curl -X POST $ONBOARDING_URL/provision_device \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"device_id": "temp-sensor-001",
"enrollment_group_id": "'$GROUP_ID'"
}'

Response (201 Created for new enrollments, 200 OK if already provisioned):

{
"device_id": "temp-sensor-001",
"enrollment_group_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"provider_client_created": true,
"enrollment_created": true,
"provisioned_at": "2025-01-15T10:30:00Z"
}
Provisioning vs. Bootstrap

Provisioning (POST /provision_device) registers the device on the provider — this is typically done by your automation or management system. Bootstrap (GET /get_settings) is called by the device itself to fetch its connection settings. A device must be provisioned before it can successfully bootstrap.

Bootstrap Key Rotation

The two-key model enables zero-downtime key rotation:

┌─────────────────────────────────────────────────────┐
│ 1. Initial state: primary = key_A, secondary = — │
│ 2. Generate: primary = key_A, secondary = key_B │
│ (both keys valid, start distributing key_B) │
│ 3. Flip: primary = key_B, secondary = — │
│ (key_A invalidated, all devices use key_B) │
└─────────────────────────────────────────────────────┘

Rotation Workflow

1. Generate a new key into the secondary slot:

curl -X POST $ONBOARDING_URL/api/enrollment-groups/$GROUP_ID/keys/generate \
-H "Authorization: Bearer $TOKEN"

At this point both keys are valid. Devices can authenticate with either one.

2. Distribute the new key to devices (firmware update, configuration push, etc.).

3. Flip — promote secondary to primary:

curl -X POST $ONBOARDING_URL/api/enrollment-groups/$GROUP_ID/keys/flip \
-H "Authorization: Bearer $TOKEN"

The old primary key is now invalidated. All devices must use the new key.

4. Verify current key status:

curl $ONBOARDING_URL/api/enrollment-groups/$GROUP_ID/keys \
-H "Authorization: Bearer $TOKEN"
warning

Flip is irreversible — the old primary key is permanently invalidated. Ensure all devices have received the new key before flipping.

Webhook Integration

The onboarding service can receive events from providers via webhooks. Currently supported:

EventBehavior
device.deletedSoft-deletes the device record in the onboarding database

Webhook Setup

When creating a routing target, provide a webhook_secret. The provider sends events to:

POST /api/webhooks/provider-events

Each request includes an X-Webhook-Signature header containing the hex-encoded HMAC-SHA256 of the request body, signed with the webhook secret. The onboarding service validates this signature using constant-time comparison before processing any event.

API Reference

Enrollment Groups

MethodRouteDescription
POST/api/enrollment-groupsCreate enrollment group
GET/api/enrollment-groups/{id}Get enrollment group
GET/api/enrollment-groupsList enrollment groups (paginated)
PUT/api/enrollment-groups/{id}Update enrollment group
DELETE/api/enrollment-groups/{id}Delete enrollment group

Bootstrap Keys

MethodRouteDescription
POST/api/enrollment-groups/{id}/keys/generateGenerate bootstrap key
POST/api/enrollment-groups/{id}/keys/flipRotate keys (secondary to primary)
GET/api/enrollment-groups/{id}/keysGet current keys

Devices

MethodRouteDescription
POST/api/enrollment-groups/{id}/devicesAdd single device
POST/api/enrollment-groups/{id}/devices/bulkBulk add devices (NDJSON, max 10k)
DELETE/api/enrollment-groups/{id}/devices/{deviceId}Remove device
GET/api/enrollment-groups/{id}/devicesList devices (paginated)

Routing Targets

MethodRouteDescription
POST/api/routing-targetsCreate routing target
GET/api/routing-targets/{id}Get routing target
GET/api/routing-targetsList routing targets (paginated)
PUT/api/routing-targets/{id}Update routing target
DELETE/api/routing-targets/{id}Delete routing target

Provisioning & Bootstrap

MethodRouteAuthDescription
POST/provision_deviceJWTProvision a device with the provider
GET/get_settingsBasicDevice fetches connection settings

Webhooks

MethodRouteAuthDescription
POST/api/webhooks/provider-eventsHMAC-SHA256Receive provider events

Pagination

List endpoints use cursor-based pagination:

GET /api/enrollment-groups?limit=50&cursor=<continuation_token>

The response includes a continuation_token field. Pass it as cursor in the next request. When null, there are no more results.

Error Responses

All errors return a consistent format:

{
"error": "error_code",
"message": "Human-readable description"
}

Error messages are sanitized — internal details are never exposed.

Next Steps