Release candidate — 1.0.0-rc.9
StitchAPI
Surfaces

HTTP serve

Expose a stitch over HTTP for callers that aren't in-process.

Reach for stitch serve when a caller can't import your stitches in-process — a remote service, another language, a shell script — and you want to expose the registry of named stitches over a thin local HTTP front door instead.

Example

Start the server, then call a registered stitch by name with a JSON body:

# Serves the registry on http://127.0.0.1:8787 by default.
stitch serve

# List the available stitch names.
curl http://127.0.0.1:8787/

# Run the `me` stitch; the request body is its input.
curl -X POST http://127.0.0.1:8787/stitch/me \
  -H 'content-type: application/json' \
  -d '{}'

The server runs the named stitch through the same engine as an in-process call and returns the final validated result as JSON.

Options

Two routes serve the registry of named stitches:

  • GET / lists the available stitch names.
  • POST /stitch/:name runs that stitch; the JSON request body is its input. Any other method returns 405, and an unknown name returns 404.

:name is one of the names GET / lists: a registry key, which for stitch serve is the export name. A stitch's configured name labels its traces and does not route to it; a module's default export has no export name, so it is keyed by its configured name.

The server binds to 127.0.0.1:8787 by default. Responses are the final validated result as JSON; send Accept: text/event-stream (or add ?stream=1) to receive the live event stream as SSE instead.

This front door runs stitches you name; it is not a webhook endpoint, and StitchAPI does not receive webhooks at all — that is your server's job. Receiving a signed webhook draws exactly where the line falls and what the library still owns on the far side of it.

Anti-pattern: don't move the bind off the 127.0.0.1 loopback default to a public interface so a remote caller can reach it directly — the front door has no auth of its own, so a public bind turns the registry into an open proxy that runs every stitch for anyone who can reach the port. Keep it on loopback and put a fronting layer (a reverse proxy or gateway that authenticates the caller) in front of it; the stitch still holds its own credential and resolves it server-side, never handing it to the caller. See Capability, not credential.

Failures

A failed run answers with a non-2xx status and a JSON body of { error, status }. An upstream error status passes through (401, 429, 503); a failure with no status, such as a timeout or a transport error, answers 502. status in the body is the upstream's own status, when there was one.

error is the standard reason phrase for the response status, not the failure's own message:

{ "error": "Bad Gateway" }

The raw message can name an internal host (getaddrinfo ENOTFOUND payments.internal.corp) or repeat an upstream's wording to whoever called the server. Every @stitchapi/* host adapter withholds it by default, and serve does the same.

SSE mode withholds the same detail. The frames and their event types stay, and these fields are dropped:

  • an event: error frame's message is the reason phrase, and so is the frame sent when the stream throws;
  • an event: start frame has no url and no template, which name the upstream host and its route (/internal/ledger/{id});
  • an event: progress or event: info frame has no detail. On a retry progress frame that field is the raw transport error text;
  • a frame's name, on start and error, is the name you addressed the stitch by in /stitch/:name. A stitch's own name defaults to its path, so /internal/ledger/{id} would otherwise appear there;
  • an event: drift frame's finding has no detail. For a failed output validation that field is the validator's issue message, which can repeat the value the upstream sent. The finding keeps its level, path, change and sample.

Anything else a stream carries is unchanged: delta and result payloads, the request input echoed on start, span ids, surface and transport, and a progress frame's status and errorType.

When the callers are trusted, such as in local development or on an internal network, turn the detail on with stitch serve --disclose, or serve(registry, { disclose: true }) in code. It restores the raw message and the dropped SSE fields together.

A delegate-backoff rate limit (a RateLimitError from throttle.delegate) also sends a Retry-After header, in whole seconds, when the upstream gave one. The value is capped at one day. A caller can then wait as long as the upstream asked.

Programmatic API

The CLI is a thin wrapper. To embed the same server in your own process, import serve from the stitchapi/serve subpath and hand it a registry of named stitches:

import { me } from './stitches';

import { serve } from 'stitchapi/serve';

const handle = await serve({ me });
console.log(`listening on ${handle.url}`);

// later
await handle.close();

serve(registry, options?) returns a handle with the bound url, port, the underlying server, and a close() method. It binds 127.0.0.1:8787 by default; pass { port: 0 } to let the OS pick an ephemeral port, and { disclose: true } to send failure detail as described in Failures.

A served stitch is the same definition as the in-process function surface and the stitch CLI — one source of truth, three ways to call it.

See also

On this page