Help Scout

Lightweight, dependency-free, in-memory fake of the Help Scout Mailbox API v2 for testing code that talks to the Help Scout REST API directly.

Default port: 4786

Quick start

import { HelpscoutServer } from "./services/helpscout/src/server.js";

const server = new HelpscoutServer(4786);
await server.start();
// ... run your app/tests ...
await server.stop();

Point a client at it (grab a token, then call resources):

const base = "http://127.0.0.1:4786";

const tok = await fetch(`${base}/v2/oauth2/token`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ grant_type: "client_credentials", client_id: "parlel", client_secret: "pat-parlel" }),
}).then((r) => r.json());

const created = await fetch(`${base}/v2/conversations`, {
  method: "POST",
  headers: { Authorization: `Bearer ${tok.access_token}`, "Content-Type": "application/json" },
  body: JSON.stringify({ subject: "Help me", mailboxId: 1, type: "email", customer: { email: "u@parlel.dev" } }),
});
// 201, with a `Resource-ID` response header pointing at the new conversation.

Access via MCP / preview URL

Plain HTTP at http://127.0.0.1:4786, reachable through the parlel MCP/preview proxy under the slug helpscout.

Implemented operations

POST /v2/oauth2/token is unauthenticated; all other /v2/* routes require Authorization: Bearer <token> (any non-empty bearer works). State is in-memory and ephemeral.

Collections use the HAL shape: { _embedded: { conversations: [...] }, _links: {...}, page: {...} }. Creates return 201 with a Resource-ID header (and Location) and no body, matching the real API.

OAuth — /v2/oauth2/token

Conversations — /v2/conversations

Customers — /v2/customers

CRUD surface (one of firstName/lastName/emails required on create).

Mailboxes — /v2/mailboxes

List/retrieve. A default mailbox (id: 1) is seeded.

Service & inspection operations (parlel extensions)

Surface coverage

This emulator faithfully replicates the API surface most application code and agents exercise. Anything below the supported lines is either an intentional design choice for a fast, zero-cost local emulator (✓ By design) or a candidate for a future release (⟳ Roadmap) — never a silent inaccuracy.

Legend: ✅ fully supported · ◐ accepted (stored, not strictly enforced) · ✓ by design · ⟳ on the roadmap.

FeatureStatus
OAuth2 client-credentials token grant✅ Supported
Conversations CRUD✅ Supported
Customers CRUD✅ Supported
Mailboxes list/retrieve (seeded)✅ Supported
HAL _embedded / _links / page envelopes✅ Supported
Resource-ID create header (201, no body)✅ Supported
Threads / attachments / tags / workflows⟳ Roadmap
Token expiry / refresh-token flow◐ Tokens never expire (until reset)
Embedded sub-resource expansion (?embed=)⟳ Roadmap
Rate limiting (429)✓ By design — Never throttles — local tests run at full speed, zero cost

Error codes & shapes

Errors use { error, message }; validation errors add _embedded.errors[].

StatusWhen
400malformed JSON / missing grant_type / validation failure
401no Authorization: Bearer header
404unknown id / resource
405method not allowed for the path

Manifest

See services/helpscout/manifest.json: name helpscout, port 4786, protocol http, healthcheck /health, startup ≈ 100ms, env HELPSCOUT_APP_ID, HELPSCOUT_APP_SECRET, HELPSCOUT_BASE_URL.

<!-- parlel:testenv:start -->

Configuration — test.env

Copy these into your test.env (used by the bridge sidecar flow). Tokens are Parlel's seeded test credentials — any non-empty value is accepted by the emulator, so you rarely need to change them. Swap in real credentials only when pointing at the live service in prod.env.

HELPSCOUT_APP_ID=parlel
HELPSCOUT_APP_SECRET=pat-parlel
HELPSCOUT_BASE_URL=http://parlel-bridge:4786
<!-- parlel:testenv:end -->