How to Correctly Enqueue a Downlink via REST API (Documentation Issue)

Viewed 9

Hey ChirpStack community,

After many hours of debugging the REST API downlink enqueue (v4.13.0, chirpstack-rest-api on port 8090), I want to share the correct way to do it with cURL — and point out two misleading parts in the docs/Swagger that cost me a lot of time.

The Swagger UI shows:

"data": "string"

and for f_port:

"fPort": 1

Both are wrong or misleading!


1. data must be Base64 of raw bytes — not text or JSON

If you send:

"data": "{\"hex\":\"03de011401000500000000\"}"

→ Device gets the JSON string, not your binary payload.

Correct: Convert hex → raw bytes → Base64

Example:

hex: 03de011401000500000000
→ Base64: A94BFAEABQAAAAAAA==

2. f_port must be snake_case, not fPort

Swagger shows fPort, but the Protobuf schema uses:

uint32 f_port = 2;

So use f_port, not fPort!


Correct cURL Command

curl -X POST "http://YOUR_HOST:8090/api/devices/YOUR_DEVEUI/queue" \
  -H "accept: application/json" \
  -H "Grpc-Metadata-Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "queueItem": {
      "confirmed": false,
      "f_port": 7,
      "data": "A94BFAEABQAAAAAAA=="
    }
  }'

Result:

  • UI shows: 03DE011401000500000000 (all zeros preserved!)
  • Device receives: raw bytes 03 de 01 14 01 00 05 00 00 00 00

JS Snippet: Hex → Base64 (Browser – no Buffer)

function hexToBase64Browser(hex) {
  hex = hex.replace(/\s/g, '').toUpperCase();
  if (!/^[0-9A-F]+$/i.test(hex) || hex.length % 2 !== 0) throw new Error("Invalid hex");
  const bytes = new Uint8Array(hex.length / 2);
  for (let i = 0; i < hex.length; i += 2) {
    bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
  }
  let binary = '';
  bytes.forEach(b => binary += String.fromCharCode(b));
  return btoa(binary);
}
console.log(hexToBase64Browser("03de011401000500000000")); // A94BFAEABQAAAAAAA==

JS Snippet: Hex → Base64 (Node.js Server-Side)

function hexToBase64Node(hex) {
  return Buffer.from(hex.replace(/\s/g, ''), 'hex').toString('base64');
}
console.log(hexToBase64Node("03de011401000500000000")); // A94BFAEABQAAAAAAA==

Docs Suggestion

Update Swagger example:

"data": "A94BFAEABQAAAAAAA=="  // Base64 of hex 03de011401000500000000

and use "f_port": 7 — not fPort.


Question to the team:
Why was the decision made to use Protobuf field names (f_port, bytes data) directly in the REST API JSON, instead of a more REST-friendly format (like fPort, plain hex, or JSON)? It breaks Swagger usability and confuses users.

Thanks for any insight!
Jan from ShopOfThings.ch

1 Answers

Please note that the chirpstack-rest-api and its documentation is automatically generated and is based on the https://github.com/grpc-ecosystem/grpc-gateway.

In any case, I would recommend using gRPC as you don't need to use the chirpstack-rest-api as proxy (which proxies the REST requests as gRPC to ChirpStack, and the gRPC response back to REST responses).

Working directly with gRPC has the benefit that all fields are immediately typed correctly (e.g. the data field would be a byte-array instead of a base64 encoded string) and you can use the structs generated by gRPC (so the correct field-names are known).

Any contributions to improve the chirpstack-rest-api tool are welcome. You can find all code here: https://github.com/chirpstack/chirpstack-rest-api.