# Custom action

> Call any API you own from inside a conversation. Define the method, URL, headers, and body with template variables, choose auto or confirm execution, and rely on built-in SSRF protection to keep requests safe.

A **custom action** lets your agent call any HTTP API you control. You define the request — method, URL, headers, and body — using template variables that the model fills in from the conversation. Bookbag makes the call, captures the response, and hands it back to the agent to use in its reply.

This is the most powerful action type. Use it for anything not covered by a built-in or connector: your own order system, an internal lookup service, a CRM endpoint, or a webhook into another tool.

## How it works

1. **Define parameters** — Write a JSON Schema describing the arguments the model supplies (for example an order number or email).
2. **Build the request template** — Set the method, URL, headers, and body. Reference parameters with `{{param}}` placeholders — they are interpolated at call time.
3. **Choose execution mode** — Read-only lookups can run in `auto`; anything that writes should stay in `confirm` (the default).
4. **Test it** — Run the action with sample parameters to confirm the request, response shape, and confirm gate behave as expected.

## Template variables

Use `{{paramName}}` anywhere in the URL, header values, or body. Each placeholder is replaced with the matching parameter the model supplied. A missing parameter resolves to an empty string rather than erroring.

```json
{
  "type": "object",
  "properties": {
    "order_number": { "type": "string", "description": "The customer's order number" },
    "email": { "type": "string", "description": "Email on the order" }
  },
  "required": ["order_number"]
}
```

## Configuration example

A custom action's config holds the HTTP template. Here is a complete example that looks up an order from your own API:

```json
{
  "method": "GET",
  "url": "https://api.yourstore.com/orders/{{order_number}}?email={{email}}",
  "headers": {
    "Authorization": "Bearer YOUR_API_KEY",
    "Accept": "application/json"
  }
}
```

For a `POST` or `PUT`, add a `body`. A string body is interpolated for `{{param}}` placeholders; an object body is sent as JSON. When you send a body and don't set a `Content-Type`, Bookbag defaults it to `application/json`.

```json
{
  "method": "POST",
  "url": "https://api.yourstore.com/returns",
  "headers": { "Authorization": "Bearer YOUR_API_KEY" },
  "body": {
    "order": "{{order_number}}",
    "reason": "{{reason}}"
  }
}
```

The agent receives the response as `{ http_status, ok, data }`, where `data` is the parsed JSON (or truncated text if the response isn't JSON). Keep responses focused — return just the fields the agent needs to answer.

## Security: SSRF protection

> **REQUESTS TO INTERNAL ADDRESSES ARE BLOCKED:** Every custom-action URL is checked before the request is made. Bookbag resolves the hostname via DNS and verifies every resolved IP. Requests to private, loopback, link-local, or cloud-metadata addresses are rejected — this prevents server-side request forgery (SSRF) against internal infrastructure.

Specifically, the following are blocked and will fail the action with a clear error:

| Blocked | Examples |
| --- | --- |
| Non-HTTP(S) protocols | `file://`, `ftp://`, `gopher://` |
| Loopback | `localhost`, `127.0.0.0/8`, `::1` |
| Private ranges | `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16` |
| Link-local & metadata | `169.254.0.0/16` (incl. `169.254.169.254`), `metadata.google.internal` |
| IPv6 unique-local / link-local | `fc00::/7`, `fe80::/10` |

> **TIP:** Because hostnames are resolved and checked, a public domain that points at a private IP is blocked too. Your API must be reachable on a genuinely public address.

## The confirm gate

Custom actions default to `confirm` execution mode. When the model calls a confirm-mode action, Bookbag does not fire the request immediately — it returns a `confirm_required` response with a one-time token. The request runs only when that token is confirmed, and is discarded if denied.

> **KEEP WRITES GATED:** Leave anything that creates, updates, cancels, or charges in `confirm` mode. Reserve `auto` for safe, read-only lookups like fetching order status.

## FAQ

**What HTTP methods are supported?**

GET, POST, PUT, PATCH, DELETE, and HEAD. A body is only sent for methods other than GET and HEAD.

**How do I authenticate to my API?**

Put your credential in a header — for example an `Authorization: Bearer ...` header. For catalog tools (Shopify, Stripe, etc.), store credentials once as an integration instead of hardcoding them.

**My request returns text, not JSON. What does the agent get?**

Bookbag tries to parse JSON; if it can't, it passes back the response text (truncated). Returning JSON gives the agent the cleanest data to work with.

**Why is my localhost endpoint failing?**

SSRF protection blocks loopback and private addresses. Expose your endpoint on a public URL (e.g. via a tunnel or a deployed staging host) to test it.

## What's next

- [Actions overview](/docs/actions/overview) — How actions become tools and how the confirm gate works.
- [Shopify actions](/docs/actions/shopify) — A connector-backed alternative for order and product lookups.
- [Webhooks](/docs/integrations/webhooks) — Send events from Bookbag to your systems.
