Test Plans API Cookbook
Copy-paste recipes for every Test Plans endpoint. Each section shows a cURL call plus the equivalent in the Go, Python and Java SDKs.
About URLs in examples: all examples use
http://localhost:5770. Replace it with your Mockarty address (e.g.https://mockarty.company.com) if your instance lives elsewhere. See Tips & Useful Features.
Related pages: Test Plans · Test Plans in CI/CD · API Reference
Authentication
All endpoints require an API token passed via X-API-Key. Tokens are issued in Settings → API Tokens (or POST /api/v1/auth/tokens) with a role scoped to a namespace.
export MOCKARTY_URL=http://localhost:5770
export MOCKARTY_API_TOKEN=mk_7_...
export NS=default
SDK clients
Go
import (
"context"
"github.com/mockarty/mockarty-go/mockarty"
)
client, err := mockarty.NewClient(mockarty.Config{
BaseURL: "http://localhost:5770",
Token: os.Getenv("MOCKARTY_API_TOKEN"),
})
ctx := context.Background()
Python
from mockarty import MockartyClient, TestPlan, TestPlanItem
client = MockartyClient(
base_url="http://localhost:5770",
token=os.environ["MOCKARTY_API_TOKEN"],
)
Java
import com.mockarty.MockartyClient;
import com.mockarty.model.*;
MockartyClient client = MockartyClient.builder()
.baseUrl("http://localhost:5770")
.token(System.getenv("MOCKARTY_API_TOKEN"))
.build();
Endpoint map
| Verb | Path | What it does |
|---|---|---|
| POST | /api/v1/test-plans |
Create a plan. |
| GET | /api/v1/test-plans |
List plans (namespace-scoped). |
| GET | /api/v1/test-plans/:id |
Get a plan by UUID or numeric ID. |
| PUT | /api/v1/test-plans/:id |
Full replace. |
| PATCH | /api/v1/namespaces/:ns/test-plans/:idOrNumericID |
Partial update with If-Match. |
| DELETE | /api/v1/test-plans/:id |
Soft delete (→ Recycle Bin). |
| POST | /api/v1/test-plans/:id/run |
Trigger a run. |
| GET | /api/v1/test-plans/runs/:runID |
Run status. |
| POST | /api/v1/test-plans/runs/:runID/cancel |
Cancel a running run. |
| GET | /api/v1/test-plans/:planID/runs |
List runs for a plan. |
| GET | /api/v1/namespaces/:ns/test-plans/:planRef/runs/:runID/stream |
SSE progress stream. |
| GET | /api/v1/namespaces/:ns/test-plans/:idOrNumericID/runs/:runID/report |
Allure JSON summary. |
| GET | /api/v1/namespaces/:ns/test-plans/:idOrNumericID/runs/:runID/report.zip |
Allure ZIP archive. |
| GET | /api/v1/namespaces/:ns/test-plans/:idOrNumericID/runs/:runID/report.junit.xml |
JUnit XML (Jenkins / GitLab / GitHub Actions). |
| GET | /api/v1/namespaces/:ns/test-plans/:idOrNumericID/runs/:runID/report.md |
Markdown summary (Slack / email / wiki). |
| GET | /api/v1/namespaces/:ns/test-plans/:idOrNumericID/runs/:runID/report.html |
Standalone HTML (open in browser, save-as-PDF). |
| GET | /api/v1/namespaces/:ns/test-plans/:idOrNumericID/runs/:runID/report.unified.json |
Native Mockarty unified JSON. |
| POST | /api/v1/namespaces/:ns/test-runs/ad-hoc |
Ad-hoc run (hidden plan). |
| GET / POST / PATCH / DELETE | /api/v1/test-plans/:id/schedules(/:schedId) |
Schedules CRUD. |
| GET / POST / PATCH / DELETE | /api/v1/test-plans/:id/webhooks(/:whId) |
Webhooks CRUD. |
| POST | /api/v1/test-plans/:id/webhooks/:whId/test |
Synthetic webhook probe. |
Create a plan
cURL
curl -X POST "$MOCKARTY_URL/api/v1/test-plans" \
-H "X-API-Key: $MOCKARTY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"namespace": "default",
"name": "Nightly regression",
"description": "Functional + fuzz + chaos against staging",
"items": [
{"order": 1, "type": "functional", "refId": "11111111-1111-1111-1111-111111111111"},
{"order": 2, "type": "fuzz", "refId": "22222222-2222-2222-2222-222222222222"},
{"order": 3, "type": "chaos", "refId": "33333333-3333-3333-3333-333333333333"}
]
}'
Response:
{
"id": "5c0f13e4-...",
"numericId": 42,
"namespace": "default",
"name": "Nightly regression",
"items": [],
"updatedAt": "2026-04-19T10:00:00Z"
}
Go
plan, err := client.TestPlans().Create(ctx, mockarty.TestPlan{
Namespace: "default",
Name: "Nightly regression",
Description: "Functional + fuzz + chaos against staging",
Items: []mockarty.TestPlanItem{
{Order: 1, Type: mockarty.PlanItemTypeFunctional, ResourceID: "11111111-..."},
{Order: 2, Type: mockarty.PlanItemTypeFuzz, ResourceID: "22222222-..."},
{Order: 3, Type: mockarty.PlanItemTypeChaos, ResourceID: "33333333-..."},
},
})
Python
plan = client.test_plans.create(TestPlan(
namespace="default",
name="Nightly regression",
description="Functional + fuzz + chaos against staging",
items=[
TestPlanItem(order=1, type="functional", ref_id="11111111-..."),
TestPlanItem(order=2, type="fuzz", ref_id="22222222-..."),
TestPlanItem(order=3, type="chaos", ref_id="33333333-..."),
],
))
Java
TestPlan plan = client.testPlans().create(new TestPlan()
.setNamespace("default")
.setName("Nightly regression")
.setItems(List.of(
new TestPlanItem().setOrder(1).setType("functional").setResourceId("11111111-..."),
new TestPlanItem().setOrder(2).setType("fuzz").setResourceId("22222222-..."),
new TestPlanItem().setOrder(3).setType("chaos").setResourceId("33333333-...")
)));
List, get and delete
cURL
# List
curl -H "X-API-Key: $MOCKARTY_API_TOKEN" \
"$MOCKARTY_URL/api/v1/test-plans?namespace=$NS&limit=50"
# Get by numeric ID (or UUID)
curl -H "X-API-Key: $MOCKARTY_API_TOKEN" \
"$MOCKARTY_URL/api/v1/test-plans/42"
# Soft delete (sends to Recycle Bin)
curl -X DELETE \
-H "X-API-Key: $MOCKARTY_API_TOKEN" \
"$MOCKARTY_URL/api/v1/test-plans/42"
Go
plans, err := client.TestPlans().List(ctx, mockarty.ListPlansOptions{
Namespace: "default", Limit: 50,
})
plan, err := client.TestPlans().Get(ctx, "42")
err = client.TestPlans().Delete(ctx, plan.ID)
Python
plans = client.test_plans.list(namespace="default", limit=50)
plan = client.test_plans.get("42")
client.test_plans.delete(plan.id)
Java
List<TestPlan> plans = client.testPlans().list(
ListPlansOptions.builder().namespace("default").limit(50).build());
TestPlan p = client.testPlans().get("42");
client.testPlans().delete(p.getId());
Partial update (PATCH with If-Match)
PATCH /api/v1/namespaces/:ns/test-plans/:idOrNumericID requires an If-Match header carrying the plan’s current strong validator (updatedAt in unix-milliseconds, quoted).
cURL
ETAG=$(curl -s -H "X-API-Key: $MOCKARTY_API_TOKEN" \
"$MOCKARTY_URL/api/v1/test-plans/42" \
| python3 -c 'import sys,json,datetime;d=json.load(sys.stdin); t=datetime.datetime.fromisoformat(d["updatedAt"].replace("Z","+00:00")); print(int(t.timestamp()*1000))')
curl -X PATCH "$MOCKARTY_URL/api/v1/namespaces/$NS/test-plans/42" \
-H "X-API-Key: $MOCKARTY_API_TOKEN" \
-H "Content-Type: application/json" \
-H "If-Match: \"$ETAG\"" \
-d '{"description": "Updated nightly suite"}'
A stale If-Match returns 412 Precondition Failed.
Go
updated, err := client.TestPlans().Patch(ctx, "42",
mockarty.PatchPlanRequest{Description: mockarty.Ptr("Updated nightly suite")},
mockarty.PatchOptions{Namespace: "default"}, // IfMatch computed automatically
)
Python
updated = client.test_plans.patch(
"42",
{"description": "Updated nightly suite"},
namespace="default", # if_match auto-derived from the current plan
)
Java
TestPlan updated = client.testPlans().patch("42",
new PatchPlanRequest().setDescription("Updated nightly suite"),
PatchOptions.builder().namespace("default").build());
Trigger a run and wait
cURL
# Trigger
RUN=$(curl -s -X POST "$MOCKARTY_URL/api/v1/test-plans/42/run" \
-H "X-API-Key: $MOCKARTY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"mode": "parallel"}')
RUN_ID=$(echo "$RUN" | jq -r .runId)
# Poll until terminal
while :; do
STATUS=$(curl -s -H "X-API-Key: $MOCKARTY_API_TOKEN" \
"$MOCKARTY_URL/api/v1/test-plans/runs/$RUN_ID/status" \
| jq -r .status)
echo "status=$STATUS"
case "$STATUS" in
completed|failed|cancelled) break ;;
esac
sleep 5
done
Go
run, _ := client.TestPlans().Run(ctx, "42", mockarty.RunOptions{Mode: "parallel"})
final, err := client.TestPlans().WaitForRun(ctx, run.ID, 3*time.Second)
// err == nil → completed
// errors.Is(err, mockarty.ErrRunFailed) → failed
// errors.Is(err, mockarty.ErrRunCancelled) → cancelled
Python
run = client.test_plans.run("42", mode="parallel")
final = client.test_plans.wait_for_run(run.id, poll_interval=3)
Java
TestPlanRun run = client.testPlans().run("42",
RunOptions.builder().mode("parallel").build());
TestPlanRun final_ = client.testPlans().waitForRun(run.getId(), Duration.ofSeconds(3));
Run a subset of items
curl -X POST "$MOCKARTY_URL/api/v1/test-plans/42/run" \
-H "X-API-Key: $MOCKARTY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"items": [1, 3], "mode": "sequential"}'
Cancel a running run
curl -X POST "$MOCKARTY_URL/api/v1/test-plans/runs/$RUN_ID/cancel" \
-H "X-API-Key: $MOCKARTY_API_TOKEN"
Stream progress (SSE)
cURL
curl -N -H "X-API-Key: $MOCKARTY_API_TOKEN" \
"$MOCKARTY_URL/api/v1/namespaces/$NS/test-plans/42/runs/$RUN_ID/stream"
Event sequence: run.started → N × (item.started / item.finished) → run.completed, with periodic heartbeat.
Go
events, _ := client.TestPlans().StreamRun(ctx, runID)
for ev := range events {
log.Printf("%s %s %s", ev.Type, ev.ItemID, ev.Status)
}
Python
for ev in client.test_plans.stream_run(run.id):
print(ev.type, ev.item_id, ev.status)
Java
client.testPlans().streamRun(run.getId(), ev ->
System.out.println(ev.getType() + " " + ev.getItemId() + " " + ev.getStatus()));
Download run reports
Five formats are served side-by-side — pick the one your tooling needs, no server-side conversion required.
cURL
# Allure JSON summary
curl -s -H "X-API-Key: $MOCKARTY_API_TOKEN" \
"$MOCKARTY_URL/api/v1/namespaces/$NS/test-plans/42/runs/$RUN_ID/report" \
-o report.json
# Allure ZIP (feed into `allure generate`)
curl -s -H "X-API-Key: $MOCKARTY_API_TOKEN" \
"$MOCKARTY_URL/api/v1/namespaces/$NS/test-plans/42/runs/$RUN_ID/report.zip" \
-o allure.zip
# JUnit XML (CI publishers)
curl -s -H "X-API-Key: $MOCKARTY_API_TOKEN" \
"$MOCKARTY_URL/api/v1/namespaces/$NS/test-plans/42/runs/$RUN_ID/report.junit.xml" \
-o report.junit.xml
# Markdown summary
curl -s -H "X-API-Key: $MOCKARTY_API_TOKEN" \
"$MOCKARTY_URL/api/v1/namespaces/$NS/test-plans/42/runs/$RUN_ID/report.md" \
-o report.md
# Standalone HTML (self-contained; print to PDF in any browser)
curl -s -H "X-API-Key: $MOCKARTY_API_TOKEN" \
"$MOCKARTY_URL/api/v1/namespaces/$NS/test-plans/42/runs/$RUN_ID/report.html" \
-o report.html
# Native Mockarty unified JSON
curl -s -H "X-API-Key: $MOCKARTY_API_TOKEN" \
"$MOCKARTY_URL/api/v1/namespaces/$NS/test-plans/42/runs/$RUN_ID/report.unified.json" \
-o report.unified.json
The Allure endpoints (/report, /report.zip) honour If-None-Match, so CI pipelines polling for readiness pay just the HTTP round-trip. The JUnit/Markdown/HTML/Unified formats are regenerated on every request (they are cheap) and always reflect the freshest per-item state.
Go
rep, _ := client.TestPlans().GetRunReport(ctx, "default", "42", runID)
zipRC, _ := client.TestPlans().GetRunReportZIP(ctx, "default", "42", runID)
defer zipRC.Close()
io.Copy(outFile, zipRC)
junit, _ := client.TestPlans().GetRunReportJUnit(ctx, "default", "42", runID)
md, _ := client.TestPlans().GetRunReportMarkdown(ctx, "default", "42", runID)
html, _ := client.TestPlans().GetRunReportHTML(ctx, "default", "42", runID)
unified, _ := client.TestPlans().GetRunReportUnified(ctx, "default", "42", runID)
Python
summary = client.test_plans.get_run_report("42", run.id, namespace="default")
junit = client.test_plans.get_run_report_junit("42", run.id, namespace="default")
md = client.test_plans.get_run_report_markdown("42", run.id, namespace="default")
html = client.test_plans.get_run_report_html("42", run.id, namespace="default")
unified = client.test_plans.get_run_report_unified("42", run.id, namespace="default")
with open("allure.zip", "wb") as f:
client.test_plans.get_run_report_zip("42", run.id, f, namespace="default")
Java
AllureReport summary = client.testPlans().getRunReport("default", "42", runID);
byte[] junit = client.testPlans().getRunReportJUnit("default", "42", runID);
byte[] md = client.testPlans().getRunReportMarkdown("default", "42", runID);
byte[] html = client.testPlans().getRunReportHTML("default", "42", runID);
UnifiedReport unified = client.testPlans().getRunReportUnified("default", "42", runID);
try (OutputStream out = Files.newOutputStream(Path.of("allure.zip"))) {
client.testPlans().getRunReportZip("default", "42", runID, out);
}
Ad-hoc run
POST /api/v1/namespaces/:ns/test-runs/ad-hoc creates a hidden plan and dispatches a run in one call — perfect for dynamically assembled CI pipelines.
cURL
curl -X POST "$MOCKARTY_URL/api/v1/namespaces/$NS/test-runs/ad-hoc" \
-H "X-API-Key: $MOCKARTY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "pr-1234 smoke",
"items": [
{"order": 1, "type": "functional", "ref_id": "11111111-..."},
{"order": 2, "type": "contract", "ref_id": "44444444-..."}
]
}'
Response:
{
"run_id": "8a1f62d0-...",
"plan_id": "9c0e...",
"status": "running",
"_links": {
"self": "/api/v1/test-plans/runs/8a1f62d0-...",
"status": "/api/v1/test-plans/runs/8a1f62d0-.../status",
"report": "/api/v1/namespaces/default/test-plans/9c0e.../runs/8a1f62d0-.../report"
}
}
The ad-hoc endpoint responds with 503 on nodes where the orchestrator is not wired (for example stripped-down desktop builds).
Optional "tags": ["release-42", "smoke"] labels the run as it is created. The response includes the normalized tags, and the run read endpoint returns the same values. Invalid tags return 400 before the plan is created; see Ad-hoc master runs for tag limits.
Add "idempotency_key":"job-4211" when a CI job may retry this POST. The same body and key return the original run with "replayed":true; a changed body with that key returns 409. Use a new key for a new job. The server stores only a digest of the key.
For an existing run, add tags atomically with POST /api/v1/namespaces/<namespace>/test-plan-runs/<runId>/tags/merge and {"tags":["smoke"]}. The response gives the full tags set and a changed flag. Repeating the request is safe; the release tag and other workers’ tags remain intact. The original /tags endpoint keeps its replace behavior.
Go
resp, err := client.TestPlans().CreateAdHocRun(ctx, mockarty.CreateAdHocRunRequest{
Namespace: "default",
Name: "pr-1234 smoke",
Items: []mockarty.TestPlanItem{
{Order: 1, Type: "functional", ResourceID: "11111111-..."},
{Order: 2, Type: "contract", ResourceID: "44444444-..."},
},
})
Python
resp = client.test_plans.create_ad_hoc_run(
namespace="default",
name="pr-1234 smoke",
items=[
TestPlanItem(order=1, type="functional", ref_id="11111111-..."),
TestPlanItem(order=2, type="contract", ref_id="44444444-..."),
],
)
Java
AdHocRunResponse resp = client.testPlans().createAdHocRun(
CreateAdHocRunRequest.builder()
.namespace("default")
.name("pr-1234 smoke")
.items(List.of(
new TestPlanItem().setOrder(1).setType("functional").setResourceId("11111111-..."),
new TestPlanItem().setOrder(2).setType("contract").setResourceId("44444444-...")
))
.build());
Schedules
List schedules
curl -H "X-API-Key: $MOCKARTY_API_TOKEN" \
"$MOCKARTY_URL/api/v1/test-plans/42/schedules"
Add a cron schedule
cURL
curl -X POST "$MOCKARTY_URL/api/v1/test-plans/42/schedules" \
-H "X-API-Key: $MOCKARTY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "nightly",
"kind": "cron",
"timezone": "Europe/Moscow",
"payload": {"expr": "0 2 * * *"}
}'
Go
sch, _ := client.TestPlans().AddSchedule(ctx, planID, mockarty.Schedule{
Name: "nightly", Kind: "cron", Timezone: "Europe/Moscow",
Payload: mockarty.SchedulePayload{"expr": "0 2 * * *"},
})
Python
sch = client.test_plans.add_schedule(plan.id, Schedule(
name="nightly", kind="cron", timezone="Europe/Moscow",
payload={"expr": "0 2 * * *"},
))
Java
Schedule sch = client.testPlans().addSchedule(plan.getId(), new Schedule()
.setName("nightly").setKind("cron").setTimezone("Europe/Moscow")
.setPayload(Map.of("expr", "0 2 * * *")));
One-shot and interval schedules
# Once (fires on 2026-05-01 00:00 UTC, then auto-disables)
curl -X POST "$MOCKARTY_URL/api/v1/test-plans/42/schedules" \
-H "X-API-Key: $MOCKARTY_API_TOKEN" -H "Content-Type: application/json" \
-d '{"name":"launch","kind":"once","payload":{"fire_at":"2026-05-01T00:00:00Z"}}'
# Interval (every 15 minutes)
curl -X POST "$MOCKARTY_URL/api/v1/test-plans/42/schedules" \
-H "X-API-Key: $MOCKARTY_API_TOKEN" -H "Content-Type: application/json" \
-d '{"name":"smoke","kind":"interval","payload":{"every_seconds":900}}'
Update / delete
# Disable temporarily
curl -X PATCH "$MOCKARTY_URL/api/v1/test-plans/42/schedules/$SCHED_ID" \
-H "X-API-Key: $MOCKARTY_API_TOKEN" -H "Content-Type: application/json" \
-d '{"enabled": false}'
curl -X DELETE "$MOCKARTY_URL/api/v1/test-plans/42/schedules/$SCHED_ID" \
-H "X-API-Key: $MOCKARTY_API_TOKEN"
Webhooks
Create a webhook
cURL
curl -X POST "$MOCKARTY_URL/api/v1/test-plans/42/webhooks" \
-H "X-API-Key: $MOCKARTY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "ci-slack",
"url": "https://hooks.example.com/mockarty",
"secret": "keep-me-safe",
"events": ["run_finished", "item_failed"],
"retryCount": 3,
"backoffSeconds": 5
}'
The secret is write-only — the server returns an empty string on read. Rotate with PATCH { "secret": "<new>" }.
Outbound URL validation (SSRF protection): HTTPS required, embedded credentials rejected, plus any literal or DNS-resolved address in loopback / RFC 1918 / link-local / multicast ranges, and hostnames matching localhost, *.internal, *.cluster.local.
Go
wh, _ := client.TestPlans().AddWebhook(ctx, planID, mockarty.Webhook{
Name: "ci-slack",
URL: "https://hooks.example.com/mockarty",
Secret: "keep-me-safe",
Events: []string{"run_finished", "item_failed"},
RetryCount: 3,
BackoffSeconds: 5,
Enabled: true,
})
Python
wh = client.test_plans.add_webhook(plan.id, Webhook(
name="ci-slack",
url="https://hooks.example.com/mockarty",
secret="keep-me-safe",
events=["run_finished", "item_failed"],
retry_count=3, backoff_seconds=5,
))
Java
Webhook wh = client.testPlans().addWebhook(plan.getId(), new Webhook()
.setName("ci-slack")
.setUrl("https://hooks.example.com/mockarty")
.setSecret("keep-me-safe")
.setEvents(List.of("run_finished", "item_failed"))
.setRetryCount(3).setBackoffSeconds(5));
Probe a webhook (dry-run)
curl -X POST "$MOCKARTY_URL/api/v1/test-plans/42/webhooks/$WH_ID/test" \
-H "X-API-Key: $MOCKARTY_API_TOKEN"
This enqueues a synthetic run_started payload so receivers can verify signature headers and HTTPS connectivity end-to-end.
Verifying the signature on the receiver (Go example)
sig := r.Header.Get("X-Mockarty-Signature") // "sha256=<hex>" or bare hex
ts := r.Header.Get("X-Mockarty-Timestamp") // RFC3339Nano
body, _ := io.ReadAll(r.Body)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(sig)) {
http.Error(w, "bad signature", http.StatusUnauthorized)
return
}
t, err := time.Parse(time.RFC3339Nano, ts)
if err != nil || time.Since(t) > 5*time.Minute {
http.Error(w, "stale request", http.StatusUnauthorized)
return
}
Receivers should reject anything outside a ±5 minute skew window to defeat replay.
List, update and delete webhooks
# List
curl -H "X-API-Key: $MOCKARTY_API_TOKEN" \
"$MOCKARTY_URL/api/v1/test-plans/42/webhooks"
# Rotate secret + disable
curl -X PATCH "$MOCKARTY_URL/api/v1/test-plans/42/webhooks/$WH_ID" \
-H "X-API-Key: $MOCKARTY_API_TOKEN" -H "Content-Type: application/json" \
-d '{"secret":"rotated-secret","enabled":false}'
# Delete
curl -X DELETE "$MOCKARTY_URL/api/v1/test-plans/42/webhooks/$WH_ID" \
-H "X-API-Key: $MOCKARTY_API_TOKEN"
Error reference
| HTTP | Meaning |
|---|---|
400 |
Malformed JSON, invalid enum, non-unique order, missing required field. Body contains {"error":"..."}. |
401 |
Missing or invalid X-API-Key. |
403 |
Valid token but the role lacks permission for the operation. |
404 |
Resource not found or belongs to a different namespace (no existence leak). |
409 |
Duplicate name / numeric-ID collision. |
412 |
If-Match validator mismatch on PATCH — re-fetch and retry. |
422 |
Validation failure (e.g. every_seconds < 10, insecure webhook URL). |
503 |
Orchestrator not wired on this admin node (ad-hoc runs, SSE). |
Where to go next
- Test Plans concepts — long-form explanation of every primitive.
- Test Plans in CI/CD — GitHub Actions / GitLab CI / Jenkins recipes.
- API Reference — the full swagger-generated endpoint catalogue.