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
- Create a test key in the panel.
- Create a profile and connect a sandbox account.
- Create a post scheduled a minute out, and watch the target move
scheduled → queued → publishing → published. - Point a webhook endpoint at your local receiver and verify the signature (Webhooks).
- Repeat until the flow is right, then swap
sk_test_forsk_.
Nothing else in your code changes.
Authentication
zhyper accepts two kinds of credential. They are not interchangeable, and a single request may carry exactly one.
Webhooks
zhyper posts events to an HTTPS endpoint you register. Delivery is retried with backoff; your endpoint should be idempotent, because a retry can arrive after you have already proce