---
title: ChatGPT custom GPTs
description: Run the Veefore relay, import its OpenAPI document into a custom GPT, and have every action the GPT takes go through Veefore.
---
ChatGPT custom actions can send only a fixed API key, and the Veefore gateway
needs a signature from the agent's private key. The **Veefore relay** bridges
the two. You run it; it holds the agent key, signs, and forwards each call. The
private key never goes to ChatGPT.

The relay decides nothing. Policy, risk, approvals and the credential broker
run at the gateway, and if the relay is down nothing goes through.

Do the [shared setup](/build/connect-agents) first: an agent for the GPT, a
tool, a policy, enforcement on.

## Step by step

:::steps
:::step Make a relay key
This is what ChatGPT sends to the relay. It is not a Veefore key.
```bash
openssl rand -base64 32
```
:::
:::step Deploy the relay
The relay is `integrations/veefore-relay` in the Veefore repository: one file,
no dependencies, and a Dockerfile. Deploy it anywhere that serves HTTPS
(Railway, Fly, Render, Cloud Run) with these variables:

| Variable | Value |
| --- | --- |
| `VEEFORE_GATEWAY_URL` | `https://gateway.veefore.com` |
| `VEEFORE_AGENT_ID` | the agent's id, `agt_…` |
| `VEEFORE_AGENT_KEY_ID` | the agent's key id, `key_…` |
| `VEEFORE_AGENT_PRIVATE_KEY` | the agent's private key, as shown in the console |
| `RELAY_API_KEYS` | the relay key from step 1 (comma-separate several to rotate) |
| `RELAY_PUBLIC_URL` | the relay's own https address |
| `VEEFORE_RUNTIME_ID` | optional, the runtime id from step 3 |

On Railway: **New → GitHub repo**, root directory `integrations/veefore-relay`,
set the variables, and **Generate domain**. With Docker:
```bash
docker build -t veefore-relay integrations/veefore-relay
docker run --env-file relay.env -p 8787:8787 veefore-relay
```
Check it: `https://<relay>/health` answers `{"status":"ok"}`.
:::
:::step Register the runtime
**Integrations → ChatGPT and API platforms → Add**, same agent. Optionally put
its id in `VEEFORE_RUNTIME_ID` and redeploy.
:::
:::step Add the action to your GPT
In ChatGPT: **Explore GPTs → Create → Configure → Actions → Create new
action**.

1. **Import from URL**: `https://<relay>/openapi.json`.
2. **Authentication → API key → Auth type: Bearer**, paste the relay key.
3. Set the **Privacy policy** URL your organization uses for GPTs.
:::
:::step Tell the GPT how to use it
Add to the GPT's **Instructions**:
```text
Before doing anything for the user that changes something (sending, creating,
updating, deleting, paying), call authorizeAction with the tool and parameters.
If the result is ALLOW, call executeAction with the same tool, the same
parameters and the authorization_id. If executeAction returns done: false,
nothing happened: tell the user why, using its message. If it says a person
must approve, tell the user it is waiting for approval and do not retry until
they say it was approved, then call executeAction again with the approval_id.
Never claim an action happened unless executeAction returned done: true.
```
:::
:::step Try it and check it
In the GPT's preview, ask it to send a test email. Then check **Actions** in
the console: the call is there with `via: chatgpt_relay`. Ask for something
your policy blocks: the GPT says it was refused, and nothing reached the
provider.
:::
:::

## What the relay exposes

| Operation | Endpoint | Notes |
| --- | --- | --- |
| `listTools` | `GET /tools` | The agent's authorized tools. |
| `authorizeAction` | `POST /authorize` | Dry run. ALLOW returns `authorization_id`, 120 s, single use. |
| `executeAction` | `POST /execute` | `done: true` only when it happened. |
| `getAction` | `GET /actions/{id}` | Outcome of one action. |
| `getApproval` | `GET /approvals/{id}` | Approval status. |

`/health` and `/openapi.json` need no key; everything else does. Each relay key
is limited to 60 requests a minute by default (`RELAY_RATE_LIMIT_PER_MINUTE`).
Request and response bodies are never logged.

A retried call that presents the same `authorization_id` or `approval_id` is
sent with the same idempotency key, so ChatGPT retrying a request does not
send the email twice.

## Security notes

- Treat the relay host like a credential store: it holds the agent's private
  key. Use the platform's secret variables, not a file in the image.
- Rotate the relay key by adding the new one to `RELAY_API_KEYS`, updating the
  GPT, then removing the old one.
- If the agent key leaks, revoke or rotate it under **Agents**; the relay stops
  working until it gets the new key.
- ChatGPT's own browsing and connectors act outside Veefore. The runtime is
  **Partial** at best for that reason.

## Troubleshooting

| Symptom | Fix |
| --- | --- |
| `A valid relay API key is required` | The GPT's action authentication is missing or the key differs from `RELAY_API_KEYS`. |
| `Agent authentication failed` | Wrong agent id, key id or private key, or `VEEFORE_GATEWAY_URL` differs from the gateway's address. |
| The relay will not start | It prints the missing variable. Relay keys must be at least 24 characters. |
| `listTools` is empty | Authorize the agent on a tool. |
| ChatGPT says "error talking to connector" | The relay is not reachable over HTTPS, or `RELAY_PUBLIC_URL` in the OpenAPI document is wrong. |
