Sandbox
Every project has two isolated data planes: live and sandbox. Sandbox is a full copy of the product surface — tickets, emails, templates, articles, webhooks, API keys — where nothing ever reaches a real customer. Use it to build and test your integration before touching production data.
The credential bounds the environment
Which data plane you touch is decided by the API key you authenticate with, and a live key may step down to sandbox for a request:
sk_sandbox_…keys read and write sandbox data only — even when given the id of a live resource, they cannot see it, and they cannot ask for live.sk_live_…keys read and write live data, unless the request carriesHelmdesk-Environment: sandbox(or?environment=sandbox), which switches that one request to the sandbox plane. This is how you — or an agent holding your key — try a flow without reaching a real inbox.
A sandbox key asked for live is refused with 403 environment.forbidden; the downgrade is the only direction. Create a dedicated sandbox key by switching the dashboard to Sandbox mode (the toggle in the top bar) and creating a key on the API Keys page — keys inherit the mode you are in — or mint one from a live key with POST /v1/api-keys and environment: "sandbox".
import { HelmdeskClient } from '@helmdesk/sdk'
// Point your staging/dev environment at sandbox — nothing else changes.
const helmdesk = new HelmdeskClient({
apiKey: process.env.HELMDESK_SANDBOX_API_KEY!, // sk_sandbox_...
})
// Works exactly like live, but the ticket lands in the sandbox plane.
await helmdesk.tickets.create({
subject: 'Test ticket',
body: 'Created from staging',
customerEmail: 'test@example.com',
})
// Or keep one live key and switch per call — the API sends
// Helmdesk-Environment: sandbox and never reaches a real inbox.
const live = new HelmdeskClient({ apiKey: process.env.HELMDESK_API_KEY! })
await live.withEnvironment('sandbox').emails.send({
templateKey: 'welcome',
to: { email: 'test@example.com' },
variables: { name: 'Test' },
})
// HELMDESK_ENVIRONMENT=sandbox in the process env sets the default.Emails are captured, never delivered
Sandbox email sends render fully — real template, real layout, real variables — but stop short of any provider. They appear in Email Logs with status sandboxed (Captured), where you can open the rendered HTML exactly as a customer would have received it. No email configuration or provider credentials are needed in sandbox, and captured sends never count against your plan quota.
Sandbox tickets behave the same way: status changes and staff replies never email the customer address on the ticket.
Logs in sandbox
Log events shipped with an sk_sandbox_… key land in the sandbox plane. They are quota-exempt (they never count against your monthly log event limit), retained for a fixed 3 days on every plan, and the Log Watch agent ignores them — sandbox errors never page anyone. Use sandbox keys to verify your log shipping integration without polluting real error groups.
Limits and retention
Sandbox is for testing a flow, not for holding data. Sandbox rows never count toward your plan's ticket allowance or email quota, but two rules stop the sandbox from becoming a second production store:
- Retention. Sandbox tickets, email sends, feedback conversations and requests are removed 30 days after they are created; sandbox customers 30 days after they were last seen. Sandbox templates, layouts, and articles are never removed — they are what you promote to live.
- Row caps per project. A sandbox holds at most a fixed number of rows of each kind at once — on Free, 200 tickets, 200 customers, 500 email sends, and 100 feedback conversations and 100 requests; on every paid plan, five times that. Creating past a cap returns
403 sandbox.cap_reachedwith the current count and the cap indetails. Delete rows, or use Reset sandbox in Project Settings to clear every transactional sandbox row and its attachments in one step.
Rate limits are the same in both planes: 100 requests per minute per API key. Sandbox attachments count toward your storage quota like any other upload, and agent runs on sandbox data count toward your monthly agent-run cap — AI costs the same either way.
Reset the sandbox
Between test runs, clear the whole sandbox plane in one call — the same purge the Reset sandbox button in Project Settings runs. It deletes every sandbox ticket (with messages, tags, and attachments), feedback conversation and request, email send, customer, and log event/issue, and removes the storage objects behind the attachments. Sandbox templates, layouts, and articles survive — they are what you promote. It only ever touches sandbox rows, whatever key you hold, so live data is never in scope. There is no undo. Requires the projects:manage scope.
// SDK — works with a live key too; the reset is always the sandbox
const result = await helmdesk.sandbox.reset()
// { tickets: 14, customers: 9, emailSends: 40, feedbackConversations: 2,
// feedbackRequests: 3, logEvents: 120, logIssues: 4, attachments: 1,
// freedBytes: 204800, environment: "sandbox", projectId, projectSlug }
// curl
curl -X POST https://helmdesk.dev/api/v1/sandbox/reset \
-H "Authorization: Bearer sk_live_..." \
-H "Helmdesk-Project: abc-salon"Minting a sandbox key from a live key
An app's dev or staging config wants a key that can never reach a real inbox. A live key with api_keys:manage can mint one by passing environment: "sandbox" to POST /v1/api-keys. The downgrade is the only direction: a sandbox request (a sandbox key, or a live key sent with Helmdesk-Environment: sandbox) can never mint a live key and gets a 403 if it asks. The new key can only reach projects the caller reaches and only hold scopes the caller holds.
const dev = await helmdesk.apiKeys.create({
name: 'ABC Salon dev',
projects: ['abc-salon'],
scopes: ['emails:send', 'logs:write', 'tickets:write'],
environment: 'sandbox',
})
// dev.key -> "sk_sandbox_..." — write it into .env.development now; it is shown once.The dashboard toggle
The Live/Sandbox toggle in the dashboard's top bar switches every page — tickets, email logs, templates, articles, webhooks, canned responses, suppressions, activity, analytics — to the selected plane. In Sandbox mode an amber banner reminds you where you are, and anything you create is sandbox content. Public portals (your knowledge base and support pages) always serve live content, and AI agents only run on live data.
Promote and pull
Content moves between planes deliberately, never automatically:
- Promote a template — publishes the sandbox template as a new live version behind the same key, so your production sends pick it up atomically.
- Promote an article — copies it to live (new slugs arrive as drafts; existing slugs update in place and keep their publish state).
- Pull from live — copies your live knowledge base or a live template into sandbox so you can iterate against real content.
Webhooks and the widget
Webhook endpoints are per-environment: endpoints created in Sandbox mode only receive sandbox events, so your staging receiver never sees production traffic. The embeddable widget's identity secret is split the same way — hd_widget_live_… and hd_widget_sandbox_…— and the secret that signed a user's identity hash decides which plane's conversations the widget shows.
What sandbox never does
Sandbox data never emails a real customer, never appears on your public portal, never fires live webhooks, and never counts toward your ticket allowance, email quota, or analytics. It does not stay, either: transactional sandbox rows are removed after 30 days, and each project holds a fixed number of them at once.