Test Mode
Build and validate integrations against an isolated sandbox without touching production data.
Overview
Parseo supports a test mode sandbox modeled after Stripe's live/test split. A team may hold both live and test API keys at the same time; each key operates in its own fully siloed environment.
- Live keys — prefix
parseo_live_— read and write real production data. - Test keys — prefix
parseo_test_— read and write sandbox data only.
A test key cannot read, create, update, or delete any live-mode resource, and a live key cannot see any test-mode resource. The two worlds never cross.
Creating a test-mode key
Create a test key in the dashboard under Settings → API Keys → Create key → Test mode. Test keys carry the same scope model as live keys (invoice:read, invoice:write, webhook:manage, etc.) and obey the same rate-limit and quota rules.
The key string itself announces its mode:
parseo_test_<48 base62 characters>Inspect the first 12 characters (parseo_test_ vs parseo_live_) to tell the two apart in logs and secret stores.
How isolation works
Every resource created by a public-API key is tagged with the key's mode at write time. Every read scopes its query by the caller's mode. Concretely:
- Invoices — test invoices never appear in
GET /invoicesresponses for a live key, and vice versa. - Contacts —
GET /contacts,POST /contacts,PATCH /contacts/{id},DELETE /contacts/{id}only see and write contacts in the caller's mode. - Clients —
GET /clientsonly returns clients in the caller's mode.X-Client-Idheaders that reference a client in the other mode are rejected. - Webhooks — endpoints, event deliveries, and redelivery operations are siloed. A webhook created by a test key receives only test-mode events; a live webhook receives only live-mode events.
- Audit logs — partner-initiated writes are recorded in separate audit streams per mode.
Cross-mode references are rejected
If a request from a test key references a live resource (or a live key references a test resource) through either the body or a header, the API returns:
HTTP 400 Bad Request
Content-Type: application/problem+json
{
"type": "https://docs.parseo.app/errors/error-registry",
"title": "Client mode mismatch",
"status": 400,
"code": "validation.client_mode_mismatch",
"detail": "The referenced client belongs to a different mode than the authenticating key."
}This surfaces integration mistakes immediately instead of silently coercing or leaking data across the sandbox boundary. See the Error Registry for the canonical entry.
Divergences from a strict sandbox
A few behaviors are intentionally shared across modes. They are listed here so you are not surprised when building test fixtures:
/me/credits and /me/quota.credits return the live balance
Test-mode operations do not consume credits, so reporting a separate zero-filled balance for test keys would be misleading. Both endpoints return the team's live credit balance regardless of the key's mode. This lets you inspect your real remaining quota from a test integration without switching credentials.
/me returns real team identity
GET /me returns the real team name and the authenticating key's prefix. This is deliberate — partners use it to verify which key is active in a given environment. No production resource contents are exposed.
Rate limits and queue depth are shared per team plan
Live and test traffic run on separate counters, but both draw from the same team plan. A burst of test-mode uploads will not deduct from your live counter, yet both modes are individually capped by your plan's per-minute and per-hour rate-limit ceilings, and by the same queue-depth limit.
Treat test mode as "free from billing, bounded by fairness."
Known limitations in v1
- Dashboard UI. Test-mode resources are API-only for this release. The dashboard currently shows live-mode invoices, contacts, and clients; a live/test toggle is planned in a follow-up.
- No bulk reset endpoint yet. Stripe-style
POST /admin/test-mode/reset(clear all test data in one shot) is not implemented in v1. To reset a test environment, delete resources individually via the API or recreate the test key. A dedicated reset endpoint is on the roadmap. - Internal ingestion path depth-in-defense. The partner-facing public API enforces mode invariants at the controller layer on every request. Internal ingestion helpers (used by mailbox and scraper ingress) rely on the ingress guard above them rather than a per-call mode assertion. All partner-initiated code paths are fully protected; internal tools should continue to use the partner API or explicitly assert the mode before calling into ingestion services.
Example: end-to-end test-mode flow
1. Upload an invoice with a test key
curl -X POST https://app.parseo.app/external/v1/invoices \
-H "Authorization: Bearer parseo_test_..." \
-H "X-Client-Id: cl_test_9Z8y7X..." \
-H "Idempotency-Key: $(uuidgen)" \
-F "file=@invoice.pdf"Response:
{
"jobId": "job_test_abc123",
"status": "processing"
}Poll GET /external/v1/jobs/job_test_abc123 until status is
completed (the fixture pipeline writes a canned invoice) or failed
(when you set hints.testScenario=failed on the upload). The
invoiceId in the completed response is the inv_test_... ID you
pass to the invoice read/list endpoints.
2. List test-mode contacts
curl https://app.parseo.app/external/v1/contacts \
-H "Authorization: Bearer parseo_test_..."Returns only contacts that were created with test-mode keys. Contacts created by live keys are never returned, even if you know their IDs.
3. Cross-mode reference is rejected
A test key targeting a live client produces an immediate 400:
curl -X POST https://app.parseo.app/external/v1/invoices \
-H "Authorization: Bearer parseo_test_..." \
-H "X-Client-Id: cl_live_5F4e3D..." \
-F "file=@invoice.pdf"{
"status": 400,
"code": "validation.client_mode_mismatch",
"detail": "The referenced client belongs to a different mode than the authenticating key."
}The same rule applies to any body field (clientId, buyerContactId, sellerContactId, etc.) that references a resource in the other mode.
Recommended workflow
- Create one live and one test key per environment (staging, CI, production).
- Seed your test environment by calling the public API with the test key — never by copying live IDs.
- Use the same scope set for both modes so your test coverage reflects real authorization behavior.
- In CI, assert on the key prefix (
parseo_test_) before running destructive operations as a defense-in-depth check. - When rotating credentials, rotate live and test keys independently — they are tracked separately in the dashboard.
