# Noctant External Journal App Integration Guide

Date: 2026-06-29
Status: shareable private-beta integration guide

This guide is safe to give to another app team or coding AI so that app can
integrate with Noctant as an independent journaling/operator application.

It intentionally describes the external contract only. It does not require the
reader to understand Noctant's internal services, deployment, databases, or
admin workflows.

## Current Truth

Noctant can support a controlled private-beta journaling integration today.

The supported first integration is:

1. The journal app provisions a user in Noctant.
2. The journal app creates a Noctant simulated trading account for that user.
3. The journal app creates a short-lived hosted launch URL.
4. The user opens Noctant, logs in, and trades.
5. Noctant sends signed webhooks back to the journal app.
6. The journal app reconciles through Operator API readback endpoints.
7. If a webhook is missed, the journal app requests a bounded replay.

This is a real third-party style integration even when both apps are owned by
Noctant. The journal app must treat Noctant as an external service: API key for
REST, webhook signing secret for incoming webhooks, stable external ids, and
explicit reconciliation.

This is not yet a public self-serve developer portal. Operator creation,
credential provisioning, launch origins, event allowlists, and historical
replay/backtest grants are still enabled by Noctant admin onboarding.

## Staging URLs

The private-beta docs surface is:

- `https://docs-staging.noctant.com/`
- `https://docs-staging.noctant.com/operator-api/self-onboarding`
- `https://docs-staging.noctant.com/operator-api/journal-app`
- `https://docs-staging.noctant.com/openapi/operator-api.json`

The Operator API staging base URL is:

- `https://api-staging.noctant.com`

The docs host publishes contracts only. It does not publish API keys, webhook
signing secrets, launch tokens, cookies, or partner-specific credentials.

## Integration Surface Decision

Decision date: 2026-06-29

The final Noctant operator integration model is:

```text
HTTPS Operator API + generated OpenAPI contract + optional generated SDKs
```

The HTTPS API is the real contract. Every operator must be able to integrate
with plain HTTPS requests and HTTPS webhooks without using a Noctant-owned SDK.

The OpenAPI document is the public machine-readable contract. It must be
generated from the same Rust request/response contracts that the server uses.
Do not hand-write a separate OpenAPI file and do not maintain a separate
partner-only schema by hand.

SDKs are convenience layers. They may improve developer experience, type
safety, examples, and onboarding speed, but they must stay thin and generated
from the OpenAPI contract. An SDK must not become the only supported
integration path and must not hide business rules that are absent from the
HTTPS/OpenAPI contract.

The docs portal is the human and AI handoff surface. It explains flows,
security rules, examples, retry behavior, webhook verification, and current
private-beta limitations. It does not replace OpenAPI and it must not publish
secrets.

### Why This Is The Target

- Plain HTTPS is universal. Any app, language, backend, or AI coding agent can
  integrate without waiting for a Noctant SDK.
- Generated OpenAPI prevents drift. The published spec stays tied to the same
  Rust types the server deserializes and serializes.
- Generated SDKs are useful but optional. They reduce boilerplate for common
  stacks while keeping the API contract language-neutral.
- A docs URL is necessary for true third-party work. A separate app should not
  need access to the Noctant source repo to understand the integration.
- Webhooks stay HTTPS too. Operators receive signed events at their own HTTPS
  endpoint, verify signatures, store event ids idempotently, and reconcile
  through REST readback.

### Current Private-Beta State

- HTTPS Operator API is available at `https://api-staging.noctant.com`.
- Generated OpenAPI is published at
  `https://docs-staging.noctant.com/openapi/operator-api.json`.
- Generated TypeScript SDK exists in the Noctant repo.
- The TypeScript SDK is not yet published as a clean public npm package.
- External apps can integrate today with raw HTTPS or by generating their own
  client from the OpenAPI URL.
- The docs portal is staged at `https://docs-staging.noctant.com`.

### Long-Term Public Shape

The public launch shape should be:

```text
developers.noctant.com
api.noctant.com/v1
api.noctant.com/v1/openapi.json
npm install @noctant/operator-api
```

The SDK package is part of the final developer experience, but the support
policy must remain: if a request is valid according to HTTPS/OpenAPI, it is a
valid integration even if it does not use the SDK.

## Sources Of Truth

Use these artifacts, in this order:

1. `docs/api/operator-api.openapi.json`
2. `packages/operator-api-typescript-sdk`
3. `packages/operator-api-typescript-sdk/examples/private-beta-self-onboarding.ts`
4. `packages/operator-api-typescript-sdk/examples/private-beta-journal-account-creator.ts`
5. `packages/operator-api-typescript-sdk/examples/private-beta-historical-launch.ts`

Do not hand-write request/response types from this guide. The OpenAPI document
is generated from the same Rust contracts the server uses, and the TypeScript
SDK is generated from that OpenAPI file.

Until the SDK is published as a public package, a private-beta app can either
use the SDK from this repo or generate its own client from the OpenAPI document.
Raw HTTP is also acceptable if it follows the OpenAPI contract exactly.

For a single step-by-step private-beta handoff, use
`https://docs-staging.noctant.com/operator-api/self-onboarding`. That guide
combines the environment values, `GET /v1/me` verification, API smoke, webhook
proof, hosted launch proof, and activation checklist into one path.

## What Noctant Gives The Journal App

For each environment, Noctant provides:

- Operator API base URL, for staging currently `https://api-staging.noctant.com`.
- One Operator API key.
- One webhook signing secret, if webhooks are enabled.
- Allowed hosted launch origins, for example `https://journal-staging.example.com`.
- Enabled Operator API scopes.
- Enabled webhook event types.
- Execution profile key for the account type, for example the private-beta
  `journal_account_creator` setup.

The API key authenticates calls from the journal app to Noctant.

The webhook signing secret verifies calls from Noctant to the journal app.

Do not send the webhook signing secret back to Noctant in API requests. Do not
log API keys, webhook signing secrets, launch tokens, cookies, or authorization
headers.

## Environment Variables For The Journal App

Recommended names:

```sh
NOCTANT_OPERATOR_API_BASE_URL=https://api-staging.noctant.com
NOCTANT_OPERATOR_API_KEY=...
NOCTANT_WEBHOOK_SIGNING_SECRET=...
NOCTANT_EXECUTION_PROFILE_KEY=...
NOCTANT_RETURN_URL=https://journal-staging.example.com/noctant/return
```

If the app receives webhooks:

```sh
NOCTANT_WEBHOOK_URL=https://journal-staging.example.com/webhooks/noctant/operator
```

`NOCTANT_RETURN_URL` must use an origin that Noctant has allowed for that
operator. The return URL can include a path, but the allowed origin is only the
scheme, host, and optional port.

## Authentication

Send the Operator API key as:

```http
Authorization: Bearer <NOCTANT_OPERATOR_API_KEY>
```

For every write request, send:

```http
x-noctant-request-id: <uuid>
```

The request id is for logs and audit. It is not the same thing as idempotency.
Endpoint-specific idempotency rules still apply.

Before doing anything else, call:

```http
GET /v1/me
```

The app must verify that the returned operator id, org id, environment, scopes,
webhook config, and allowed origins match the expected onboarding record.

## Stable Ids

The journal app must keep stable ids on its side:

- `external_user_id`: the user's id in the journal app.
- `external_account_id`: the account/session/account-like object id in the
  journal app.

Noctant treats those ids as opaque strings. They must not change between
retries.

`POST /v1/users` is idempotent by `(operator_id, external_id)`.

`POST /v1/accounts` is idempotent by `(operator_id, external_account_id)`.

If the journal app retries with the same external ids, Noctant returns the
existing objects instead of creating duplicates.

## First Supported Flow: Journal Account Creator

Use this flow first. It is the cleanest private-beta path for an independent
journaling app.

### 1. Verify Operator Context

```ts
import {
  client,
  getOperatorContext,
} from "@noctant/operator-api-typescript-sdk";

client.setConfig({
  baseUrl: process.env.NOCTANT_OPERATOR_API_BASE_URL!,
  auth: () => process.env.NOCTANT_OPERATOR_API_KEY!,
});

const me = await getOperatorContext();
if (me.error || !me.data) {
  throw new Error(`GET /v1/me failed with ${me.response?.status}`);
}

console.log({
  operatorId: me.data.operator.operator_id,
  orgId: me.data.operator.org_id,
  environment: me.data.operator.environment,
  scopes: me.data.operator.operator_api_scopes,
  webhook: me.data.operator.webhook,
  allowedOrigins: me.data.operator.allowed_origins,
});
```

### 2. Provision The User

```ts
import { provisionUser } from "@noctant/operator-api-typescript-sdk";

const user = await provisionUser({
  headers: { "x-noctant-request-id": crypto.randomUUID() },
  body: {
    external_id: "journal-user-123",
    email: "trader@example.com",
    display_name: "Private Beta Trader",
  },
});
```

The email helps the hosted launch/login experience, but identity merging must
remain controlled by Noctant's launch/auth flow. The journal app should not try
to merge Noctant users by email on its own.

### 3. Create The Sponsored Trading Account

```ts
import { createAccount } from "@noctant/operator-api-typescript-sdk";

const account = await createAccount({
  headers: { "x-noctant-request-id": crypto.randomUUID() },
  body: {
    external_user_id: "journal-user-123",
    external_account_id: "journal-account-123",
    account_name: "Journal practice account",
    initial_balance: "100000.00",
    currency: "USD",
    execution_profile_key: process.env.NOCTANT_EXECUTION_PROFILE_KEY!,
    feed_source: "noctant",
    provider_type: "journal",
    rules_owner: "none",
    branding_policy: "partner_managed",
    visibility_policy: "same_sponsor_only",
    claim_allowed: true,
    marketing_allowed: false,
  },
});
```

This creates a persistent simulated account sponsored by the operator. It is not
a historical replay/backtest session.

### 4. Create A Hosted Launch

```ts
import { createLaunchSession } from "@noctant/operator-api-typescript-sdk";

const launch = await createLaunchSession({
  headers: { "x-noctant-request-id": crypto.randomUUID() },
  body: {
    external_user_id: "journal-user-123",
    external_account_id: "journal-account-123",
    requested_product_mode: "terminal",
    requested_route: "/terminal",
    return_url: process.env.NOCTANT_RETURN_URL!,
  },
});

if (launch.error || !launch.data) {
  throw new Error(`launch failed with ${launch.response?.status}`);
}

const urlForBrowserRedirect = launch.data.launch_url;
```

Launch URLs are short-lived, one-time URLs. Create one only when the browser is
ready to redirect. Do not store the embedded token in logs.

### 5. Read Back Account State

After trading, reconcile using REST. Do not rely only on webhooks.

Useful endpoints:

- `GET /v1/accounts/by-external-id/summary`
- `GET /v1/accounts/by-external-id/orders`
- `GET /v1/accounts/by-external-id/positions`
- `GET /v1/accounts/by-external-id/trades`

The generated SDK exposes these as:

- `getAccountSummaryByExternalId`
- `listOrdersByExternalId`
- `listPositionsByExternalId`
- `listTradesByExternalId`

Trades are paginated. If `next_cursor` is present, keep reading with
`before_closed_at` and `before_trade_id`.

## Webhooks

Noctant sends webhooks to the operator's configured HTTPS webhook URL.

Each delivery includes:

- `X-Noctant-Event-Id`
- `X-Noctant-Event-Type`
- `X-Noctant-Signature`

`X-Noctant-Signature` format:

```text
t=<unix_seconds>,v1=<hex-hmac-sha256>
```

The HMAC is computed over:

```text
<unix_seconds>.<raw body bytes>
```

using the operator's webhook signing secret.

The journal app must:

1. Read the raw request body bytes before JSON parsing.
2. Verify `X-Noctant-Signature`.
3. Reject stale timestamps.
4. Verify the event id and event type headers match the JSON envelope.
5. Store `X-Noctant-Event-Id` in durable storage before/with business effects.
6. Treat repeated `X-Noctant-Event-Id` as a duplicate success, not a new event.
7. Reject the same event id if the body facts differ from the stored event.

This makes webhook handling safe across retries, redeploys, and receiver
restarts.

## Expected Journal Webhook Events

For the first journaling flow, the important events are account/trading events
such as:

- `account.created`
- `account.balance_changed`
- `order.accepted`
- `order.rejected`
- `order.filled`
- `position.opened`
- `position.updated`
- `position.closed`
- `trade.closed`

The exact enabled event set is returned by `GET /v1/me` and controlled by
Noctant admin onboarding. The journal app should accept only event types it
understands and should ignore or reject unknown event types according to its
own receiver policy.

## Delivery Readback And Replay

For support and reconciliation, use:

```http
GET /v1/webhooks/deliveries
```

To request bounded webhook replay:

```http
POST /v1/webhooks/replays
```

Example:

```ts
import { createWebhookReplay } from "@noctant/operator-api-typescript-sdk";

const replay = await createWebhookReplay({
  headers: { "x-noctant-request-id": crypto.randomUUID() },
  body: {
    idempotency_key: "replay-journal-account-123-2026-06-29T00:00Z",
    external_account_id: "journal-account-123",
    event_types: ["trade.closed"],
    from_created_at: "2026-06-29T00:00:00Z",
    to_created_at: "2026-06-29T01:00:00Z",
    limit: 100,
  },
});
```

Use the same `idempotency_key` only with the same body facts. Reusing the same
key with different facts is a caller bug and should be treated as a conflict.

## Historical Replay And Manual Backtest

Historical replay/backtest is a separate product lane from sponsored live-style
accounts.

Current policy:

- Use `POST /v1/historical-launch-sessions`.
- Do not create a fake account for replay/backtest.
- Do not use `POST /v1/launch-sessions` for `replay` or `backtest`.
- Noctant must grant the operator access to the feed, time window, and product
  mode.
- Historical replay is accountless read-only playback.
- Historical backtest is accountless historical trading.
- Historical backtest webhooks use event types such as
  `backtest.trade.closed` and `backtest.session.completed`.

The maintained example is:

```text
packages/operator-api-typescript-sdk/examples/private-beta-historical-launch.ts
```

Use historical replay/backtest only after the normal journaling account flow is
proven against the target app. It is intentionally more controlled because it
must never mix live account state with historical simulation state.

## Retry Rules

Retry:

- Network timeouts.
- `429`, using `Retry-After` when present.
- `503`.
- Idempotent user/account creation with the same external ids.
- Idempotency-key endpoints with the same idempotency key and same body facts.

Do not blindly retry:

- `400`: request bug.
- `401`: missing or invalid API key.
- `403`: scope, operator status, origin, or grant problem.
- `404`: wrong id or object not visible to this operator.
- `409`: idempotency or state conflict; inspect before retrying.

## Minimum Smoke Test Before Trusting The Integration

The journal app is not considered integrated until this full proof passes:

1. `GET /v1/me` returns the expected operator and scopes.
2. `POST /v1/users` creates or returns the test user.
3. `POST /v1/accounts` creates or returns the test account.
4. `POST /v1/launch-sessions` returns a launch URL.
5. Browser launch opens Noctant and lands in the expected route/account.
6. User places at least one test trade.
7. Journal app receives and verifies at least one signed webhook.
8. Journal app stores webhook event id idempotently.
9. Journal app reads account summary/orders/positions/trades through REST.
10. Journal app reads webhook delivery ledger.
11. Journal app requests a bounded replay for one known window.
12. Same replay request with the same idempotency key is safe.
13. Same replay key with different facts returns a conflict.

Do not record secrets in smoke evidence. Record only object ids, event ids,
timestamps, status codes, and sanitized request ids.

## Copy-Paste Prompt For Another Coding AI

Use this when asking another AI to integrate a journal app:

```text
Integrate this app with Noctant as a private-beta external journal operator.

Use the Noctant Operator API, not Noctant internal databases or services.
Authenticate REST calls with Authorization: Bearer NOCTANT_OPERATOR_API_KEY.
First call GET /v1/me and verify operator id, org id, scopes, webhook policy,
and allowed origins.

Implement the journal_account_creator flow:
1. POST /v1/users with stable external_id.
2. POST /v1/accounts with stable external_account_id.
3. POST /v1/launch-sessions with requested_product_mode "terminal".
4. Redirect the browser to launch_url immediately.
5. Reconcile with account summary/orders/positions/trades readback endpoints.
6. Receive Noctant webhooks at the configured HTTPS webhook URL.
7. Verify X-Noctant-Signature over "<timestamp>.<raw body bytes>" using
   NOCTANT_WEBHOOK_SIGNING_SECRET.
8. Deduplicate by X-Noctant-Event-Id in durable storage.
9. Use GET /v1/webhooks/deliveries and POST /v1/webhooks/replays for support
   and bounded backfill.

Do not log API keys, webhook signing secrets, launch tokens, cookies, or raw
Authorization headers. Do not use internal Noctant databases. Do not implement
historical replay/backtest until the normal journal account flow passes.
```

## What This Guide Does Not Yet Cover

Still private/internal:

- Public self-serve operator application and approval.
- Public package publishing for the SDK.
- Public developer portal UI.
- Automated operator credential issuance by external users.
- Production-grade support docs for arbitrary partners.
- Final public historical replay/backtest onboarding.

That is deliberate. The current goal is a true private-beta external app
experience with a narrow, testable, non-confusing contract.
