# Call an HTTP API without code

> Declare a tool that is one HTTP request, with its URL filled from the input and its credential resolved by Kindgi.

This guide declares a tool that is one HTTP request, with no handler, and sends a credential with it.

## Before you start

- **A pack,** on a runtime such as `kindgi dev`, with Kindgi 0.1's CLI. This guide uses the quickstart's pack, `my-pack`.
- **A one-step flow for each tool,** to try it, built like the one in [Write a tool](https://docs.kindgi.com/v0.1.10/guides/tools/write-a-tool.md#try-it-from-a-flow). This guide uses `my-pack.note-order` and `my-pack.try-token`.
- **Access to httpbin.org,** which echoes requests back, so the examples run without an account.

## Declare the request

A tool that is a single HTTP request needs no handler. You declare the request. Kindgi makes it on every call, fills the URL from the tool's input and adds the credential.

Ask your coding agent: "I want a tool that adds a note to an order with one HTTP request, no code." Kindgi's skills in your pack tell it how ([How it knows Kindgi](https://docs.kindgi.com/v0.1.10/start/coding-agents.md)). For reference, the tool looks like this. This guide calls it `my-pack.add-order-note`:

- **TypeScript**

  `tools/add-order-note/index.ts`

  ```ts
  import { defineTool } from '@kindgi/sdk/define';
  import type { ToolId } from '@kindgi/sdk/types';
  import { z } from 'zod';

  const defined = defineTool({
    id: 'my-pack.add-order-note' as ToolId,
    description: 'Adds a note to an order.',
    version: '0.1.0',
    input: z.object({
      orderId: z.string(),
      text: z.string().min(1),
    }),
    // The API answers with more fields than these: a loose object keeps them.
    output: z.looseObject({
      method: z.string(),
      url: z.string(),
    }),
    effects: [{ kind: 'writes', resource: 'external:orders' }],
    spec: {
      kind: 'http',
      method: 'POST',
      urlTemplate: 'https://httpbin.org/anything/orders/{orderId}/notes',
      headers: [{ name: 'Accept', value: 'application/json' }],
      timeoutMs: 10_000,
    },
  });

  if (defined.kind === 'err') {
    throw new Error(`my-pack.add-order-note failed to compile: ${defined.error.message}`);
  }

  export default defined.value;
  ```

  `spec` takes the place of `handler`. A tool has one or the other.

- **Python**

  `tools/orders_api.py`

  ```python
  from pydantic import BaseModel, Field

  from kindgi import http_tool


  class Note(BaseModel):
      order_id: str = Field(alias="orderId")
      text: str = Field(min_length=1)


  class Echoed(BaseModel):
      method: str
      url: str


  add_order_note = http_tool(
      id="my-pack.add-order-note",
      description="Adds a note to an order.",
      input=Note,
      output=Echoed,
      method="POST",
      url_template="https://httpbin.org/anything/orders/{orderId}/notes",
      headers={"Accept": "application/json"},
      effects=[{"kind": "writes", "resource": "external:orders"}],
      timeout_ms=10_000,
  )
  ```

  `http_tool` declares the tool at module level, like `@tool`, with no function.

- **Java**

  Not in the Java SDK: an HTTP tool without code is declared in TypeScript or Python. In a Java pack, write a tool whose handler makes the request ([When to write a handler instead](https://docs.kindgi.com/v0.1.10/guides/tools/http-tools.md#when-to-write-a-handler-instead)).

- **Scala**

  Not in the Scala SDK: an HTTP tool without code is declared in TypeScript or Python. In a Scala pack, write a tool whose handler makes the request ([When to write a handler instead](https://docs.kindgi.com/v0.1.10/guides/tools/http-tools.md#when-to-write-a-handler-instead)).

* **The URL.** Each `{name}` in the URL template is filled from the input field with that wire name, URL-encoded. Every placeholder must be an input field.
* **The body.** A `POST`, `PUT`, `PATCH` or `DELETE` sends the input fields the URL didn't use, as JSON: here `{"text": …}`. A `GET` sends no body. For another shape, set the request body. `input-passthrough` sends the whole input. `text` sends a template's result as `text/plain`.
* **The answer.** The response's JSON is the tool's output, checked against the output schema. A Zod `z.object` rejects fields it doesn't list, and most APIs return more than you need. So in TypeScript, use `z.looseObject`. A pydantic model allows extra fields as it is.
* **Failures.** A status outside 200 to 299 fails the call. `successStatus`, or `success_status` in Python, changes the range. A response slower than the timeout fails it too: 30 seconds by default. Kindgi doesn't retry.

The tool writes, so it doesn't say `mutating: false`. See [Mark a tool read-only](https://docs.kindgi.com/v0.1.10/guides/tools/read-only-tools.md).

## Run it

Run the tool from `my-pack.note-order`, a one-step flow that calls it. `--flow` is the flow, and `--input` its input:

- **TypeScript**

  ```sh
  pnpm exec kindgi runs start --flow=my-pack.note-order --input='{"orderId":"ord_1001","text":"Customer called"}'
  ```

- **Python**

  ```sh
  uv run kindgi runs start --flow=my-pack.note-order --input='{"orderId":"ord_1001","text":"Customer called"}'
  ```

- **Java**

  Not in the Java SDK: the HTTP tool above is declared in TypeScript or Python. A handler tool you write in a Java pack instead runs from its flow the same way, with `./kindgiw runs start` ([When to write a handler instead](https://docs.kindgi.com/v0.1.10/guides/tools/http-tools.md#when-to-write-a-handler-instead)).

- **Scala**

  Not in the Scala SDK: the HTTP tool above is declared in TypeScript or Python. A handler tool you write in a Scala pack instead runs from its flow the same way, with `./kindgiw runs start` ([When to write a handler instead](https://docs.kindgi.com/v0.1.10/guides/tools/http-tools.md#when-to-write-a-handler-instead)).

```json excerpt
  "flowId": "my-pack.note-order",
  "flowVersion": "0.1.0",
  "status": "completed",
  …
  "output": {
    "url": "https://httpbin.org/anything/orders/ord_1001/notes",
    "args": {},
    "data": "{\"text\":\"Customer called\"}",
    "form": {},
    "json": {
      "text": "Customer called"
    },
    "files": {},
    "method": "POST",
    …
    "headers": {
      "Host": "httpbin.org",
      "Accept": "application/json",
      "User-Agent": "node",
      "Content-Type": "application/json",
      …
    }
  },
```

A request that fails fails the call, and the run's `failureMessage` says why: `got status 404 NOT FOUND`, or `timed out after 1000ms`.

`timeoutMs` covers the whole call, reading the answer included. It's 30 seconds by default. A run's cancel covers the whole call too. So a server that sends its headers and then stalls fails the call. An answer longer than 10 MiB, counted after it's decompressed, fails the call: `http-tool "my-pack.add-order-note": the response body is longer than 10485760 bytes, the most this tool reads`.

## Send a credential

`authorization` names a secret. Kindgi resolves it on every call and sends it as a header. Your code never holds the value.

Ask your coding agent: "I want a tool that checks the API accepts my token." For reference, the tool looks like this. It sends the secret `HTTPBIN_TOKEN`, which you store below:

- **TypeScript**

  `tools/check-token/index.ts`

  ```ts
  import { defineTool } from '@kindgi/sdk/define';
  import type { ToolId } from '@kindgi/sdk/types';
  import { z } from 'zod';

  const defined = defineTool({
    id: 'my-pack.check-token' as ToolId,
    description: 'Checks that the API accepts our token.',
    version: '0.1.0',
    input: z.object({}),
    output: z.looseObject({ authenticated: z.boolean() }),
    effects: [],
    mutating: false,
    spec: {
      kind: 'http',
      method: 'GET',
      urlTemplate: 'https://httpbin.org/bearer',
      authorization: {
        kind: 'bearer',
        secretRef: { envName: 'local', name: 'HTTPBIN_TOKEN' },
      },
    },
  });

  if (defined.kind === 'err') throw new Error(defined.error.message);
  export default defined.value;
  ```

- **Python**

  ```python excerpt
  # in tools/orders_api.py
  class NoInput(BaseModel):
      pass


  class TokenCheck(BaseModel):
      authenticated: bool


  check_token = http_tool(
      id="my-pack.check-token",
      description="Checks that the API accepts our token.",
      input=NoInput,
      output=TokenCheck,
      method="GET",
      url_template="https://httpbin.org/bearer",
      authorization={"kind": "bearer", "secretRef": {"envName": "local", "name": "HTTPBIN_TOKEN"}},
      mutating=False,
  )
  ```

- **Java**

  Not in the Java SDK: an HTTP tool without code is declared in TypeScript or Python. In a Java pack, write a tool whose handler makes the request ([When to write a handler instead](https://docs.kindgi.com/v0.1.10/guides/tools/http-tools.md#when-to-write-a-handler-instead)).

- **Scala**

  Not in the Scala SDK: an HTTP tool without code is declared in TypeScript or Python. In a Scala pack, write a tool whose handler makes the request ([When to write a handler instead](https://docs.kindgi.com/v0.1.10/guides/tools/http-tools.md#when-to-write-a-handler-instead)).

* `bearer` sends `Authorization: Bearer <value>`. For an API that wants the key in its own header, use `{ kind: 'header', headerName: 'X-Api-Key', secretRef: … }`.

* `secretRef` names the secret and the environment it lives in. Under `kindgi dev`, `local` is your app's `.env` and `.env.local`, then Kindgi's own `.kindgi/secrets.env`. Any other name is that environment's file, `.env.<name>`.

* A secret a header can't carry, such as a value pasted with a line break inside it, fails the call by its header's name, never showing the value:

  ```json excerpt
  "message":"Tool \"acme.probe\" handler threw: http-tool \"acme.probe\": its secret local/K for the header \"X-Api-Key\" has a line break, which a header can't carry: store the secret without it"
  ```

  A trailing line break is fine: it's dropped, as HTTP drops it.

Until the secret exists, the call fails and names it:

```json excerpt
  "status": "failed",
  "failureMessage": "handler-error: Tool \"my-pack.check-token\" handler threw: secret-unavailable: tool \"my-pack.check-token\" needs secret \"HTTPBIN_TOKEN\" in env \"local\": No secret \"HTTPBIN_TOKEN\" for env \"local\" in .env, .env.local, .kindgi/secrets.env at /pack",
```

Store it, then run `my-pack.try-token`, a one-step flow that calls the tool:

- **TypeScript**

  ```sh
  printf 'demo-token-123' | pnpm exec kindgi secrets set HTTPBIN_TOKEN --env=local --scope=tenant --from-stdin
  pnpm exec kindgi runs start --flow=my-pack.try-token --input='{}'
  ```

- **Python**

  ```sh
  printf 'demo-token-123' | uv run kindgi secrets set HTTPBIN_TOKEN --env=local --scope=tenant --from-stdin
  uv run kindgi runs start --flow=my-pack.try-token --input='{}'
  ```

- **Java**

  Not in the Java SDK: the HTTP tool above is declared in TypeScript or Python. A Java pack stores the secret the same way, with `./kindgiw secrets set`, and a handler tool reads it ([Give a tool a secret](https://docs.kindgi.com/v0.1.10/guides/tools/give-a-tool-a-secret.md)).

- **Scala**

  Not in the Scala SDK: the HTTP tool above is declared in TypeScript or Python. A Scala pack stores the secret the same way, with `./kindgiw secrets set`, and a handler tool reads it ([Give a tool a secret](https://docs.kindgi.com/v0.1.10/guides/tools/give-a-tool-a-secret.md)).

* **`--env=local`** is the env `kindgi dev` serves, and `--scope=tenant` stores the secret for the whole tenant.
* **`--from-stdin`** reads the value from the pipe. Without it, the command prompts for the value without echoing it.

The first command prints the secret's name, scope, environment and version as JSON, then that it set `HTTPBIN_TOKEN` at tenant in `local`. The run completes:

```json excerpt
  "status": "completed",
  …
  "output": {
    "token": "demo-token-123",
    "authenticated": true
  },
```

httpbin echoes the token back. A real API doesn't. The next call picks the secret up, and nothing restarts. [Store a secret](https://docs.kindgi.com/v0.1.10/guides/secrets/store-a-secret.md) covers `kindgi secrets`.

## When to write a handler instead

An HTTP tool is one request and its JSON answer. Paging, retries, several calls, or reshaping the answer belong in a [code tool](https://docs.kindgi.com/v0.1.10/guides/tools/write-a-tool.md).

In Python, calling an HTTP tool's object raises: Kindgi makes the request, not Python. Try it through `kindgi dev` instead of a unit test:

```text
RuntimeError: Tool "my-pack.add-order-note" is an HTTP tool: the Kindgi runtime makes its request, not Python. Call it through Kindgi (an agent, a flow, `kindgi dev`).
```

## Next

- [Give a tool a secret](https://docs.kindgi.com/v0.1.10/guides/tools/give-a-tool-a-secret.md)
- [Mark a tool read-only](https://docs.kindgi.com/v0.1.10/guides/tools/read-only-tools.md)
- [Write a tool](https://docs.kindgi.com/v0.1.10/guides/tools/write-a-tool.md)
