> Part of the walkerOS documentation. Project overview and full index: <https://www.walkeros.io/llms.txt>

# API

[Server](#)[ Source code](https://github.com/elbwalker/walkerOS/tree/main/packages/server/stores/api)[ Package](https://www.npmjs.com/package/@walkeros/server-store-api)

Read-only store that reads one JSON record per key from a third-party HTTP API, such as your CRM, a product catalog or a pricing service. It uses raw `fetch` with zero runtime dependencies: `get` sends one `GET` request to `settings.url` with the URL-encoded key in place of `{key}`, an `Accept: application/json` header and an optional bearer token, and hands back the parsed JSON body. Pair it with a [`state`](https://www.walkeros.io/docs/collector/state.md) get on a transformer to enrich events with what the API knows about a user or a product.

## Installation[​](#installation "Direct link to Installation")

```bash
npm install @walkeros/server-store-api
```

* Integrated
* Bundled

```typescript
import { startFlow } from '@walkeros/collector';
import { storeApiInit } from '@walkeros/server-store-api';

await startFlow({
  stores: {
    crm: {
      code: storeApiInit,
      config: {
        settings: { url: 'https://crm.example.com/customers/{key}' },
        // Omit credentials for an API without a token; an empty token
        // stops the flow at startup.
        credentials: { token: process.env.CRM_API_TOKEN ?? '' },
      },
    },
  },
});
```

Add to your `flow.json`:

```json
"stores": {
  "crm": {
    "package": "@walkeros/server-store-api",
    "config": {
      "settings": { "url": "https://crm.example.com/customers/{key}" },
      "credentials": { "token": "$secret.CRM_API_TOKEN" }
    }
  }
}
```

## Configuration[​](#configuration "Direct link to Configuration")

This store uses the standard store config wrapper (consent, data, env, id, ...). For the shared fields see [store configuration](https://www.walkeros.io/docs/stores.md#configuration). Package-specific fields live under `config.settings` and are listed below.

## Settings[​](#settings "Direct link to Settings")

| Property | Type     | Description                                                                                                                                                                                                                   | More |
| -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- |
| `url*`   | `string` | Absolute http or https URL with exactly one {key} placeholder after the host, such as https\://crm.example.com/customers/{key} or https\://crm.example.com/customers?id={key}. The URL-encoded key is the only variable part. |      |

\* Required fields

## Mapping[​](#mapping "Direct link to Mapping")

This package does not define custom rule-level settings. For the standard rule fields (consent, condition, data, batch, name, policy) see [mapping](https://www.walkeros.io/docs/mapping.md).

## Examples

### Read a record

Read the customer record of a known id from the API and receive its parsed JSON body

Event

```json
{
  "operation": "get",
  "key": "cust-42"
}
```

Out

```javascript
get("cust-42", {
  "ltv": 420,
  "segment": "loyal"
})
```

### Unknown id

The API answers 404 for an id it does not know: get answers a miss, so the mapping fallbacks apply

Event

```json
{
  "operation": "get",
  "key": "cust-404"
}
```

Out

```javascript
get("cust-404", null)
```

## When it fails

* **unauthorized**: The API refuses the request (401, 403, 429 or 5xx): get throws a StepError with the status instead of answering a miss, and the collector reports it. Reported as `failed` with code `transport`.

## Enrich events with a lookup[​](#enrich-events-with-a-lookup "Direct link to Enrich events with a lookup")

A `state` get reads the record of the event's user id and merges the fields its `mapping` picks into `event.user`. On a miss, the mapping's fallbacks apply:

```json
{
  "stores": {
    "crm": {
      "package": "@walkeros/server-store-api",
      "config": {
        "settings": { "url": "https://crm.example.com/customers/{key}" },
        "credentials": { "token": "$secret.CRM_API_TOKEN" }
      },
      "cache": { "rules": [{ "ttl": 300 }] }
    }
  },
  "transformers": {
    "loadUser": {
      "state": {
        "mode": "get",
        "store": "crm",
        "key": "event.user.id",
        "value": "event.user",
        "mapping": {
          "map": {
            "ltv": "ltv",
            "segment": { "key": "segment", "value": "unknown" }
          }
        }
      }
    }
  }
}
```

Wire `loadUser` into a chain (`collector.next`, a destination's `before`) like any transformer. An order from `cust-42`, whose record is `{ "ltv": 420, "segment": "loyal" }`, reaches the destinations with `user: { id: 'cust-42', ltv: 420, segment: 'loyal' }`; an id the API answers 404 for gets `segment: 'unknown'` and no `ltv`.

## The URL and the key[​](#the-url-and-the-key "Direct link to The URL and the key")

`settings.url` is an absolute `http:` or `https:` URL with exactly one `{key}` placeholder, after the host: in the path (`https://crm.example.com/customers/{key}`) or in the query (`https://crm.example.com/customers?id={key}`), never after a `#`, since the fragment is never sent. The key is URL-encoded as one segment, so a `/`, `?`, `#` or `%` in it never changes which resource is asked, and it is the only variable part of the request. A URL that breaks one of these rules, such as `https://{key}.crm.example.com/`, stops the flow at startup with a config error.

Use an `https:` URL whenever you send a token.

A key that is empty or has a `.` or `..` segment (split on `/` and `\`) is a miss without a request. The store never logs the key, a URL carrying it or the token, and its error messages name none of them.

## Authentication[​](#authentication "Direct link to Authentication")

Put the token in `config.credentials.token` and back it with a managed secret, `$secret.NAME` as the whole value. Never use `$env` for it, and never put it in `settings` or in the URL. The store sends it as `Authorization: Bearer <token>`; without credentials it sends no `Authorization` header.

* An unset secret stops the flow at startup with `required secret "CRM_API_TOKEN" is not set`.
* Credentials that are not `{ "token": "<a non-empty string>" }`, a bare string included, stop the flow at startup with a config error.
* Requests refuse redirects: a redirect fails the read instead of carrying the token to another origin.

## What a read answers[​](#what-a-read-answers "Direct link to What a read answers")

| The API answers                                        | `get`                                        | Reported                                   |
| ------------------------------------------------------ | -------------------------------------------- | ------------------------------------------ |
| 2xx with a JSON body                                   | the parsed body                              | no                                         |
| 404, 204, or a 200 with the JSON body `null`           | a miss                                       | no; a `state` get applies its fallbacks    |
| any other status (401, 403, 429, 5xx)                  | throws a `transport` `StepError` with status | once per cause at ERROR under `store.<id>` |
| a network error, a timeout after 5 seconds, a redirect | throws a `transport` `StepError`             | once per cause at ERROR under `store.<id>` |
| a 2xx body that is not JSON, the empty body included   | throws a `decode` `StepError`                | once per cause at ERROR under `store.<id>` |

A read that throws never fails the event: a `state` get treats it as a miss for that event, applies its fallbacks and the event goes on.

## Caching and rate limits[​](#caching-and-rate-limits "Direct link to Caching and rate limits")

The store has no cache of its own; add the store `cache` block (see [Cache](https://www.walkeros.io/docs/stores/cache.md)) to keep hits in memory for the rule's `ttl`. Misses are never cached, so when the keys come from the client, such as a user id in the request, every unknown id costs one API request. Put a gateway with a rate limit in front of the API when that matters.

## Check the URL before you go live[​](#check-the-url-before-you-go-live "Direct link to Check the URL before you go live")

A wrong path on the right host answers 404 for every key. The store reads each one as a miss, so nothing is reported at ERROR and every lookup quietly falls back. Check the URL with a known id and the token before you deploy:

```bash
curl -i -H "Authorization: Bearer $CRM_API_TOKEN" \
  https://crm.example.com/customers/cust-42
```

Expect `200` with the record. A simulation (`walkeros push --simulate`) runs the store on the package's mock API and cannot show this.

## Read only[​](#read-only "Direct link to Read only")

`set` and `delete` throw a config `StepError` (`API store is read-only`), so a `state` set against this store logs at ERROR for every event. The store holds structured JSON only: `file: true` stops the flow at startup.

## Next steps[​](#next-steps "Direct link to Next steps")

* [State](https://www.walkeros.io/docs/collector/state.md): read and write stores from a step
* [Cache](https://www.walkeros.io/docs/stores/cache.md): cache rules for a store
* [Stores](https://www.walkeros.io/docs/stores.md): every store package
