# 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.2'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.5.0/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.5.0/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**

  `src/main/java/mypack/tools/AddOrderNote.java`

  ```java
  package mypack.tools;

  import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
  import com.kindgi.pack.Tool;
  import com.kindgi.pack.http.HttpMethod;
  import com.kindgi.pack.http.HttpToolSpec;
  import jakarta.validation.constraints.Size;

  public final class AddOrderNote {
    public record Input(String orderId, @Size(min = 1) String text) {}

    // The API answers with more fields than these; @JsonIgnoreProperties keeps them.
    @JsonIgnoreProperties(ignoreUnknown = true)
    public record Output(String method, String url) {}

    public static final Tool<Input, Output> TOOL = Tool.define("my-pack.add-order-note")
        .description("Adds a note to an order.")
        .version("0.1.0")
        .input(Input.class)
        .output(Output.class)
        .effect("writes", "external:orders")
        .http(HttpToolSpec.builder()
            .method(HttpMethod.POST)
            .urlTemplate("https://httpbin.org/anything/orders/{orderId}/notes")
            .header("Accept", "application/json")
            .timeoutMs(10_000)
            .build());

    private AddOrderNote() {}
  }
  ```

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

- **Scala**

  `src/main/scala/mypack/tools/AddOrderNote.scala`

  ```scala
  package mypack.tools

  import com.fasterxml.jackson.annotation.JsonIgnoreProperties
  import com.kindgi.pack.http._
  import com.kindgi.pack.scaladsl._
  import jakarta.validation.constraints.Size

  object AddOrderNote {
    final case class Input(orderId: String, @Size(min = 1) text: String)

    // The API answers with more fields than these; @JsonIgnoreProperties keeps them.
    @JsonIgnoreProperties(ignoreUnknown = true)
    final case class Output(method: String, url: String)

    val tool: Tool[Input, Output] = Tool[Input, Output]("my-pack.add-order-note")
      .description("Adds a note to an order.")
      .version("0.1.0")
      .effect("writes", "external:orders")
      .http(HttpToolSpec.builder()
        .method(HttpMethod.POST)
        .urlTemplate("https://httpbin.org/anything/orders/{orderId}/notes")
        .header("Accept", "application/json")
        .timeoutMs(10_000)
        .build())
  }
  ```

  `.http(spec)` takes the place of `.handler`, on a `val` of an object under `tools/` like any tool.

* **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. In Java and Scala, annotate the output type `@JsonIgnoreProperties(ignoreUnknown = true)` to keep the fields it doesn't list.
* **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.5.0/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**

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

- **Scala**

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

```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**

  `src/main/java/mypack/tools/CheckToken.java`

  ```java
  package mypack.tools;

  import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
  import com.kindgi.pack.Tool;
  import com.kindgi.pack.http.HttpAuthSpec;
  import com.kindgi.pack.http.HttpMethod;
  import com.kindgi.pack.http.HttpToolSpec;
  import com.kindgi.pack.http.ToolSecretRef;

  public final class CheckToken {
    public record Input() {}

    @JsonIgnoreProperties(ignoreUnknown = true)
    public record Output(boolean authenticated) {}

    public static final Tool<Input, Output> TOOL = Tool.define("my-pack.check-token")
        .description("Checks that the API accepts our token.")
        .version("0.1.0")
        .input(Input.class)
        .output(Output.class)
        .mutating(false)
        .http(HttpToolSpec.builder()
            .method(HttpMethod.GET)
            .urlTemplate("https://httpbin.org/bearer")
            .authorization(new HttpAuthSpec.Bearer(new ToolSecretRef("local", "HTTPBIN_TOKEN")))
            .build());

    private CheckToken() {}
  }
  ```

- **Scala**

  `src/main/scala/mypack/tools/CheckToken.scala`

  ```scala
  package mypack.tools

  import com.fasterxml.jackson.annotation.JsonIgnoreProperties
  import com.kindgi.pack.http._
  import com.kindgi.pack.scaladsl._

  object CheckToken {
    final case class Input()

    @JsonIgnoreProperties(ignoreUnknown = true)
    final case class Output(authenticated: Boolean)

    val tool: Tool[Input, Output] = Tool[Input, Output]("my-pack.check-token")
      .description("Checks that the API accepts our token.")
      .version("0.1.0")
      .readOnly
      .http(HttpToolSpec.builder()
        .method(HttpMethod.GET)
        .urlTemplate("https://httpbin.org/bearer")
        .authorization(new HttpAuthSpec.Bearer(new ToolSecretRef("local", "HTTPBIN_TOKEN")))
        .build())
  }
  ```

* `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**

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

- **Scala**

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

* **`--env=local`** is the env `kindgi dev` serves, and `--scope=tenant` stores the secret for the whole workspace.
* **`--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.5.0/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.5.0/guides/tools/write-a-tool.md).

In Python, Java and Scala, calling an HTTP tool in-process raises: Kindgi makes the request, not your SDK. Try it through `kindgi dev` instead of a unit test. In Python it's a `RuntimeError`; in Java and Scala, an `UnsupportedOperationException` that says the same:

```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.5.0/guides/tools/give-a-tool-a-secret.md)
- [Mark a tool read-only](https://docs.kindgi.com/v0.5.0/guides/tools/read-only-tools.md)
- [Write a tool](https://docs.kindgi.com/v0.5.0/guides/tools/write-a-tool.md)
