Skip to content

Call an HTTP API without code

View as Markdown

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

  • 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-order and my-pack.try-token.
  • Access to httpbin.org, which echoes requests back, so the examples run without an account.

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:

tools/add-order-note/index.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.

  • 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.

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

Terminal window
pnpm exec kindgi runs start --flow=my-pack.note-order --input='{"orderId":"ord_1001","text":"Customer called"}'
"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",
…
}
},
Excerpt

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.

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:

tools/check-token/index.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;
  • 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:

    "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"
    Excerpt

    A 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",
Excerpt

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

Terminal window
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='{}'
  • --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:

"status": "completed",
…
"output": {
"token": "demo-token-123",
"authenticated": true
},
Excerpt

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.

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`).