Call an HTTP API without code
This guide declares a tool that is one HTTP request, with no handler, and sends a credential with it.
Before you start
Section titled “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. This guide uses
my-pack.note-orderandmy-pack.try-token. - Access to httpbin.org, which echoes requests back, so the examples run without an account.
Declare the request
Section titled “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). For reference, the
tool looks like this. This guide calls it my-pack.add-order-note:
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.
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.
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).
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).
- 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,PATCHorDELETEsends the input fields the URL didn't use, as JSON: here{"text": …}. AGETsends no body. For another shape, set the request body.input-passthroughsends the whole input.textsends a template's result astext/plain. - The answer. The response's JSON is the tool's output, checked against
the output schema. A Zod
z.objectrejects fields it doesn't list, and most APIs return more than you need. So in TypeScript, usez.looseObject. A pydantic model allows extra fields as it is. - Failures. A status outside 200 to 299 fails the call.
successStatus, orsuccess_statusin 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.
Run it
Section titled “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:
pnpm exec kindgi runs start --flow=my-pack.note-order --input='{"orderId":"ord_1001","text":"Customer called"}'uv run kindgi runs start --flow=my-pack.note-order --input='{"orderId":"ord_1001","text":"Customer called"}'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).
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).
"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
Section titled “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:
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;# in tools/orders_api.pyclass 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,)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).
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).
-
bearersendsAuthorization: Bearer <value>. For an API that wants the key in its own header, use{ kind: 'header', headerName: 'X-Api-Key', secretRef: … }. -
secretRefnames the secret and the environment it lives in. Underkindgi dev,localis your app's.envand.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:
"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"ExcerptA trailing line break is fine: it's dropped, as HTTP drops it.
Until the secret exists, the call fails and names it:
"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:
printf 'demo-token-123' | pnpm exec kindgi secrets set HTTPBIN_TOKEN --env=local --scope=tenant --from-stdinpnpm exec kindgi runs start --flow=my-pack.try-token --input='{}'printf 'demo-token-123' | uv run kindgi secrets set HTTPBIN_TOKEN --env=local --scope=tenant --from-stdinuv run kindgi runs start --flow=my-pack.try-token --input='{}'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).
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).
--env=localis the envkindgi devserves, and--scope=tenantstores the secret for the whole tenant.--from-stdinreads 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:
"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 covers kindgi secrets.
When to write a handler instead
Section titled “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.
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:
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`).