zhyper docs

Test mode

Test mode is a full copy of the API that never reaches a real social platform. You can connect accounts, schedule posts, watch them publish and receive webhooks — end to end — without anything appearing on Instagram or LinkedIn.

It is not a mock layer bolted on for tests. It is how the product runs when the mode is test.

Getting into it

Use a key that starts with sk_test_:

export function createTestModeClient(testKey: string, baseUrl: string): ZhyperClient {
  // A key prefixed `sk_test_` ALWAYS runs in test mode. The mode is bound to the
  // key ITSELF; no header can change it, and there is no way to reach live data
  // by accident.
  return new ZhyperClient({ apiKey: testKey, baseUrl });
}

That is the whole switch. The mode is bound to the key, so there is no code path where a test integration accidentally publishes for real.

Panel sessions are the exception: a signed-in user works in both modes and picks one per request with the X-Zhyper-Mode: live|test header (the panel exposes this as a toggle). Omitting it means live.

An unrecognised value is rejected rather than treated as live. Falling back silently would let a client believe it was in the sandbox while posting to a real audience, and that mistake cannot be undone.

Sending a mode header that contradicts your key is also rejected:

export async function modeConflictCode(
  liveKey: string,
  baseUrl: string,
): Promise<string> {
  // The X-Zhyper-Mode header only means something for a PANEL SESSION. Send a
  // header that contradicts an API key and the request is rejected -- it does not
  // silently fall back to the key's mode.
  const zhyper = new ZhyperClient({ apiKey: liveKey, baseUrl, mode: 'test' });
  try {
    await zhyper.request('listProfiles', {});
    return 'no_error';
  } catch (error) {
    return (error as { code?: string }).code ?? 'unknown';
  }
}

What actually happens

When a target is dispatched, the worker resolves an adapter for that execution mode. Live and test adapters are separate objects and the registry refuses to share an instance between them. Test-mode adapters talk to a local fake-platforms service that speaks the real protocol shape — the same request bodies, the same status codes, the same error taxonomy.

The consequence worth relying on: an empty test registry does not fall back to live. If a platform has no sandbox adapter, dispatch fails loudly instead of publishing for real.

Data separation

Test and live data live side by side under the same team, separated by a mode column that the server applies to every query:

export async function testDataIsIsolated(
  testClient: ZhyperClient,
  liveClient: ZhyperClient,
): Promise<{ inTest: boolean; inLive: boolean }> {
  const created = await testClient.request<Schemas['ProfileDto']>('createProfile', {
    body: { name: 'Sandbox profile', timezone: 'UTC' },
  });

  const testNames: string[] = [];
  for await (const p of testClient.paginate<Schemas['ProfileDto']>('listProfiles', {})) {
    testNames.push(p.id);
  }
  const liveNames: string[] = [];
  for await (const p of liveClient.paginate<Schemas['ProfileDto']>('listProfiles', {})) {
    liveNames.push(p.id);
  }

  // Data created in test mode does NOT appear in a live listing. Same team, same
  // table, different `mode`; the split is enforced server side, by RLS and at the
  // query level.
  return {
    inTest: testNames.includes(created.id),
    inLive: liveNames.includes(created.id),
  };
}

Profiles, accounts, posts and queues created in test mode are invisible to live requests and vice versa. You never have to remember to filter.

What is the same, and what is not

Same:

  • Every endpoint, request shape and response shape.
  • Validation, error codes, and the error envelope.
  • Rate limits, idempotency, pagination.
  • Webhook delivery, including signatures — so you can develop your receiver against real signed traffic.

Different:

  • No request leaves the deployment for a social platform.
  • Platform-specific quotas are simulated rather than enforced by the platform.
  • Media is processed locally; nothing is uploaded to a platform CDN.

A realistic loop

  1. Create a test key in the panel.
  2. Create a profile and connect a sandbox account.
  3. Create a post scheduled a minute out, and watch the target move scheduled → queued → publishing → published.
  4. Point a webhook endpoint at your local receiver and verify the signature (Webhooks).
  5. Repeat until the flow is right, then swap sk_test_ for sk_.

Nothing else in your code changes.

On this page