> ## Documentation Index
> Fetch the complete documentation index at: https://docs.curator.interworks.com/llms.txt
> Use this file to discover all available pages before exploring further.

# API Relays

> Forward requests to external APIs through Curator, keeping credentials secure and centralizing access control.

## Overview

An API relay is a server-side proxy that sits between your users and an external API, letting Curator call
third-party services without exposing credentials to the browser. You configure the relay once; Curator then
attaches your stored credentials, validates incoming calls, and forwards the assembled request to the external
service on the caller's behalf.

The result: sensitive credentials never leave your server, your external API surface is limited to exactly what
you expose, and you get a single, auditable integration point you can monitor, update, or disable without
touching the calling code.

## When to use an API relay

API relays bridge Curator with any third-party API. Common scenarios:

* **Custom BI embeds from non-integrated platforms** — a custom JavaScript embed that fetches live data from an
  external REST API, with the API key stored as a Curator constant instead of in the browser.
* **Submitting form data to a webhook** — Data Manager routes mark-commenting submissions to any HTTP endpoint,
  using an API relay as the connection point. See [Sending Data to Webhooks](https://docs.curator.interworks.com/embedding_using_analytics/data_manager/sending_data_to_webhooks.md).
* **AI chat integration** — let users talk to an AI assistant without exposing your paid AI account credentials.

## Prerequisites

API relays require the **Integration Automation** feature to be enabled:

1. Log in to the Curator backend.
2. Navigate to **Settings** > **Portal Settings** > **Features**.
3. Enable **Integration Automation** under the Functionality section.
4. Save and refresh the page.

After enabling, the **Integrations** > **Automation** > **API Relay** section will appear in the left-hand menu.

Your backend administrator account also needs the **Manage API relay** permission, on the **Integration** tab of
**Settings** > **Curator** > **Backend Administrators**. This permission is only listed once Integration Automation is
enabled. Superuser accounts have every permission. See
[Backend Administrator Permissions](https://docs.curator.interworks.com/site_administration/backend_administrators/permissions.md).

> [!NOTE]
>
> If you can see the **Automation** menu but not **API Relay**, Integration Automation is already enabled, and your
> account has another Integration permission but not **Manage API relay**. Ask an administrator to grant it.

## Creating an API relay

1. Navigate to **Integrations** > **Automation** > **API Relay**.
2. Click **New** at the top of the list.
3. Fill in the form (see the field reference below) and click **Save**.
4. Once saved, the **Link** shown at the top of the edit form is the URL you will call to trigger
   the relay. Copy it and use it in your integration.

> **Security note:** The Portal Link is publicly accessible by default — any caller who knows the URL can trigger
> the relay, and it is not tied to a logged-in user. If the relay exposes a sensitive upstream API, restrict
> access (see [Securing a relay](#securing-a-relay) below).

## Field reference

### Basic settings

| Field | Description |
| - | - |
| **Name** | A descriptive label for the relay. Used only in the backend list — not exposed to callers. |
| **Description** | Optional free-text notes. Useful for documenting what the relay does and who relies on it. |
| **Log Relay Usage?** | When enabled, Curator writes a log entry every time the relay is triggered. Includes the incoming input, outgoing request details, and the response. Useful for debugging but may be noisy in production. |

***

### Outgoing Request URL

This section controls where Curator sends the relayed request.

| Field | Description |
| - | - |
| **URL** | The full external API endpoint URL. Do not include a query string here, variables and constants are appended automatically. |
| **Allow Dynamic URL** | When enabled, a caller can supply a different target URL at call time via the variable name set in **Dynamic URL Variable**. The URL must use the same scheme, host, and port as the static URL above; only the path and query can change. If the caller omits the variable, the static URL above is used. |
| **Dynamic URL Variable** | The query parameter name the caller should use to pass an override URL. Only shown when **Allow Dynamic URL** is enabled. |
| **Used In Webhook?** | Changes how the outgoing request body is constructed. When enabled, Curator forwards the entire incoming request body to the upstream URL rather than only the configured variables and constants. Enable this when the relay will be selected in a Data Manager Data Group's **Send to Webhook** setting, where the full submitted payload needs to be forwarded. |

***

### Outgoing Request Content

This section defines what Curator sends to the external API: the headers, fixed values, caller-supplied values,
and body content.

#### Headers

HTTP headers to attach to every outgoing request (e.g. `Content-Type: application/json`, `Accept: application/json`).
Each header entry has a **Key**, a **Value**, and an **Allow Dynamic Values** toggle. When dynamic values are enabled,
a caller can override that header's value by sending an HTTP header of the same name in their request to the relay.

#### Constants

Fixed key-value pairs that are always sent with the outgoing request regardless of what the caller provides.
This is the right place for credentials, API keys, tokens, or any other value that must remain server-side and
must never be visible to the caller. Constants cannot be overridden by incoming requests.

Each constant also has an **Encrypt Value** toggle. Turn it on for sensitive values such as API keys or client
secrets: the value is then stored encrypted in the database and shown as `***redacted***` in relay logs instead
of in plain text. The outgoing request still receives the real value.

Encryption is one-way. Once saved, the form only ever shows the encrypted form of the value, and Curator never
decrypts it back for display. Turning **Encrypt Value** off while the encrypted value is still in the field is
rejected when the relay is saved. To store a constant unencrypted again, clear the field and enter a new value.

#### Variables

Named parameters that the caller is expected to supply at call time (e.g. `start_date`, `region`, `user_id`).
Each variable has a **Key** (the parameter name) and an **Is Required?** toggle. Required variables will cause
the relay to reject the call if they are missing. Variables are passed by the caller as query string parameters
or POST body fields using the same key name defined here.

#### Body Content

Static body content to include in the outgoing request (e.g. a fixed JSON or XML payload). Leave blank if the
request body is built entirely from variables and constants. Body content applies to POST relays only — a GET
relay sends its parameters on the URL and ignores the body.

To let an incoming request set or edit the body at call time — whole-body replacement, placeholder substitution,
or transforming the value before it is injected — enable **Allow Dynamic Body Content**. This is covered in full
in [Dynamic Body Content in the API Relay](https://docs.curator.interworks.com/embedding_using_analytics/data_manager/api_relay_dynamic_body_content.md).

***

### Response Caching

Optional. A relay makes a fresh outgoing request on every call by default. Turn on caching when the same
response is valid for more than one caller or more than one moment, so repeated calls skip the round trip to
the upstream API.

* **Cache Relay Responses?** — off by default. When on, Curator stores the upstream response and reuses it for
  any later call that would have produced the same outgoing request. Calls that differ in their HTTP method or
  in the parameters they send are cached separately; parameter order does not matter, so `?a=1&b=2` and
  `?b=2&a=1` share one cached response.
* **Cache Duration (seconds)** — how long a cached response stays valid. Defaults to 60. Keep this shorter
  than the lifetime of anything the response contains: a relay that returns a short-lived OAuth token should
  expire its cache well before the token does.
* **Cache Per User?** — off by default, which is correct when every caller legitimately receives the same
  response, such as a client credentials OAuth flow. Turn it on when the upstream response differs per user.
  Curator then keeps a separate cached response for each signed-in user, and any call it cannot attribute to a
  specific user is not cached at all — including calls to the relay's own unauthenticated URL, and every call
  on a portal using Pass-Through authentication, where all visitors share one identity.

Three things are never served from, or written to, the cache:

* **Webhook relays.** A webhook forwards the incoming payload upstream as a write, so replaying an earlier
  response would silently drop a repeated submission.
* **Requests carrying a file upload**, which cannot be matched against an earlier call.
* **Requests that fail Incoming Request Validation**, which are rejected before the cache is consulted.

Saving the relay clears its cached responses, so any configuration change takes effect on the next call rather
than after the cache duration elapses.

***

### Access

* **Require a Curator Session?** — off by default, and on for relays an AI assistant creates through MCP. When on,
  the relay only answers visitors who are signed in to Curator, on the site or in the backend. Anyone else gets an
  **HTTP 401** response and the relay does not call the external API. Turn this on for relays that Curator pages
  and embedded HTML call. Leave it off for
  relays that outside systems call, such as webhooks, because those callers have no Curator session. It also
  refuses requests that another website makes from a signed-in visitor's browser, with an **HTTP 403** response.
  On a portal that uses Pass-Through authentication, every visitor counts as signed in, so this setting only
  refuses those cross-site requests there.
* **Available to MCP Tools?** — on by default. When on, AI assistants connected with a Curator MCP token can
  see, change, and call this relay, acting as the backend user who owns the token. That user also needs the
  **Manage API relay** permission. See [Curator MCP Server](https://docs.curator.interworks.com/curator_api/api_docs/mcp.md). Turn this off to hide
  the relay from every MCP token.
* **Callable from Embedded HTML?** — off by default, and on for relays an AI assistant creates through MCP. When
  on, HTML files embedded on Curator pages can call this relay with `curatorApiRelay()` (see
  [From embedded HTML](#from-embedded-html)). Any embedded HTML file can ask for any relay, so only turn this on for
  relays meant to be used that way. Calls from embedded HTML to a relay without it get an **HTTP 403** response.

***

### Incoming Request Validation

Optional rules that Curator evaluates before forwarding any request. If a request fails validation, the relay
is not called and Curator returns an error.

#### Header-based validation

Define conditions on specific HTTP headers in the incoming request. For each rule, specify:

* **Header** — the HTTP header name to inspect.
* **Condition** — one of: Matches, Does not match, Contains, Does not contain, Starts with, Ends with.
* **Value** — the value to test against.

All defined rules must pass for the relay to proceed.

#### Variable-based validation

The same conditional structure applied to query string or POST body parameters rather than headers.

#### Validation failure response

By default, a validation failure returns an **HTTP 403** response. Disable **Should validation failures respond
with error code?** to return an **HTTP 200** with an error message in the body instead. Some webhook endpoints
or client-side integrations expect 200 for all responses; use this toggle if the caller cannot handle 4xx
status codes.

***

## Securing a relay

Unless **Require a Curator Session?** is on, the relay endpoint is public and unauthenticated — it does not
require a logged-in user, and any caller who knows the Portal Link can trigger it. Your credentials stay safe
(they live in **Constants**, server-side), but the endpoint itself is open. To lock it down:

* **Require a Curator session (for relays that Curator pages call).** Turn on **Require a Curator Session?**
  so only signed-in Curator users can call the relay. Pages and embedded HTML keep working, because they call
  the relay as the signed-in viewer.
* **Require a shared secret (for relays that outside systems call).** Add an **Incoming Request Validation** rule that checks for a header
  or variable only authorized callers know — e.g. a header `X-Relay-Token` with condition **Matches** and a
  long random value. Requests without it are rejected before the relay forwards anything.
* **Restrict by IP at the network layer.** Curator has no built-in per-relay IP allowlist. To limit the relay to
  specific source IPs, enforce it in front of Curator — in your web server (nginx/Apache), load balancer,
  firewall, or WAF — by allowing only trusted IPs to reach the `/api/v1/integration/apiRelay` path. Do not rely
  on a validation rule against a forwarded header such as `X-Forwarded-For`: that header is caller-supplied and
  can be spoofed.
* **Avoid Allow Dynamic URL on relays with credentials.** A relay with **Allow Dynamic URL** on sends its
  **Headers** and **Constants** to any path the caller supplies on the relay's host, so any caller can use your
  credentials against every endpoint of that API. That includes AI assistants with MCP access when **Available to
  MCP Tools?** is on. Leave **Allow Dynamic URL** off on relays that hold credentials, or lock the relay down with
  the options above and turn off **Available to MCP Tools?**.
* **Redirects stay on the relay's host.** A relay follows a redirect only to the same host (an `http` relay may
  move to `https` on that host). A redirect to any other host fails the call instead, so set the relay's URL to
  where the redirect leads.

## Calling the relay

Once a relay is saved, Curator generates a **Portal Link** shown at the top of the edit form. This is the
URL you call to trigger the relay. Its structure is:

```
https://your-curator-instance.com/api/v1/integration/apiRelay?api_relay_id=X&variable1=X&variable2=X
```

* `api_relay_id` is the numeric ID of the relay record (fixed).
* Each configured **Variable** appears as a query parameter.

The relay accepts both **GET** and **POST** requests. For POST, variables can be supplied in the request body
instead of the query string.

### Response format

Curator returns a JSON object in all cases. The shape depends on whether the relay succeeded, failed, or was
blocked by validation.

**On success:**

```json theme={null}
{
  "result": "Success",
  "msg": { ... }
}
```

| Field | Description |
| - | - |
| `result` | `"Success"` or `"Failure"`. |
| `msg` | The external API's response body — parsed if it is JSON, otherwise a raw string. |

**On failure** — `result` is `"Failure"` and `msg` is a human-readable error. This covers both relay-level
problems (missing or invalid `api_relay_id`, a missing required variable) **and** upstream API errors: if the
external API responds with an HTTP status of 400 or higher, the relay reports failure with a message like
`There was an error processing your request: API Relay call failed with HTTP code 500`.

```json theme={null}
{
  "result": "Failure",
  "msg": "There was an error processing your request: ..."
}
```

**When Incoming Request Validation blocks the call:**

```json theme={null}
{
  "status": "error",
  "msg": "Invalid incoming request for API relay"
}
```

Validation failures use a different envelope (`status` instead of `result`) and return HTTP 403 by default (or
HTTP 200 if you have disabled the **Should validation failures respond with error code?** toggle).

**When the relay requires a Curator session and the caller is not signed in:** HTTP 401 with

```json theme={null}
{
  "result": "Failure",
  "msg": "Sign in to Curator to use this API relay."
}
```

If such a relay is called from another website, the response is HTTP 403 with the message
`This API relay only answers requests from Curator's own pages.`

### From embedded HTML

An HTML file can call a relay when a page shows it in a sandboxed frame, which is isolated from Curator. Show the
file in a sandboxed frame in either of these ways:

* On a page, add a **File** element, set **File type** to **Embed**, select the HTML file, and turn on
  **Sandbox HTML (allow API relays)**. The switch is off by default, and an embed without it is not sandboxed,
  so `curatorApiRelay` is not defined in it.
* Create a page from the HTML file. Pages created this way are sandboxed.

A sandboxed frame cannot call the relay URL directly. Instead, Curator gives every sandboxed embed a
`curatorApiRelay` function that calls the relay through the page, as the signed-in viewer. The relay must have
**Callable from Embedded HTML?** turned on:

```html theme={null}
<div id="weather">Loading...</div>
<script>
curatorApiRelay(12, {city: 'London'})
    .then(function (data) {
        document.getElementById('weather').textContent = data.current.temperature + ' degrees';
    })
    .catch(function (error) {
        document.getElementById('weather').textContent = 'Could not load the weather: ' + error.message;
    });
</script>
```

* The first argument is the relay ID and the second holds the relay's variables. Pass `{method: 'POST'}` as a
  third argument to send a POST.
* The function returns a promise of the external API's response (the `msg` value above), already parsed when it
  is JSON. When the call fails, the error has a `message` and a `status`. The `status` is the HTTP status of the
  relay's own response, described above, not the external API's: 401 when the relay requires a session, and 200
  when the external API failed. The external API's code is in the message instead.
* Because the call runs as the viewer, it works with relays that have **Require a Curator Session?** turned on.

***

## Good to know

* **Constants are never exposed to callers.** They are stored server-side and merged into the outgoing request
  by Curator. A caller inspecting browser network traffic will only see the Curator relay URL and their own
  variables — not the upstream API credentials.

* **Variable names are pass-through.** A variable named `start_date` in the relay configuration must be sent
  as `start_date` in the incoming request. Curator does not rename or transform variable names — only dynamic
  body content advanced processing can modify values.

* **Headers sent by the caller are not forwarded by default.** Only headers you explicitly define in the
  **Headers** section are included in the outgoing request. This prevents accidental forwarding of cookies,
  session tokens, or other browser headers to the external API.

* **Relay calls are unauthenticated by default.** Curator does not require the caller to be a logged-in user to
  hit the relay endpoint — see [Securing a relay](#securing-a-relay) if you need to restrict access.

* **Enabling Log Relay Usage adds full request/response logging.** This is useful during development and
  debugging but can be verbose. Logs are written to Curator's standard application log files under
  `storage/logs/`. Constants flagged with **Encrypt Value** are redacted from these logs (shown as
  `***redacted***`) in the logged arguments and outgoing URL, and are also scrubbed the same way from the logged
  upstream response, from the curl connection's effective and redirect URLs, and from its Content-Type response
  header when those echo the value back verbatim or URL/JSON-encoded. Other connection details (status code,
  timings) are not scrubbed, since they cannot carry a constant's value. An encrypted value that cannot be
  matched safely in free-form content -- too short, or one that happens to match Curator's own `***redacted***`
  marker -- has the response and those three fields replaced by an `***omitted...***` placeholder instead; the
  arguments and outgoing URL are still logged with the value redacted either way. Scrubbing only catches an
  upstream echoing the value itself: a response that returns the value transformed (for example base64-encoded,
  hashed, or embedded in a derived token) is not detected, so still disable logging in production once the
  integration is confirmed working.

* **The relay is a thin forwarder.** It returns the upstream response body largely as-is (parsing it if it is
  JSON) rather than reshaping it. The one thing it does act on is the upstream HTTP status: a 400-or-higher
  response is turned into a `"Failure"` envelope rather than passed through as a success.

* **`[Grouped] API Relay X was used` in the Event Log is log grouping, not a cap on relay calls.** When the same
  Event Log message repeats often enough, Curator stops writing a separate line for every occurrence and logs one
  entry standing in for the duplicates instead — see
  [Log Rate Limiter](https://docs.curator.interworks.com/site_administration/logging/log_rate_limiter.md). Grouping does not throttle or block the
  relay, and there is no per-relay limit to raise. Relay calls only share the
  [API rate limits](https://docs.curator.interworks.com/curator_api/api_docs/authentication.md#api-rate-limiting). Read it as a signal that something is repeating: open the
  grouped entry for the count and the time it started, and look at the entries logged before grouping began to
  find the underlying error. Turning on **Log Relay Usage?** temporarily captures the request and response detail
  behind those entries.

***

## Related

* [Sending Data to Webhooks](https://docs.curator.interworks.com/embedding_using_analytics/data_manager/sending_data_to_webhooks.md) — how to connect
  an API relay to Data Manager for mark-commenting webhook submissions.
* [Dynamic Body Content in the API Relay](https://docs.curator.interworks.com/embedding_using_analytics/data_manager/api_relay_dynamic_body_content.md) —
  letting an incoming request set or edit the outgoing request body.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.