> ## Documentation Index
> Fetch the complete documentation index at: https://repost.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Test TypeScript Sends Without the Network

> The stub transport, custom transports for full-request assertions, and deterministic generators.

Everything a send does before the network — validation, defaults, serialization, the typed method tree — runs for real under the stub transport, so tests exercise your schema without any HTTP. The compiler is part of the test surface too: wrong payloads, unknown events, and missing fields fail at build time.

## The stub transport

`stubTransport` is exported from the runtime subpath and plugs into the `transport` option:

```ts theme={"languages":{"custom":["/languages/repost.json"]}}
import { createRepostClient } from "@repost/client";
import { stubTransport } from "@repost/client/runtime";

const repost = createRepostClient({
  apiKey: "test",
  transport: stubTransport,
});

const result = await repost.webhooks.user.created({
  customerId: "customer_test",
  data: { id: "user_test", email: "test@example.com" },
});

console.log(result.id); // "msg_stub"
```

The returned result echoes the envelope's `type`, `customerId`, and `timestamp` with the fixed id `msg_stub`. Credentials still resolve before the stub runs, so tests exercise the production configuration path; any non-empty key satisfies it.

<Note>
  `stubTransport` is imported from `@repost/client/runtime`, not from the package root.
</Note>

## Asserting on the full request

Use the native testing helper to assert on the serialized request and idempotency key. It implements the same one-attempt `Transport` interface as the production transport:

```ts theme={"languages":{"custom":["/languages/repost.json"]}}
import { ScriptedStubTransport } from "@repost/client/testing";

const transport = new ScriptedStubTransport().enqueueResponse(
  202,
  JSON.stringify({
    id: "msg_test",
    type: "user.created",
    customerId: "customer_test",
    timestamp: "2026-01-01T00:00:00.000Z",
  }),
  { "content-type": "application/json" },
);

const repost = createRepostClient({
  apiKey: "test",
  transport,
  generators: { now: () => "2026-01-01T00:00:00.000Z" },
});

await repost.webhooks.user.created({
  customerId: "customer_test",
  data: { id: "user_test", email: "test@example.com" },
});

expect(JSON.parse(transport.requests[0].bodyText)).toMatchObject({
  type: "user.created",
  customerId: "customer_test",
});
expect(transport.requests[0].idempotencyKey).toBeTruthy();
```

`bodyText`, `bodyBytes`, headers, attempt number, timeout, and idempotency key are captured from the actual runtime attempt.

## Deterministic values

Pin the injectable generators to freeze timestamps and generated ids:

```ts theme={"languages":{"custom":["/languages/repost.json"]}}
const repost = createRepostClient({
  apiKey: "test",
  transport: stubTransport,
  generators: {
    now: () => "2026-01-01T00:00:00.000Z",
    uuid: () => "00000000-0000-4000-8000-000000000000",
  },
});
```

With `now` pinned, the envelope timestamp and every `@default(now())` field are fixed.

## Testing retry behavior

Queue failures and responses on `ScriptedStubTransport`, then use `deterministicClientOptions()` or `ManualClock` from `@repost/client/testing` to advance retry delays without sleeping. Set `maxAttempts`, `retryBaseDelayMs`, and `attemptTimeoutMs` on the client; retries never live inside the transport.

## Continue

<Columns cols={3} className="gap-y-4">
  <Card title="Quickstart" icon="rocket" href="/docs/send/typescript/quickstart" cta="Start" arrow="true">
    The full setup, from install to first send.
  </Card>

  <Card title="Reliability" icon="shield-check" href="/docs/send/typescript/reliability" cta="Errors" arrow="true">
    The behaviors these seams let you test.
  </Card>

  <Card title="Configuration" icon="sliders-horizontal" href="/docs/send/typescript/configuration" cta="Configure" arrow="true">
    The options and transport seams in full.
  </Card>
</Columns>
