← Provider Desk / API

Provider Desk API

Everything the web app does over the model is one HTTP call. Base URL https://api.skillsafe.ai/v1/app-api. Every request carries Authorization: Bearer <token> and every response uses the same envelope.

The response envelope

Success is {"ok": true, "data": {...}}. Failure is {"ok": false, "error": {"code": "...", "message": "...", "details": {...}}}. Always branch on ok, never on the HTTP status alone.

CodeHTTPWhat it means here
UNAUTHORIZED401Missing, malformed or expired token. Mint a new one on the token page.
PAYMENT_REQUIRED402Balance below min_credits for this lane. Call /estimate first and top up.
VALIDATION_ERROR400The input object is the wrong shape — usually a missing source or an unknown task.
RATE_LIMITED429Back off and retry. Do not tight-loop.
NOT_FOUND404Wrong job id, or a job that belongs to another subject.
INTERNAL500Retry once with the same idempotency key.

The task field comes first

Provider Desk is one app with four lanes over one work object — the Go source of a provider resource. Every request must set task; it selects the lane, the prompt section, the output body shape and the price. If task is missing the model picks the closest lane and reports lane_inferred: true — usable, but never what you want from a script.

taskWhat that lane returns
schemaAudit the resource: an attribute-by-attribute verdict, a lifecycle table, the breaking changes a fix would introduce, an ordered plan, and the corrected .go file.
testsDesign the acceptance suite: the test plan, the attribute assertions, a CheckDestroy, the import step, HCL fixtures and the complete _test.go.
docsWrite the registry page: front matter, prose, the Required / Optional / Read-Only schema table, worked examples and the import section, as Markdown.
releaseClear the release: the semver decision, the changelog entry, a pre-publish checklist, release-build issues, the registry manifest and the signing status.

Input fields

Taken from readForm() in app.js — this is exactly what the web app sends.

FieldTypeRequiredNotes
taskstringyesOne of schema, tests, docs, release.
sourcestringyesThe pasted Go. Separate multiple files with a // file: name.go line. Clipped from the middle at 52,000 characters, both ends kept.
notesstringnoFree text about the repository and the API. The release lane leans on it heavily — module path, current version, build tooling — because none of that is in the Go.
frameworkstringnoplugin-framework, sdkv2, mixed or unknown.
prescanobjectnoWhat the browser reader found. Omit it and the model simply has fewer facts — but then coverage_check comes back empty, because there are no flags to reconcile.
retry_notestringnoSent by the web app only on the one reformat retry, when the first reply did not parse. It is an instruction to the model to answer again with nothing but the single JSON object the contract requires, naming the lane it was asked for. A first attempt never carries it, and it changes no part of the output contract.
clip_notestringnoSend it when you clipped source yourself, so the model writes around the gap rather than inventing across it.

Step 1 — get a token

Open the token page, reveal your token and copy the shell export. It is the same token the web app holds in this browser, so a script and the page share one identity, one balance and one history. Keep it out of source control — export it as PROVIDER_DESK_TOKEN and read it from there, the way every sample below does.

Step 2 — confirm the session and the balance

GET /me is free. It tells you whether the token is a personal or a guest subject and how many credits it can spend.

curl -sS "https://api.skillsafe.ai/v1/app-api/me" \
  -H "Authorization: Bearer $PROVIDER_DESK_TOKEN"

Step 3 — estimate before you spend

POST /estimate is free and charges nothing. It returns model, model_alias, markup_bps, hold_credits and min_credits. The hold differs per lane, so estimate the lane you are about to run — never reuse another lane’s number.

curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/estimate" \
  -H "Authorization: Bearer $PROVIDER_DESK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "task": "schema",
  "source": "package provider\n\ntype WidgetResourceModel struct {\n\tID   types.String `tfsdk:\"id\"`\n\tName types.String `tfsdk:\"name\"`\n}\n\nfunc (r *WidgetResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {\n\tresp.Schema = schema.Schema{\n\t\tAttributes: map[string]schema.Attribute{\n\t\t\t\"id\": schema.StringAttribute{Computed: true},\n\t\t\t\"name\": schema.StringAttribute{Required: true, Computed: true},\n\t\t},\n\t}\n}",
  "notes": "",
  "framework": "plugin-framework"
}'

Step 4 — run a lane

POST /run is metered and returns {"job_id": "..."}. Always send Idempotency-Key, and derive it from the lane plus a hash of the input plus an attempt counter — two lanes over the same file are two distinct runs and must not collide on one key. Below is the schema lane in eight languages; a worked request for the other three lanes follows it. The only field that has to change is task — and, on the release lane, notes, which that lane cannot do without.

curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/run" \
  -H "Authorization: Bearer $PROVIDER_DESK_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: provider-desk:schema:1f9k2m:a1" \
  -d '{
  "task": "schema",
  "source": "package provider\n\ntype WidgetResourceModel struct {\n\tID   types.String `tfsdk:\"id\"`\n\tName types.String `tfsdk:\"name\"`\n}\n\nfunc (r *WidgetResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {\n\tresp.Schema = schema.Schema{\n\t\tAttributes: map[string]schema.Attribute{\n\t\t\t\"id\": schema.StringAttribute{Computed: true},\n\t\t\t\"name\": schema.StringAttribute{Required: true, Computed: true},\n\t\t},\n\t}\n}",
  "notes": "",
  "framework": "plugin-framework",
  "prescan": {
    "summary": {
      "framework": "plugin-framework",
      "kind": "resource",
      "type_name": "widget",
      "attributes": 2
    },
    "flags": [
      {
        "id": "PD01",
        "severity": "critical",
        "label": "Attributes are both Required and Computed",
        "line": 12,
        "detail": "name",
        "why": "The framework rejects this combination at startup."
      }
    ],
    "checks": [
      {
        "id": "C10",
        "label": "Attribute semantics legal",
        "status": "fail",
        "detail": "1 attribute declares an illegal combination"
      }
    ],
    "resources": [
      {
        "id": "R2",
        "label": "Terraform type suffix _widget"
      }
    ]
  }
}'

The same call on the other three lanes

Every language sample above sends the same JSON body, so only the body is shown here. The release lane is the one that genuinely needs notes: the module path, the released version and the existing build tooling are simply not in the Go, and without them the lane can only answer unknown.

task: "release" — with the repository facts the Go cannot carry

curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/run" \
  -H "Authorization: Bearer $PROVIDER_DESK_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: provider-desk:release:1f9k2m:a1" \
  -d '{
  "task": "release",
  "source": "package provider\n\nfunc (r *WidgetResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {\n\tresp.TypeName = req.ProviderTypeName + \"_widget\"\n}\n\nfunc (r *WidgetResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {\n\tresp.Schema = schema.Schema{\n\t\tAttributes: map[string]schema.Attribute{\n\t\t\t\"id\": schema.StringAttribute{Computed: true},\n\t\t\t\"name\": schema.StringAttribute{Required: true},\n\t\t\t\"region\": schema.StringAttribute{Required: true},\n\t\t},\n\t}\n}",
  "notes": "Module path github.com/examplecloud/terraform-provider-examplecloud. Currently released as v1.3.2. This change makes region Required; it was Optional in v1.3.2. We build with GoReleaser 2 from .goreleaser.yml and sign SHA256SUMS with the GPG key already registered with the registry. terraform-registry-manifest.json exists and declares protocol 6. There is no docs/resources/widget.md yet.",
  "framework": "plugin-framework"
}'

That notes block is what turns the checklist from eight unknown rows into a judgement: the version decision becomes major and names region as the attribute that drove it, and the missing documentation page is a fail rather than a shrug.

task: "docs" — the registry page for the same resource

curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/run" \
  -H "Authorization: Bearer $PROVIDER_DESK_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: provider-desk:docs:1f9k2m:a1" \
  -d '{
  "task": "docs",
  "source": "package provider\n\nfunc (r *WidgetResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {\n\tresp.TypeName = req.ProviderTypeName + \"_widget\"\n}\n\nfunc (r *WidgetResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {\n\tresp.Schema = schema.Schema{\n\t\tAttributes: map[string]schema.Attribute{\n\t\t\t\"id\": schema.StringAttribute{Computed: true},\n\t\t\t\"name\": schema.StringAttribute{Required: true, MarkdownDescription: \"The name of the widget.\"},\n\t\t\t\"region\": schema.StringAttribute{Required: true},\n\t\t},\n\t}\n}",
  "notes": "Provider name examplecloud. Import is by widget id.",
  "framework": "plugin-framework"
}'

The docs lane answers with body.schema_table split into Required / Optional / Read-Only and artifact.kind: "markdown" holding the whole page. region has no MarkdownDescription in that paste, so it also comes back as a finding: the generator would otherwise publish an empty description cell.

task: "tests" — the acceptance suite for the same resource

curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/run" \
  -H "Authorization: Bearer $PROVIDER_DESK_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: provider-desk:tests:1f9k2m:a1" \
  -d '{
  "task": "tests",
  "source": "package provider\n\nfunc (r *WidgetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {\n\tvar m WidgetResourceModel\n\tresp.Diagnostics.Append(req.State.Get(ctx, &m)...)\n\tw, err := r.client.GetWidget(ctx, m.ID.ValueString())\n\tif err != nil {\n\t\tresp.Diagnostics.AddError(\"read failed\", err.Error())\n\t\treturn\n\t}\n\tm.Name = types.StringValue(w.Name)\n\tresp.Diagnostics.Append(resp.State.Set(ctx, &m)...)\n}",
  "notes": "CheckDestroy should use r.client.GetWidget and treat a not-found error as proof of destruction.",
  "framework": "plugin-framework"
}'

Step 5 — poll the job

GET /jobs/{job_id} until status is terminal. The model reply is the string at data.output.output; parse it as JSON.

curl -sS "https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID" \
  -H "Authorization: Bearer $PROVIDER_DESK_TOKEN"

Step 6 — or stream it

POST /run-stream is the same call over server-sent events. Deltas arrive as they are produced; the app uses them to advance its progress card on the section headings appearing in the stream.

curl -sS -N -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" \
  -H "Authorization: Bearer $PROVIDER_DESK_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: provider-desk:schema:1f9k2m:a1" \
  -d '{
  "task": "schema",
  "source": "package provider\n\ntype WidgetResourceModel struct {\n\tID   types.String `tfsdk:\"id\"`\n\tName types.String `tfsdk:\"name\"`\n}\n\nfunc (r *WidgetResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {\n\tresp.Schema = schema.Schema{\n\t\tAttributes: map[string]schema.Attribute{\n\t\t\t\"id\": schema.StringAttribute{Computed: true},\n\t\t\t\"name\": schema.StringAttribute{Required: true, Computed: true},\n\t\t},\n\t}\n}",
  "notes": "",
  "framework": "plugin-framework",
  "prescan": {
    "summary": {
      "framework": "plugin-framework",
      "kind": "resource",
      "type_name": "widget",
      "attributes": 2
    },
    "flags": [
      {
        "id": "PD01",
        "severity": "critical",
        "label": "Attributes are both Required and Computed",
        "line": 12,
        "detail": "name",
        "why": "The framework rejects this combination at startup."
      }
    ],
    "checks": [
      {
        "id": "C10",
        "label": "Attribute semantics legal",
        "status": "fail",
        "detail": "1 attribute declares an illegal combination"
      }
    ],
    "resources": [
      {
        "id": "R2",
        "label": "Terraform type suffix _widget"
      }
    ]
  }
}'

The output contract

One JSON object. The envelope is identical on all four lanes; only body differs. These are the fields normalize() in app.js actually reads — anything else you send is preserved but never rendered.

FieldTypeMeaning
lanestringThe lane that answered. Always one of the four ids; compare it against the task you sent.
lane_inferredbooleantrue when task was missing or unrecognised and the model picked a lane. A script should treat this as an error.
titlestringA short human title naming the resource.
posturestringregistry-ready, fix-first or not-shippable — about the resource, not about this lane's output.
verdictstringOne paragraph you could paste into a pull request.
frameworkstringThe framework the model concluded you are on.
resource_typestringThe Terraform type suffix without the provider prefix — the widget in examplecloud_widget.
summarystringTwo or three sentences of context.
assumptionsstring[]What the model had to assume because the paste did not say.
open_questionsstring[]What it would ask before merging.
findingsobject[]id, title, severity (critical/high/medium/low), area, file, line, evidence, why, fix, fix_code. Sorted by severity by the client.
coverage_checkobject[]One entry per prescan.flags[].id you sent: flag_id, status (confirmed/set-aside/superseded), finding_id, note. A flag with no entry is shown to the user as ignored.
artifactobjectkind (none/go/markdown/hcl/yaml), filename, content.
next_laneobjectlane and reason — the pipeline is schema → tests → docs → release.
bodyobjectThe only per-lane part. Shapes below.

One worked body per lane

task: "schema"

{
  "attribute_review": [{ "name": "api_token", "verdict": "fail", "issue": "...", "corrected": "..." }],
  "lifecycle": [{ "method": "Read", "status": "fail", "note": "..." }],
  "breaking_changes": ["..."],
  "hardening_plan": [{ "step": "...", "why": "...", "code": "..." }]
}

task: "tests"

{
  "test_plan": [{ "name": "TestAccWidgetResource_basic", "purpose": "...", "steps": ["..."] }],
  "checks": [{ "attribute": "name", "check_func": "resource.TestCheckResourceAttr(...)", "note": "..." }],
  "destroy_check": { "approach": "...", "code": "..." },
  "import_step": { "enabled": true, "id_format": "...", "verify_ignore": ["..."] },
  "fixtures": [{ "name": "step 1 - create", "hcl": "..." }]
}

task: "docs"

{
  "front_matter": { "page_title": "...", "subcategory": "", "description": "..." },
  "sections": [{ "heading": "Example Usage", "markdown": "..." }],
  "schema_table": [{ "name": "name", "kind": "String", "requirement": "Required", "description": "..." }],
  "examples": [{ "filename": "examples/resources/.../resource.tf", "hcl": "..." }],
  "import_doc": { "supported": true, "id_format": "...", "command": "..." }
}

task: "release"

{
  "semver": { "level": "minor", "version": "1.4.0", "reason": "..." },
  "changelog": [{ "type": "FEATURES", "text": "..." }],
  "checklist": [{ "item": "...", "status": "pass", "note": "...", "fix": "..." }],
  "goreleaser": { "issues": ["..."], "snippet": "..." },
  "registry_manifest": { "required": true, "snippet": "..." },
  "signing": { "status": "pass", "note": "..." }
}

Notes that will save you an hour