Say you're building out a fleet of devices that shift some functionality based on real-time data derived from an external REST API. The problem is that API requires a bearer token, and you have 4,000 devices in the field. Somewhere in your firmware, a line like this starts to look inevitable:
#define GRID_API_KEY "sk_live_8f2c…"It works, but it also means that credential now exists in 4,000 places you don't control, in flash memory a bad actor can read with a $20 programmer, and rotating it requires an OTA firmware update.
There's a better solution! Blues Notecard has always been able to make HTTP calls to third-party services via Web Transactions. But you may not realize it can also make authenticated calls where the secure credentials live in Notehub and are never stored on the device.
Why Notecard Can't Just Call the API Directly
To clarify a common misconception, Notecard is deliberately restricted in who it can talk to. It has no publicly-accessible IP address, it doesn't listen for inbound connections, and its outbound TLS connection goes to exactly one place: Notehub. This is a real security advantage rather than a limitation!
The tradeoff is that Notecard can't open a socket to api.gridoperator.com on
its own. Long ago we added a path that enables Notecard to make those calls: the
web.* APIs combined with a Notehub
proxy route.
The device asks Notehub to make the request on its behalf. Notehub holds the URL, the TLS trust, and the credentials, then performs the HTTP call, and hands the response back down the existing device connection. From the third-party API's perspective it's being called by Notehub. From the device's perspective it made a web request. Neither one needs to know about the other's authentication.
We offer a good walkthrough of the mechanics in How to Call Third-Party Web Services From Notecard if you want an end-to-end explanation. What I want to focus on in this article is the part that matters once you're past a prototype phase. Specifically, where the secrets live and how to keep the response small enough to be useful on a constrained host.
A View of the Round Trip
Here's a look at what the full workflow of using a proxy route looks like:
- The device issues a
web.get,web.post, orweb.putrequest, naming aroutealias that corresponds to a proxy route in Notehub. - Notehub resolves the alias, applies an optional outbound JSONata transform to build the request body, substitutes placeholder variables into the URL and headers, and performs the HTTPS call.
- The third-party API responds to Notehub.
- Notehub applies an optional inbound JSONata transform and returns the
result to the device, with the HTTP status code in a
resultfield.
Let's look at this entire process in more detail:
Step 1: Put the Credential in Notehub, Not the Route
Notehub offers an encrypted Project Secrets store and it's the right home for any mission-critical secrets.
Under Settings → Secrets in your Notehub project, create a key/value pair.
Name it something you'll recognize in a route definition, like GRID_API_KEY.
Secret values are encrypted at rest, decrypted in-memory only at the instant a
route fires, and are never returned by the Notehub API or shown in the UI after
you save them. They also stay out of event history and route logs.
Notehub also supports
environment variables
with a similar [$name] placeholder syntax, and they work fine for
non-sensitive configuration like a region code or a customer ID. But environment
variables are stored and displayed as plaintext. Secure credentials on the other
hand belong in Secrets.
Step 2: Create the Proxy Route
In your Notehub project, go to Routes, click Create Route, and select Proxy for Notecard Web Requests. You'll fill in:
- Route name: whatever you want to see in the routes list in your Notehub project.
- URL: the base host, e.g.
https://api.gridoperator.com. - Alias: the short string your firmware will reference, e.g.
grid-prices.
Next, open the HTTP Headers section, choose Additional Headers, and add the credential as a header whose value is a secret placeholder:
Authorization: Bearer [$$GRID_API_KEY]Placeholder variables
use square brackets, and the $$ prefix specifically means "look this up in my
Notehub project secrets". Notehub substitutes the real value as the request
leaves its infrastructure. Secrets work in the route's URL and header fields,
which covers the two places REST APIs actually look for credentials.
Secrets cannot be used inside the request body or inside a JSONata
transform expression. If you're integrating an API that demands its key in a
JSON body field, this constraint can be an issue and you'll want to put a small
function of your own in front of it rather than trying to work around it.
Step 3: Make the Request from Firmware
Two conditions must be met before a web.* request that hands a response back
to your host can execute:
- Notecard must be in
continuousmode. - Notecard must actually be connected to Notehub.
You can go permanently continuous with a
hub.set request that
includes the "mode":"continuous" argument, but on a battery-powered device
you'll likely want to open a temporary window with the "on":true argument:
{
"req": "hub.set",
"mode": "periodic",
"on": true
}Next, confirm you're online with a hub.status request
{"req": "hub.status"}When the response returns {"connected":true}, you can make your web.*
request.
Note what isn't in this request: no hostname, no key, and no certificate!
{
"req": "web.get",
"route": "grid-prices",
"name": "/v1/prices/current?zone=NYIS"
}The name argument is the path relative to the route's configured URL, query
parameters included.
Using the note-arduino library, reading the result can look something like this:
J *req = notecard.newRequest("web.get");
if (req != NULL) {
JAddStringToObject(req, "route", "grid-prices");
JAddStringToObject(req, "name", "/v1/prices/current?zone=NYIS");
J *rsp = notecard.requestAndResponse(req);
if (rsp != NULL) {
if (!notecard.responseError(rsp) && JGetInt(rsp, "result") == 200) {
J *body = JGetObject(rsp, "body");
if (body != NULL) {
double price = JGetNumber(body, "price");
bool peak = JGetBool(body, "peak");
setDutyCycle(price, peak);
}
}
notecard.deleteResponse(rsp);
}
}The result field carries the HTTP status code, body carries a JSON
response, and non-JSON responses arrive base64-encoded in payload instead.
Step 4 (Optional): Shrink the Response
This is a completely optional step that can be incredibly helpful on constrained devices. Let's say the above mocked up API returns something like this:
{
"request_id": "0f8c2a91-4b6e-4f2a-9a11-77c3e5d1b204",
"status": "ok",
"meta": {
"zone": "NYIS",
"currency": "USD",
"units": "$/MWh",
"generated_at": "2026-08-25T14:05:03Z",
"api_version": "2.4.1",
"rate_limit": { "limit": 1000, "remaining": 987, "reset": 1756137600 }
},
"intervals": [
{
"start": "2026-08-25T14:00:00Z",
"end": "2026-08-25T14:05:00Z",
"lmp": 128.4471,
"congestion": 4.11,
"losses": 1.92,
"energy": 122.42
}
]
}That's 385 bytes to deliver one number your device cares about. Paying cellular
data to move a rate_limit object to a microcontroller, and then spending
that microcontroller's RAM parsing it, is a bad trade at any fleet size.
Using Inbound JSONata Transforms
Thankfully, the Notehub proxy route has an Inbound Response Transform field
that takes a
JSONata expression.
It evaluates against the raw upstream response body, and whatever it returns
becomes the body the device sees:
{
"price": $round(intervals[0].lmp, 2),
"peak": intervals[0].lmp > 100,
"at": intervals[0].start
}385 bytes becomes 56:
{
"result": 200,
"body": {
"price": 128.45,
"peak": true,
"at": "2026-08-25T14:00:00Z"
}
}Notice that the transform sees only the upstream body. The result envelope
is applied around whatever your expression produced, so don't write your JSONata
expecting a result field to be present.
Using Outbound JSONata Transforms
Note that there's a matching outbound transform for web.post and
web.put, and it works a little differently. It evaluates against the whole
Notehub event, not just the body your Notecard sent. So a device can send a
minimal body and let Notehub enrich it with what it already knows about the
device, like its DeviceUID, serial number, and the best_* location fields
Notehub resolved.
$merge([body, {"device_id": device}])$merge combines objects, so this one reads as "everything the device sent,
plus one more field". A web.post body of
{"level_cm":184.62,"pump_running":true} arrives upstream as:
{
"level_cm": 184.62,
"pump_running": true,
"device_id": "dev:864475040261234"
}Summary
Reach for a proxy route in Notehub when your device needs a response from a third-party API to act on. This could be any real-time data, a configuration lookup, a work-order status check, a "should I run right now?" decision, or anything else that provides important context for your product. And if you find yourself needing a secure credential in the request, just follow our advice in this article and you'll be well on your way.
To go deeper, consult the Web Transactions walkthrough which also covers queued transactions and large binary uploads, and the web.* API reference has the full list of APIs and arguments.
Happy hacking! 💙
Frequently Asked Questions
How do I call a third-party API from a Notecard without storing the API key on the device?
Create a "Proxy for Notecard Web Requests" route in Notehub, store the
credential as a Notehub Project Secret, and reference it from the route's HTTP
headers using the [$$SECRET_NAME] placeholder syntax. The device then issues a
web.get, web.post, or web.put request naming only the route's alias.
Notehub attaches the credential as the request leaves its infrastructure, so the
key never exists in firmware, in flash, or on the wire between the device and
Notehub.
Where can Notehub project secrets be used in a route?
Project secret placeholders work in the open text fields of a route definition,
including the URL and HTTP headers. They cannot be used inside the request body
or inside JSONata transform expressions. Since virtually every REST API expects
its credential in an Authorization header or a query string parameter, the
header and URL fields cover the common cases.
What is an Inbound Response Transform on a Notehub proxy route?
It is a JSONata expression that rewrites the third-party API's response before Notehub returns it to Notecard. The expression evaluates against the raw upstream response body, and whatever it returns becomes the body the device receives. It is the cleanest way to reduce a verbose REST response down to the two or three fields a memory-constrained host MCU actually needs, which also cuts the cellular data spent receiving it.
Do web transactions work on every Notecard?
The web.* APIs are available on Notecard Cellular, Notecard for Skylo, Notecard WiFi, and Notecard Cell+WiFi devices. They are not supported on Notecard for LoRa, nor while a device is operating in NTN mode. Web transactions also require an active connection to Notehub, which means continuous mode or a temporary window opened with hub.set.

