API Reference / Ingest / POST /v1/telemetry:batch
Copy page as MarkdownJoin the sandbox waitlist
POST

/v1/telemetry:batch

copyEARLY ACCESS

Submit signed meter readings for a registered device.

Bearer + Device signatureYields up to L21,000 req/minIdempotent

Request body

FieldTypeReq.Description
device_idstringRegistered device
readings[]arrayMax 1,000 per batch, ascending by device_ts
readings[].device_tsRFC 3339Device clock. Drift beyond ±300 s is flagged, not rejected
readings[].energy_wh_cumulativeintegerLifetime counter. Monotonically non-decreasing within a meter_epoch
readings[].meter_epochintegerIncrements on meter replacement or counter reset. The counter is monotonic inside one epoch and says nothing across two
readings[].sequence_numberintegerMonotonic within the device. Does not reset across meter_epoch — it is what orders readings when the energy counter cannot
readings[].reading_idstringDevice-assigned identifier. What a correction points at
readings[].reset_reasonenummeter_replaced · counter_rollover · device_reset · manual_correction. Set on the first reading of a new meter_epoch, null otherwise
readings[].measurement_qualityenumgood · estimated · substituted · suspect. Defaults to good
readings[].supersedes_reading_idstringPresent only on corrections. See POST /v1/telemetry:correct
readings[].active_power_wintegerInstantaneous active power
readings[].irradiance_w_m2integerPlane-of-array irradiance. Improves anomaly detection
readings[].module_temp_cnumberModule temperature
readings[].status_flagsstring[]Device fault codes, passed through unmodified
ℹ Monotonicity rule

The monotonic constraint holds within a meter_epoch. It does not hold across epochs, and requiring it to would make every legitimate meter change look like an attack. Meter replacement, counter rollover, numeric overflow, device reset, and reading correction all push a cumulative counter down legitimately. On a swap, submit one reading carrying reset_reason: meter_replaced with the outgoing meter's final value and the incoming meter's initial value — register it first through POST /v1/devices/{id}:meter-swap — and cumulative energy stays continuous across the boundary.

Response · 200 OK

FieldTypeDescription
accepted / rejectedintegerCounts. Check rejected on every response
batch_idstringtbx_ reference
errors[]arrayPer-reading failures with index and ark_code
verification.signatureenumvalid · invalid · absent. Synchronous — decidable on receipt
verification.continuityenumok · gap · regression. A comparison against the previous reading in the same meter_epoch
verification.anomaly_scorenumber0–1. Above 0.8 opens review
verification.anomaly_statusenumprovisional · confirmed. The baseline model scores immediately; that score is provisional until review or the epoch closes
attestation.statusenumpending · anchored · failed. Anchoring is batched per epoch and is never synchronous
attestation.expected_atRFC 3339When anchoring is expected
⚠ Cumulative, not interval

energy_wh_cumulative is a lifetime counter. Sending an interval value produces a continuity regression and a provisional downgrade. If your source emits intervals, accumulate on your side before submitting.

⚠ 200 does not mean all accepted

Batches accept partially. Read rejected and errors[] every time. A silently dropped reading becomes a continuity gap thirty days later, when it is much harder to diagnose.

⚠ anomaly_status: provisional is not a verdict

The score you get back is the baseline model's immediate opinion. Anomalies can be resolved, and resolved anomalies can still be excluded from verified generation. Do not wire a provisional score straight into an alerting rule that pages someone.

⚠ attestation is asynchronous, and this response never carries the anchor

Anchoring is batched per epoch. Wait for the attestation.anchored webhook rather than polling this endpoint or, worse, treating expected_at as a guarantee.

✓ Batch on a natural cadence

One request per five-minute interval is the sweet spot. Sub-minute batching burns rate limit without improving assurance; hourly delays attestation.

Errors

CodeHTTPMeaning
device_not_found404Unknown or deregistered device_id
signature_invalid401Signature failed verification against the registered public key
counter_regression422Cumulative value below the last accepted reading in the same meter_epoch
meter_epoch_invalid422Epoch not registered, or lower than the device's current epoch
sequence_number_regression422Sequence number not monotonic within the device
batch_too_large413More than 1,000 readings
baseline_incomplete409Device still in its baseline window; readings stored but not attestable

Related

GuidesShip a device that reports at L2ConceptsAssurance, readiness, and eligibility · The fact chainEndpointsPOST /v1/telemetry:correct · POST /v1/devices/{id}:meter-swapWebhookstelemetry.anomaly_detected · assurance.provisionally_downgraded · attestation.anchored
curl -X POST https://sandbox.api.arkreen.com/v1/telemetry:batch \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "device_id": "dev_sandbox_m3",
    "readings": [{
      "device_ts": "2026-08-22T09:00:00Z",
      "energy_wh_cumulative": 4182339000,
      "meter_epoch": 2,
      "sequence_number": 88214,
      "reading_id": "rdg_7wK2",
      "reset_reason": null,
      "measurement_quality": "good",
      "active_power_w": 214300
    }]
  }'
200 OK
{
  "accepted": 1,
  "rejected": 0,
  "batch_id": "tbx_01J9Z…",
  "errors": [],
  "verification": {
    "signature": "valid",
    "continuity": "ok",
    "anomaly_score": 0.02,
    "anomaly_status": "provisional"
  },
  "attestation": {
    "status": "pending",
    "expected_at": "2026-08-22T10:00:00Z"
  }
}
422 counter_regression
{
  "error": "counter_regression",
  "message": "value below last accepted in meter_epoch 2",
  "last_accepted_wh": 4182339000,
  "hint": "register a meter swap first if the meter changed"
}
Try it — request sandbox accessSend request