Skip to main content
The sandbox is where you build, test and certify your integration. It runs the same API as production, at a different base URL, with guard rails that make it safe to experiment: you never receive a real contact detail, you never spend a credit, and you can trigger every state, notice and error on demand. RFC 2119 keywords apply.

Base URL and credentials

The sandbox base URL is https://vort-partner-api.vercel.app/sandbox/v1. Vort sends you a sandbox partner key (vpk_live_…), an activation code for a sandbox customer organization, and (on request) your webhook registration at onboarding. Keep the base URL in configuration: the production URL is different and is issued after certification. Every sandbox response carries the header:
A production response never carries it. Log it next to your requests, so a support conversation can tell the two apart at a glance. Webhooks sent by the sandbox carry the same header. Everything in the Integration flow, Errors and the API reference applies unchanged: the headers, the error envelope, rate limits, idempotency, the request log. The differences are listed on this page and on Certification fixtures, and nowhere else.

What is real and what is synthetic

A sandbox reveal answers 200 with the usual fields plus "sandbox": true:
The e-mail is always candidate-<candidate_id>@sandbox.vort.invalid (the .invalid top-level domain can never receive mail) and the phone is always +97250000 followed by the last four digits of the candidate id. The same candidate always gets the same contact, so you can test that a second reveal is idempotent. After a reveal, GET /runs/{run_id} reports that candidate as contact_state: "revealed", and a candidate_revealed webhook is queued once per (run, candidate) when your webhook is registered. The sandbox never answers 202 pending or 402 insufficient_credits on a reveal; the scoping errors (run_not_found, user_not_found, candidate_not_in_run) behave exactly as in production.

Candidate names and identifiers

On runs of your own jobs, the sandbox masks direct identifiers before they leave Vort:
  • name becomes a stable pseudonym, Candidate XXXX (four hex digits derived from the candidate id; the same candidate always gets the same pseudonym).
  • Inside evidence, the candidate’s real name (and each part of it) is replaced by the same pseudonym.
  • In evidence, title, company and location, any e-mail address, URL (including profile, LinkedIn and photo links) and phone number is replaced by [email hidden in sandbox], [link hidden in sandbox] or [phone hidden in sandbox].
Everything else stays real: title, company, location, verdict, requirement met and evidence, notices. That is what you are demoing.
Real names are shown only after the Data Processing Annex is signed, when Vort turns them on for your partner account. Contact identifiers stay masked in the sandbox regardless. Your UI MUST NOT depend on the name format: render name as an opaque string.

Sandbox limits

Certification fixture runs do not count toward the daily run quota. A POST /runs refused with 422 creates nothing and does not count either.

Certification fixtures

A POST /runs whose job_external_id starts with cert- never runs the engine. It returns immediately with a deterministic payload you can build and test against. The fixtures, what each returns, and what you must verify are on Certification fixtures.

Simulating errors

Add this header to any authenticated sandbox request to get the exact error a real failure of that kind returns. The request is authenticated and logged first, so it appears in Vort’s request log like a real failure.
curl
Any other value answers 422 invalid_request with the valid values in allowed. Use it for certification C-20: a GET /runs with rate_limited MUST be retried only after Retry-After; a POST /runs with rate_limit_unavailable MUST NOT be retried automatically. Simulated requests count toward your per-minute rate limit like any other request.

Reveal price override

On GET /runs/{run_id}, an integer from 0 to 1000 replaces reveal_price_credits on every candidate of that response, for that request only. Use it to prove your UI shows the price from the payload and never a stored or hard-coded value (C-13). A reveal still charges 0. Anything that is not an integer 0 to 1000 answers 422 invalid_request.

Test webhooks

curl
string
required
run_completed | run_failed | candidate_revealed
boolean
default:"true"
true (default) | false
Answer: 202 {"event_id": "<uuid>", "event": "...", "run_id": "<uuid>", "valid_signature": true}. The delivery goes out through the normal webhook queue, usually within a minute, to the URL Vort registered for you, with the envelope you would get in production (event_id equals the returned event_id). The payload is a fixture: organization_external_id is this organization’s, job_external_id is cert-webhook, run_id is a fresh id that does not exist on GET /runs (answer 2xx anyway; your handler must not depend on the run existing), run_completed carries progress, candidate_revealed carries candidate_id: 990000000001.
With "valid_signature": false, the same event is signed with a deliberately wrong secret. Your endpoint MUST answer non-2xx (typically 401) and MUST NOT process it (C-18). A bad-signature test is sent once and never retried; Vort sees whether you rejected it.
If no webhook is registered for your partner account, the call answers 409 webhook_not_configured. Ask Vort to register your HTTPS URL and share the signing secret out of band.

Requirement validation examples

Send these as the body of POST /runs to see each code. A 422 creates nothing and costs nothing. All but location_as_requirement also work on any fixture job (fixtures have no location). For location_as_requirement, first create a job of your own with "location_text": "Tel Aviv, Israel". Rejections answer 422 requirements_rejected, with rejected[] (and warnings[]), each item carrying the requirement index, the code, message_he / message_en and suggestion_he / suggestion_en. Show the message and suggestion verbatim under the requirement at index (C-05). Warnings answer 202 with warnings[] of the same shape; the run starts (C-06).

Rejections

kind must be must or nice. A requirements value that is not an array is also invalid_kind, at index -1.
At most 3 musts.
On a run of your own job, the job title counts as one must (Vort adds it as the last requirement unless one of yours is the title), so three musts of your own are refused too, at the third, with the message “The job title counts as one must, so at most 2 more musts are possible (3 in total).”
Same text after trimming and case-folding, whatever the kind.
Over 300 characters.
The requirement is a place that is part of the job’s location_text. Needs a job of your own. First POST /jobs:
then POST /runs:
On the job above; indexes 1 empty_text, 2 invalid_kind, 3 duplicate, 4 too_long, 5 location_as_requirement, 7 too_many_musts:

Warnings (the run starts)

A term shorter than 3 characters.
A bounded years range that can act as a ceiling.
A must that profiles almost never state (salary, availability, willingness, clearance, driving licence, soft traits).
Indexes 0, 1, 2; valid on your own job too, where the job title makes it three musts.
Warnings on a fixture run are returned on the 202 like on a real run; the fixture’s payload still uses its fixed requirement list.

Sandbox-only codes and headers

Request headers (ignored in production): Response header: x-vort-environment: sandbox on every response. Route (answers 404 not_found in production): POST /sandbox/webhooks/test. Error codes that only the sandbox returns: Handle them like any unknown code, by HTTP status: your production client never sees them.