All docs pages
REST API#
Everything the TryFast app does goes through the same REST API, so your scripts can create projects, build and launch experiments and read results. AI agents get the same operations through the MCP server.
- Base URL:
https://tryfast.eu/api/v1 - OpenAPI 3.1 description:
https://tryfast.eu/api/openapi.yaml
Authentication#
| Credential | How to send it | Who uses it |
|---|---|---|
API key tf_pk_... |
Authorization: Bearer tf_pk_... |
Scripts, CI, headless agents. Create one in the app under API keys, or get one when you create a workspace from a script. |
Session cookie tryfast_session |
Sent by the browser | The TryFast app, after sign-in. |
Browser cookie tryfast_grant |
Sent by the browser | The TryFast app for workspaces without an account. |
API keys have one of three permissions, chosen when you create them: Read (experiments:read), Build (adds experiments:write; launches wait for approval) and Full (adds experiments:publish). A key never has more rights than the role of the person who created it, and it stops working when that person leaves the workspace. The key is shown once; TryFast stores only a hash.
The recovery token of an unsaved workspace (tfe_...) is not a credential for the API. It only saves the workspace to an account or opens it in a browser; give its link to the person.
Approving or rejecting an approval request accepts only a person's browser session, never a Bearer credential.
Quick start#
Who am I, and which workspaces can I see:
curl -s https://tryfast.eu/api/v1/me -H "Authorization: Bearer $TRYFAST_API_KEY"
Create a project and get its snippet:
curl -s https://tryfast.eu/api/v1/workspaces/ws_4fK9zQ2mPx/projects \
-H "Authorization: Bearer $TRYFAST_API_KEY" -H 'Content-Type: application/json' \
-d '{"name": "Acme shop", "domains": ["acme-shop.de", "www.acme-shop.de"]}'
curl -s https://tryfast.eu/api/v1/projects/prj_k2L9mQ0aZx/snippet \
-H "Authorization: Bearer $TRYFAST_API_KEY"
Create a click goal and a complete draft experiment:
curl -s https://tryfast.eu/api/v1/projects/prj_k2L9mQ0aZx/goals \
-H "Authorization: Bearer $TRYFAST_API_KEY" -H 'Content-Type: application/json' \
-d '{"name": "Started checkout", "key": "started_checkout", "type": "click", "selector": "a.checkout"}'
curl -s https://tryfast.eu/api/v1/projects/prj_k2L9mQ0aZx/experiments \
-H "Authorization: Bearer $TRYFAST_API_KEY" -H 'Content-Type: application/json' \
-d '{
"name": "Free shipping headline",
"hypothesis": "Naming free shipping in the headline makes more visitors start checkout.",
"targeting": {"pages": {"include": [{"match": "exact", "value": "/pricing"}]}},
"variations": [
{"name": "Control", "is_control": true},
{"name": "Free shipping", "changes": [
{"op": "set_text", "selector": "#hero h1", "value": "Free shipping on every order"}
]}
],
"goals": [{"goal_id": "goal_Pu9rC4s3Ez", "role": "primary"}]
}'
Launch it, then read the results:
curl -s -X POST https://tryfast.eu/api/v1/experiments/exp_7Hq2kD9xQa/start \
-H "Authorization: Bearer $TRYFAST_API_KEY" -H 'Content-Type: application/json' -d '{}'
curl -s https://tryfast.eu/api/v1/experiments/exp_7Hq2kD9xQa/results \
-H "Authorization: Bearer $TRYFAST_API_KEY"
With a Full key the start answers 200 and the experiment runs. With a Build key it answers 202 with "status": "awaiting_approval" and an approve_url for a person to open.
Start without an account#
A script can create a workspace without an account, with an optional project and API key:
curl -s https://tryfast.eu/api/v1/anonymous-workspaces \
-H 'Content-Type: application/json' \
-d '{"website_url":"https://shop.example","issue_api_key":true}'
The 201 response contains workspace, project and snippet, api_key (shown once), mcp_url, expires_at and claim_url. The claim_url carries the workspace's recovery token after #token=: it is for the person, never for the script or agent. Launches requested with the key wait for approval, and approving needs a signed-in person who opens the claim_url. Creation is limited to 3 workspaces per hour per network address. Creating a workspace accepts the Terms of Service and the Data Processing Agreement; the Privacy policy explains how we handle data. Workspaces and accounts explains the limits and the lifecycle of unsaved workspaces.
Conventions#
- JSON in and out (
Content-Type: application/json),snake_casefield names, timestamps in RFC 3339 UTC. - Ids carry a prefix:
ws_,prj_,exp_,var_,goal_,aud_,apr_,key_. A project also has a public 10-characterkeyused in its snippet URL; it is not a credential. - Traffic shares are integer basis points (
10000= 100%). Rates and lifts are fractions (0.12= 12%). - Lists return
{"items": [...], "next_cursor": "..."}; passlimit(1 to 200, default 50) andcursorfor the next page.next_cursorisnullon the last page. - Dry runs: every create, update, delete and lifecycle action of projects, goals, audiences, experiments and variations, and the visitor erase, accepts
?dry_run=true(the OpenAPI file lists it on each). Nothing is saved; the response shows the diff, the status it would have, whether it would need approval, and the preflight checks. Other writes answerdry_run=truewith 400 instead of running, and writes refuse unknown query parameters, so a typo never executes the call. - Idempotency: creates accept an
Idempotency-Keyheader (8 to 64 characters, remembered for 24 hours). Repeating the request returns the first response. Operations that return a secret (workspace creation, API keys, invitations, recovery, preview and editor links) reject the header. - Versions: objects with a
versionacceptexpected_version. When the object changed since you read it, the write answers409 version_conflictwith the current version. - Approvals: launch-type actions (start, resume, schedule, traffic changes, stop, ship, ending a rollout, and project settings that change live behavior) need the
experiments:publishpermission. Without it they answer202with anActionResultwhosestatusisawaiting_approval, anapprovalobject and anapprove_url. PollGET /approvals/{approval_id}. - Unknown fields in a request body are rejected (
400 invalid_request), so typos never pass silently.
Endpoints#
The OpenAPI file documents every request and response. An overview:
| Area | Endpoints |
|---|---|
| Account | GET /me, DELETE /me, GET /workspaces, POST /workspaces, GET, PATCH, DELETE /workspaces/{id}, GET /workspaces/{id}/usage, GET /workspaces/{id}/activity |
| Without an account | POST /anonymous-workspaces, POST /anonymous-workspaces/redeem, POST /workspaces/{id}/claim, POST /workspaces/{id}/edit-token |
| Team (people only) | GET, PATCH, DELETE /workspaces/{id}/members/..., GET, POST, DELETE /workspaces/{id}/invitations/..., GET /invitations/{token}, POST /invitations/{token}/accept |
| Access | GET, POST /workspaces/{id}/api-keys, DELETE /api-keys/{id}, GET /workspaces/{id}/oauth-grants, DELETE /oauth-grants/{id} |
| Approvals | GET /workspaces/{id}/approvals, GET /approvals/{id}, POST /approvals/{id}/approve, POST /approvals/{id}/reject |
| Projects | GET, POST /workspaces/{id}/projects, GET, PATCH, DELETE /projects/{id}, GET /projects/{id}/snippet, GET /projects/{id}/install-status, POST /projects/{id}/install-check, POST /projects/{id}/inspect, GET /projects/{id}/events, POST /projects/{id}/estimate, GET /projects/{id}/activity, POST /projects/{id}/visitors/erase |
| Goals | GET, POST /projects/{id}/goals, GET, PATCH, DELETE /goals/{id} |
| Audiences | GET, POST /projects/{id}/audiences, GET, PATCH, DELETE /audiences/{id} |
| Experiments | GET, POST /projects/{id}/experiments, GET, PATCH, DELETE /experiments/{id}, GET /experiments/{id}/preflight, POST /experiments/{id}/duplicate, GET /experiments/{id}/activity |
| Lifecycle | POST /experiments/{id}/start, /pause, /resume, /traffic, /stop, /ship, /archive, /unarchive |
| Variations | POST /experiments/{id}/variations, PATCH, DELETE /variations/{id}, PUT /variations/{id}/changes |
| Results | GET /experiments/{id}/results, GET /experiments/{id}/results.csv, GET /experiments/{id}/estimate |
| Preview and editor | POST /experiments/{id}/preview-links, POST /experiments/{id}/preview-links/reset, POST /experiments/{id}/editor-links |
| Reference | GET /targeting/conditions (the targeting condition catalog), GET /docs/search?q= |
The results format is explained in Statistics; the experiment format in Experiments, Variations and Targeting.
Errors#
Every error response (status 400 and above) has the same shape:
{
"error": {
"code": "validation_failed",
"message": "The experiment has 2 problems.",
"next_step": "Fix the listed fields and retry.",
"doc_url": "https://tryfast.eu/docs/api#errors",
"details": {},
"field_errors": [
{"field": "variations[1].js", "code": "compile_error", "message": "Unexpected \"}\"", "line": 3, "column": 1},
{"field": "variations[0].weight_bp", "code": "invalid", "message": "Weights must sum to 10000 (got 9000)."}
]
}
}
message says what happened, next_step what to do. details and field_errors appear when they apply. Server errors carry a details.request_id; include it when you write to support@tryfast.eu. The MCP server returns the same object inside its tool errors.
| Code | HTTP | Meaning |
|---|---|---|
invalid_request |
400 | Malformed JSON, wrong content type, an unknown field, a bad query parameter, or an Idempotency-Key on an operation that returns a secret. |
unauthorized |
401 | No credential or an invalid one. The response carries WWW-Authenticate: Bearer realm="tryfast". |
insufficient_scope |
403 | The credential is valid but its permission is too narrow (details.required_scope). |
forbidden |
403 | Your role does not allow this, a Bearer credential tried to approve or reject, or an editor token was used for another project. |
csrf_failed |
403 | A cookie request without same-origin proof. |
claim_required |
403 | The action needs a workspace saved to an account or a signed-in person: custom JavaScript going live, members, invitations, more API keys, or approving in a workspace created by a script. |
plan_limit_reached |
403 | A plan limit was reached. details holds limit (projects, running_experiments, monthly_tested_users or free_workspaces), current, max, plan and upgrade_url. |
not_found |
404 | The object does not exist or is not visible to you. Objects of other workspaces always answer this. |
method_not_allowed |
405 | The path exists but not with this method. |
conflict |
409 | A unique key is taken or a precondition failed; details.reason is one of duplicate_key, last_owner, goal_in_use, goal_key_has_conversions, currency_locked, sole_owner_with_members, audience_in_use. |
version_conflict |
409 | The object changed since you read it (details.current_version). Read it again and retry. |
invalid_transition |
409 | The lifecycle action is not allowed from the current status (details.status, details.allowed). |
experiment_running_immutable |
409 | The change would invalidate the statistics of a running experiment. Stop it and duplicate it instead. |
approval_stale |
409 | The experiment or project changed after the approval was requested. The request was replaced; ask again. |
idempotency_key_reused |
409 | The same Idempotency-Key was sent with a different request. |
payload_too_large |
413 | The request body is over the limit. |
validation_failed |
422 | The request is well formed but not valid. field_errors lists every problem. |
preflight_failed |
422 | The experiment cannot start; details.checks lists the preflight checks. |
bundle_too_large |
422 | Publishing would make the project's script larger than 200 KB uncompressed. |
workspace_frozen |
423 | The unsaved workspace is paused after 30 days without use. Reading works; save it to an account to make changes. |
rate_limited |
429 | Too many requests. Wait for the number of seconds in Retry-After. |
fetch_failed |
502 | A page check could not fetch the URL (details.reason: blocked_address, timeout, too_large, status_<code>, redirect_off_domain or tls). |
idp_unavailable |
503 | The sign-in service is not reachable. Sign-in only. |
busy |
503 | The service is busy. Retry after the seconds in Retry-After. |
internal |
500 | Something failed on our side. Include details.request_id when you contact support. |
Field error codes: required, invalid, too_long, too_many, out_of_range, compile_error (with line and column), invalid_selector, invalid_regex, not_on_project_domain, unknown_goal, unknown_audience, unsafe_html, unsafe_url, missing_from_list, unknown_field. The field is a JSON path into your request, such as variations[1].changes[0].selector or targeting.audience.items[1].value.