Docs Test Plans API Cookbook

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