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

# Journeys

> Report one important flow in your product, so a support ticket shows how far the customer got and what failed.

A journey is one flow in your product that has a clear start, a clear success, and ways to fail. Examples are connecting an integration, checkout, submitting onboarding, and inviting a teammate. Your code reports four events for it: the journey started, the customer attempted an action, an attempt failed, and the journey succeeded.

When the customer opens a support conversation during a journey, the ticket arrives with that journey attached. The Inbox shows the journey's name, where it stood when the customer wrote, where it stands now, how many attempts and failures there were, and the latest failure. The investigation reads the same events in the session timeline.

## Requirements

* `@gethinto/browser-sdk` 0.5.0 or later for journeys. Help offers need `@gethinto/browser-sdk` 0.8.0 or later, and `@gethinto/native-support-react` 0.5.1 or later for the React slot.
* Capture that knows the signed-in customer, through `initialIdentity` or `bindConversationIdentity` with `{ kind: "external_id", value: user.id }`. [Hinto Signal](/native-support) sets this up. Without a signed-in customer, `journey()` returns a handle that does nothing and, with `debug: true`, logs why.

## Report a journey

Call `journey(key)` where the flow begins. Call `attempt(action)` in the submit handler, before the request. Call `fail(...)` in the error branch you already have. Call `succeed()` only after your server confirms the result, never on click.

```ts theme={null}
import type { HintoClient } from "@gethinto/browser-sdk";

type ConnectResult =
  { ok: true } | { ok: false; reason: "invalid_key" | "declined" | "offline" | "server_error" };

const CATEGORY = {
  invalid_key: "client_validation",
  declined: "third_party",
  offline: "network",
  server_error: "server",
} as const;

// Call where the flow begins, for example when the connect screen mounts.
export function startStripeConnection(capture: HintoClient) {
  return capture.journey("stripe_connection", { metadata: { plan: "pro" } });
}

// Call from the submit handler.
export async function connectStripe(
  capture: HintoClient,
  submit: () => Promise<ConnectResult>,
): Promise<ConnectResult> {
  const journey = capture.journey("stripe_connection");
  journey.attempt("connect");
  const result = await submit();
  if (result.ok) journey.succeed();
  else journey.fail({ action: "connect", category: CATEGORY[result.reason], feedback: "inline" });
  return result;
}

// After an OAuth redirect, the same tab resumes the same journey.
export function finishStripeRedirect(capture: HintoClient, confirmed: boolean) {
  const journey = capture.journey("stripe_connection");
  if (confirmed) journey.succeed();
  else journey.fail({ action: "authorize", category: "third_party", feedback: "summary" });
}
```

`journey(key)` is idempotent while the journey is open. It returns the open instance for that key in this tab, or starts a new one. The instance lasts through reloads and redirects in the same tab, so a flow that leaves for an OAuth provider and comes back is still one journey. After `succeed()`, the next `journey(key)` starts a new instance. Two tabs track two instances.

### When a submit control can be disabled

A disabled button emits no click, so the customer can try to submit and nothing happens. If your form disables its submit control while it is invalid, report the attempt from a wrapper that sees the attempt, and say how the customer was told:

* `feedback: "inline"`: the error appears next to the field or the control.
* `feedback: "summary"`: the error appears in a banner, toast, or summary at the top.
* `feedback: "none"`: the customer saw nothing.

## Offer help on the journey

Reporting a journey alone shows nothing to the customer. When the journey's events suggest the customer is stuck, for example after repeated failures, Hinto Signal decides whether to offer help. The offer appears only where your product placed a slot for it. Without a slot, Hinto still records the decision, but the customer never sees it.

With the React kit, place `<Support.HelpOffer>` next to the step the journey covers, under your `Support.Provider`:

```tsx theme={null}
import { Support } from "@gethinto/native-support-react";

// Render inside your Support.Provider, on the screen where the journey runs.
// `connect` is the handler that already reports the journey.
export function ConnectStripeStep({ connect }: { connect: () => Promise<unknown> }) {
  return (
    <section>
      <button onClick={() => void connect()}>Connect Stripe</button>
      <Support.HelpOffer journey="stripe_connection" topic="connecting Stripe" onRetry={connect} />
    </section>
  );
}
```

* `journey` is the same key you pass to `journey(key)`, or an array of keys.
* `topic` names the step in the customer's words. It fills the card's title and the message that Contact support sends.
* `onRetry` is the step's existing handler. Without it, the card has no Try again button.
* Keep the slot mounted. Do not wrap it in a condition. It renders nothing until support has started for a signed-in customer and there is an offer, and it shows the offer only while it is on screen.
* Place one slot per journey on a screen, on the screen where the customer fails. A slot on a different page never shows the offer.

The card has three actions. **Contact support** sends one message in the customer's words that carries the journey, and opens your support view. **Try again** runs `onRetry`. **Not now** hides the offer. Nothing is sent to your team until the customer chooses Contact support.

The slot sits on your product page, outside the conversation. Theme tokens you set on the conversation's class do not reach it. Set them on `:root` or on a class you pass as the slot's `className`, and pass `theme="dark"` in dark mode. [Customization](/native-support-customization) lists the tokens.

Hinto decides when to offer help. Repeated failures, a missing prerequisite or permission, a rejected input, or a failure the customer never saw can bring an offer. Your team controls it in Hinto under Settings > Hinto Signal: **Proactive help** turns offers on or off for each journey, and **Offer limits** sets how long an offer stays, how long to wait between offers, and how many a customer sees in a week. A customer who chooses Not now is not offered help on that journey again for the quiet period.

### Offer help without the React kit

With `@gethinto/browser-sdk/conversation` alone, for example inside your own chat, read `runtime.offers` and draw the offer yourself:

1. `offers.subscribe(listener)` and `offers.snapshot()` give the current offer. Signal checks for one only while something is subscribed, the page is visible and online, and the customer has an open journey.
2. When the snapshot's `status` is `"eligible"`, call `offers.show(id)` and draw the offer only when it returns `"ok"`. That records the impression. `"closed"` means the offer is gone, and `"unavailable"` means the call did not complete. After a reload, a `"shown"` status means draw it again without calling `show`. `"none"` means there is no offer.
3. **Not now** calls `offers.dismiss(id)`.
4. **Contact support** calls `offers.contact(id, { message })` with a sentence in the customer's words. On `"started"`, call `runtime.open()`, which sends it. While that message is on its way, `contact` answers `"unavailable"` and a typed send answers `"busy"`.
5. After `journey.succeed()`, call `await offers.refresh()` so the offer goes away at once.

With session capture only, there is no conversation, so there are no offers.

## Choose the journey

Instrument a flow your product already has. Add calls to its existing handlers, and do not add product behavior, server routes, or test hooks to make it fail. If the flow is a stub, or has no real point where it succeeds or fails, leave it without a journey.

Pick one flow first. A good first journey:

* happens while the customer is signed in,
* has an explicit success that your server confirms,
* fails in ways customers write to support about.

Connecting an integration is usually the best choice, because its success is explicit and journeys survive the OAuth redirect.

## Vocabulary

**Key, action, and category** are tokens: lowercase letters, digits, and underscores, starting with a letter, at most 39 characters. `stripe_connection`, `connect`, and `third_party` are tokens. The Inbox shows the key as a label: `stripe_connection` reads as "Stripe connection".

**Failure categories.** Map your errors to one of these:

| Category | Use it when |
| - | - |
| `client_validation` | The input was invalid and your app rejected it. |
| `prerequisite` | Something must happen first, such as a missing plan or setup. |
| `permission` | The customer is not allowed to do this. |
| `third_party` | A provider such as Stripe or an OAuth server refused or broke. |
| `network` | The request never got an answer. |
| `server` | Your server returned an error. |
| `unknown` | None of the above fits. |

**Feedback** is `none`, `inline`, or `summary`, as described above.

## Limits

| Limit | Value |
| - | - |
| Metadata keys | 8 per event, tokens only |
| Metadata values | Strings of 200 characters or fewer, finite numbers, or booleans. No objects or arrays. |
| Event size | 2 KiB. A larger event keeps its type and step and drops its metadata. |
| Steps per journey | 200. Later calls are dropped. |
| Journeys tracked per browser | 20 per customer per tab. The oldest finished journey is removed first. |
| Journeys on one message | 5: open journeys and journeys that finished in the last 10 minutes, the newest first. |

Metadata is for tokens such as a plan name or a provider, never for customer data. The SDK redacts metadata strings with the same patterns it uses for every payload, and an event whose metadata has a key that looks like a secret, such as `password`, `api_key`, or `tokens_used`, keeps its step and drops all of its metadata. Journeys never carry free text: a failure has a category, not a message. See [Privacy controls](/privacy).

## Check it

Run the flow once, the natural way, with `debug: true` in `initHinto`. You do not need to make it fail. The SDK writes its debug lines with `console.warn` and a `[Hinto]` prefix, so they appear as warnings, not errors.

The SDK logs one `ingest response: accepted N, duplicates N, rejected N` line per batch, and a batch can hold journey events and other capture together. In the browser's Network tab, find the `/v1/ingest` request whose body contains the `journey` events. The journey works when the response line for that batch shows `rejected 0`. A rejected event is dropped on its own and never blocks other capture, so a nonzero count means a key, action, category, or metadata value broke a rule above.

To check the help offer slot, open the screen where the journey runs and confirm the page has an element with `data-hinto-part="help-offer"`. It is empty until there is an offer. To see the card without making the flow fail, render the slot with the kit's testing fixture and call `controls.presentOffer({ journey })`. The package's `AGENT_INSTALL.md` describes the fixture.

If the journey reports correctly but a stuck customer never sees an offer, the slot is missing, is on a different screen, or was off screen while the offer was open. Proactive help may also be off for the journey, or the customer may have reached an offer limit.

## In the Inbox

When a signed-in customer sends a Native Support message during a journey, the ticket links that journey. The first message that links it records how far the journey had progressed, so the Inbox can say "failing when they wrote". Events that arrive later fill in the history, and a later success changes the current outcome. The block is labeled **Reported by your app**, because journey events come from your browser code and a customer could forge them.

Tickets without journeys look the same as before.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.