---
title: 'Keep API Keys Off the Device: Use Authenticated Web Calls via Proxy Routes'
description: Stop including secret keys on your devices! Notehub proxy routes let your credentials live in the cloud while the device stays anonymous.
source_url: https://dev.blues.io/blog/keep-api-keys-off-device-proxy-routes/
canonical_url: https://dev.blues.io/blog/keep-api-keys-off-device-proxy-routes/
markdown_url: https://dev.blues.io/blog/keep-api-keys-off-device-proxy-routes.md
---

# Keep API Keys Off the Device: Use Authenticated Web Calls via Proxy Routes

![Keep API Keys Off the Device: Use Authenticated Web Calls via Proxy Routes banner](https://dev.blues.io/images/blog/posts/keep-api-keys-off-device-proxy-routes/banner.png?v=dd3ef83c)

August 26, 2026

## Stop including secret keys on your devices! Notehub proxy routes let your credentials live in the cloud while the device stays anonymous.

- [Notecard](https://dev.blues.io/blog/tag/notecard)
- [Notehub](https://dev.blues.io/blog/tag/notehub)
- [Security](https://dev.blues.io/blog/tag/security)
- [JSONata](https://dev.blues.io/blog/tag/jsonata)

[![Rob Lauer](https://dev.blues.io/images/blog/authors/rob-lauer.jpg?v=57fb21a7)](https://dev.blues.io/blog/author/rob-lauer/)

[Rob LauerSenior Director of Developer Relations](https://dev.blues.io/blog/author/rob-lauer/)

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:

```c
#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](https://dev.blues.io/notecard.md) has always been able to make HTTP calls to third-party services via [Web Transactions](https://dev.blues.io/notecard/notecard-walkthrough/web-transactions.md). But you may not realize it can *also* make authenticated calls where the secure credentials live in [Notehub](https://dev.blues.io/notehub.md) and are never stored on the device.

![Two approaches side by side. On the left, the credential is compiled into the firmware image, so every one of 4,000 deployed devices carries its own copy and rotating it requires an OTA firmware campaign. On the right, the credential is a single encrypted Notehub project secret referenced by placeholder, the devices carry no credential at all, and rotating it changes nothing on the devices.](https://dev.blues.io/images/blog/posts/keep-api-keys-off-device-proxy-routes/key-in-firmware-vs-notehub.svg?v=fabc1b7a)

## 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](https://dev.blues.io/api-reference/notecard-api/web-requests.md) 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.

> **Tip:**
>
> We offer a good walkthrough of the mechanics in [How to Call Third-Party Web Services From Notecard](https://dev.blues.io/blog/notecard-web-requests.md) 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:

1. The device issues a `web.get`, `web.post`, or `web.put` request, naming a `route` alias that corresponds to a [proxy route in Notehub](https://dev.blues.io/blog/notecard-web-requests.md#creating-a-proxy-route).
2. 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.
3. The third-party API responds to Notehub.
4. Notehub applies an optional **inbound** JSONata transform and returns the result to the device, with the HTTP status code in a `result` field.

![A host MCU and Notecard issue a web.get naming only a route alias, with no URL and no credential. Notehub resolves the alias, applies an outbound JSONata transform, substitutes the encrypted project secret into the Authorization header, and calls the third-party API over HTTPS. The API returns 385 bytes of raw JSON to Notehub, where an inbound response transform reduces it to 56 bytes before it travels back down to the device.](https://dev.blues.io/images/blog/posts/keep-api-keys-off-device-proxy-routes/proxy-route-data-flow.svg?v=52e5408d)

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](https://dev.blues.io/api-reference/glossary.md#project-secret) 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.

> **Note:**
>
> Notehub also supports [environment variables](https://dev.blues.io/guides-and-tutorials/notecard-guides/understanding-environment-variables.md) 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:

```plaintext
Authorization: Bearer [$$GRID_API_KEY]
```

[Placeholder variables](https://dev.blues.io/notehub/notehub-walkthrough.md#using-placeholder-variable-substitution-interpolation) 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.

> **Note:**
>
> 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:

1. Notecard must be in `continuous` mode.
2. Notecard must actually be connected to Notehub.

You can go permanently continuous with a [hub.set request](https://dev.blues.io/api-reference/notecard-api/hub-requests/latest.md#hub-set) 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:

```json
{
  "req": "hub.set",
  "mode": "periodic",
  "on": true
}
```

Next, confirm you're online with a [hub.status request](https://dev.blues.io/api-reference/notecard-api/hub-requests/latest.md#hub-status)

```json
{"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!*

```json
{
  "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](https://dev.blues.io/tools-and-sdks/firmware-libraries/arduino-library.md) library, reading the result can look something like this:

```cpp
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:

```json
{
  "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](https://dev.blues.io/guides-and-tutorials/notecard-guides/using-jsonata-to-transform-json-in-notehub.md). It evaluates against the raw upstream response body, and whatever it returns becomes the `body` the device sees:

```jsonata
{
  "price": $round(intervals[0].lmp, 2),
  "peak": intervals[0].lmp > 100,
  "at": intervals[0].start
}
```

385 bytes becomes 56:

```json
{
  "result": 200,
  "body": {
    "price": 128.45,
    "peak": true,
    "at": "2026-08-25T14:00:00Z"
  }
}
```

> **Tip:**
>
> 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.

```jsonata
$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:

```json
{
  "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](https://dev.blues.io/notecard/notecard-walkthrough/web-transactions.md) which also covers queued transactions and large binary uploads, and the [web.\* API reference](https://dev.blues.io/api-reference/notecard-api/web-requests.md) 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.
