Skip to content
RFrftools.io

API Documentation

Integrate 241 RF & electronics calculators into your scripts, notebooks, and applications via a simple REST API. One endpoint, any calculator.

Quick Start

1

Get an API Key

Get a free API key on your dashboard (5 calls a month free), or subscribe for more — see pricing for Pro ($9/mo, 100 calls a month) and API ($19/mo, 10,000 calls a month).

2

Call the Endpoint

POST to /api/py/v1/calculate with a slug and inputs.

3

Use the Results

Parse the JSON response with computed values, warnings, and errors.

curlExample request
curl -X POST https://rftools.io/api/py/v1/calculate \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{"slug":"vswr-return-loss","inputs":{"vswr":2.0}}'

Authentication

Send your API key in the X-API-Key header, or as Authorization: Bearer rfc_…; the calculator routes, the jobs routes and the usage endpoint accept both. Every calculator call needs a key, on every tier, and a free one costs nothing — see Get a Key.

X-API-Key: rfc_a1b2c3d4e5f6...
Authorization: Bearer rfc_a1b2c3d4e5f6...

The monthly allowance belongs to the account, not to a key: the calls of all its keys, revoked ones included, count against one figure — 5 a month on Free, 100 on Pro and 10,000 on API. It resets at the start of each calendar month in UTC. Every call is still attributed to the key that made it.

Get a Key

A person gets one from the dashboard. A program has two ways to hand its user one, and both give an ordinary rfc_ key, metered, listed and revocable like any other. A key is shown once, when it is created, and never again: only its hash is stored.

A key link

Open https://rftools.io/dashboard/?newKey=1&client=<clientId>[&label=<text>] in the user's browser. clientId names the program and matches ^[a-z0-9][a-z0-9-]{1,39}$, for example kicad-plugin; a value that does not match is ignored, not refused. A signed-out user signs in or signs up and comes back to the same link. The dashboard opens its create-key dialog with the label prefilled (<clientId> — <date> unless the link gave one), and shows the key once for the user to paste into the program.

The dialog calls POST /api/py/v1/apikeys, which a signed-in session may call directly. The body is optional:

curl -X POST https://rftools.io/api/py/v1/apikeys \
  -H "Authorization: Bearer <signed-in session token>" \
  -H "Content-Type: application/json" \
  -d '{"label":"Bench PC","client":"kicad-plugin"}'
# → {"key":"rfc_…","key_id":"rfc_…","created_at":1758722711,
#    "label":"Bench PC","client":"kicad-plugin","monthly_quota":5}

The device grant, for a program with no browser

The OAuth 2.0 device authorization grant (RFC 8628) gets a key to a program — a plugin, a CLI, a local MCP server — without the user pasting anything. The program asks for a code, shows the user the code and https://rftools.io/device/, and polls. The user signs in there, sees the name the program gave for itself, and approves or denies. On approval the program's next poll returns the key, once. Both endpoints take JSON or a form body.

  • A code is 8 letters from BCDFGHJKLMNPQRSTVWXZ, shown as XXXX-XXXX, and expires 10 minutes after it is issued.
  • Poll no faster than interval (5 s to start with).
  • The key counts toward the account's key limit; an account at its limit is asked to revoke a key first.
  • Show the user the code only. The device_code is the program's secret until it is exchanged.
# 1. the program asks for a code
curl -X POST https://rftools.io/api/py/v1/oauth/device/code \
  -H "Content-Type: application/json" \
  -d '{"client_id":"kicad-plugin"}'
# → {"device_code":"…","user_code":"BCDF-GHJK",
#    "verification_uri":"https://rftools.io/device/",
#    "verification_uri_complete":"https://rftools.io/device/?user_code=BCDF-GHJK",
#    "expires_in":600,"interval":5}

# 2. it shows the user the code and the address, then polls every interval seconds
curl -X POST https://rftools.io/api/py/v1/oauth/device/token \
  -H "Content-Type: application/json" \
  -d '{"grant_type":"urn:ietf:params:oauth:grant-type:device_code",
       "device_code":"…","client_id":"kicad-plugin"}'
# → 400 {"error":"authorization_pending", …}   until the user decides
# → 200 {"access_token":"rfc_…","token_type":"Bearer","key_id":"rfc_…",
#        "label":"kicad-plugin — 2026-09-24","client":"kicad-plugin","monthly_quota":5}

While waiting, the token endpoint answers 400 with an RFC 8628 error:

authorization_pendingThe user has not decided yet. Keep polling at the interval.
slow_downPolled faster than the interval. The interval grows by 5 s; the body carries the new interval.
access_deniedThe user denied the code, or the account reached its key limit before the key was collected. Stop polling.
expired_tokenThe code expired (10 minutes after it was issued) or its key was already collected. Start again.
invalid_grantThe device code was issued to another client_id.
invalid_request / unsupported_grant_typeA parameter is missing or the grant_type is wrong.

Usage

GET /api/py/v1/usage answers a key or a signed-in session with what the account has used this month and what is left, the overage state, and the calls attributed to each of its keys. Reading it is never counted, so a program can show its remaining calls as often as it likes, within the per-minute limit.

curl https://rftools.io/api/py/v1/usage -H "X-API-Key: YOUR_API_KEY"
{
  "tier": "pro",
  "period": "2026-09",
  "resetAt": "2026-10-01T00:00:00+00:00",
  "allowance": 100,
  "used": 37,
  "remaining": 63,
  "overage": {
    "eligible": false,
    "active": false,
    "used": 0,
    "cap": 50000,
    "reportedThisPeriod": null,
    "manageUrl": null
  },
  "keys": [
    {
      "keyId": "rfc_AbCdEfGh",
      "label": "kicad-plugin — 2026-09-02",
      "client": "kicad-plugin",
      "active": true,
      "used": 37,
      "lastUsedAt": "2026-09-23T14:05:11+00:00"
    }
  ],
  "rateLimitPerMinute": 120
}

Every metered response — a calculator call, a batch, a job submitted with a key — carries the same figures in headers a browser can read:

X-Usage-AllowanceThe account's monthly allowance.
X-Usage-UsedCalls counted this month, including this one.
X-Usage-ResetWhen the allowance resets (ISO-8601, UTC): the first instant of next month.
X-Usage-OveragePresent only when pay-as-you-go overage was counted this month: how many calls.

Refusals

Every refusal carries errorKind beside detail, and the fields to act on. Branch on the status and errorKind; the wording of detail is written for people and may change. These are the bodies the API sends, with this month's figures.

401No key sent
{
  "detail": "An API key is required. Get a free key at https://rftools.io/dashboard/?newKey=1.",
  "errorKind": "invalid_request",
  "keyUrl": "https://rftools.io/dashboard/?newKey=1"
}
401A key that is not valid
{
  "detail": "This API key is not valid. It may have been revoked.",
  "errorKind": "invalid_request"
}

Malformed, unknown or revoked. A 401 never means the allowance is spent.

402The allowance is spent
{
  "detail": "This account's 100 API calls for September are used. They reset on 2026-10-01. The API plan includes 10,000 a month.",
  "errorKind": "rate_limited",
  "reason": "allowance",
  "limit": 100,
  "used": 100,
  "upgradeUrl": "https://rftools.io/pricing/",
  "overageUrl": null,
  "resetAt": "2026-10-01T00:00:00+00:00"
}

With Retry-After in seconds until resetAt. The same body on every metered route, calculators included. overageUrl is set where the tier can turn pay-as-you-go on; reason is overage_cap once that cap is spent too. Nothing was counted.

429Too many requests in a minute
{
  "detail": "More than 30 requests in 60 seconds from this address. An API key allows 120.",
  "errorKind": "rate_limited",
  "limit": 30,
  "windowSeconds": 60,
  "upgradeUrl": "https://rftools.io/pricing/"
}

With Retry-After. The limit follows the credential the request carries (see Rate Limits).

400 / 409No room for another key
{
  "detail": "This account already holds 1 API key, the most the Free plan allows. Revoke a key to free its place, or upgrade to Pro for 3 API keys.",
  "errorKind": "not_available",
  "keyLimit": 1,
  "manageUrl": "https://rftools.io/dashboard/#api-usage",
  "upgradeUrl": "https://rftools.io/pricing/"
}

POST /apikeys answers 400; the device approval answers 409 with reason: key_limit.

Endpoints

POST/api/py/v1/calculate

Run any calculator with the given inputs and return computed values.

Auth: Required: X-API-Key: rfc_…, or Authorization: Bearer rfc_…

Request Body

{
  "slug": "vswr-return-loss",
  "inputs": { "vswr": 2.0 }
}

Response

{
  "slug": "vswr-return-loss",
  "values": {
    "gamma": 0.3333333333333333,
    "returnLoss": 9.54242509439325,
    "mismatchLoss": 0.5115252244738131,
    "reflectedPct": 11.11111111111111,
    "transmittedPct": 88.88888888888889
  },
  "warnings": [],
  "errors": [],
  "provenance": {
    "method": "calculator:vswr-return-loss",
    "version": "api@3f2c1a9b04de",
    "formulaRef": "Pozar, \"Microwave Engineering\" 4th ed., Chapter 2",
    "assumptions": [],
    "validRange": {
      "status": "inside",
      "bounds": {
        "vswr": {
          "min": 1,
          "max": 100,
          "unit": ":1"
        }
      },
      "outside": [],
      "model": null
    },
    "computedAt": "2026-09-24T14:05:11.003Z",
    "inputs": {
      "vswr": 2
    },
    "seed": null,
    "elapsedSeconds": 0.003
  }
}

curl

curl -X POST https://rftools.io/api/py/v1/calculate \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{"slug":"vswr-return-loss","inputs":{"vswr":2.0}}'
POST/api/py/v1/calculate/solve

Find the value of one input that brings an output to a target — e.g. the trace width for 50 Ω — and return it with the forward result at exactly that value. See Solve for a Target below.

Auth: Required: X-API-Key: rfc_…, or Authorization: Bearer rfc_…

Request Body

{
  "slug": "microstrip-impedance",
  "inputs": { "substrateHeight": 1.51, "dielectricConstant": 4.5, "copperThickness": 35 },
  "solveFor": "traceWidth",
  "target": { "output": "impedance", "value": 50 },
  "grid": 0.001
}

Response

{
  "slug": "microstrip-impedance",
  "solveFor": "traceWidth",
  "target": {
    "output": "impedance",
    "value": 50
  },
  "grid": 0.001,
  "value": 2.784,
  "unrounded": 2.784010722886359,
  "reached": true,
  "evaluations": 68,
  "warnings": [],
  "result": {
    "slug": "microstrip-impedance",
    "values": {
      "impedance": 50.0001127254694,
      "effectiveDielectric": 3.394742006654256,
      "propagationDelay": 6.145860301041207
    },
    "warnings": [],
    "errors": [],
    "provenance": {
      "method": "calculator:microstrip-impedance",
      "version": "api@3f2c1a9b04de",
      "formulaRef": "Hammerstad & Jensen (1980); Wadell, \"Transmission Line Design Handbook\" 1991",
      "assumptions": [
        {
          "code": "quasi-static",
          "text": "Quasi-static: Z₀ and εeff are the Hammerstad–Jensen static values, with no frequency dependence; dispersion is not modelled."
        },
        {
          "code": "bare-microstrip",
          "text": "Bare microstrip: air above the trace, with no soldermask or other cover layer."
        },
        {
          "code": "homogeneous-substrate",
          "text": "One homogeneous, isotropic, non-magnetic substrate of relative permittivity εr over a solid ground plane, both extending well beyond the trace."
        },
        {
          "code": "thickness-effective-width",
          "text": "Copper thickness t is modelled as an effective width increase Δw = (t/π)(1 + ln(2h/t)), applied to both Z₀ and εeff."
        },
        {
          "code": "lossless",
          "text": "Lossless: conductor and dielectric losses are neglected, so Z₀ is real and the propagation delay is √εeff/c."
        }
      ],
      "validRange": {
        "status": "inside",
        "bounds": {
          "traceWidth": {
            "min": 0.01,
            "max": 50,
            "unit": "mm"
          },
          "substrateHeight": {
            "min": 0.05,
            "max": 10,
            "unit": "mm"
          },
          "dielectricConstant": {
            "min": 1,
            "max": 100,
            "unit": ""
          },
          "copperThickness": {
            "min": 1,
            "max": 200,
            "unit": "μm"
          }
        },
        "outside": [],
        "model": null
      },
      "computedAt": "2026-09-24T14:05:11.004Z",
      "inputs": {
        "traceWidth": 2.784,
        "substrateHeight": 1.51,
        "dielectricConstant": 4.5,
        "copperThickness": 35
      },
      "seed": null,
      "elapsedSeconds": 0.004
    }
  }
}

curl

curl -X POST https://rftools.io/api/py/v1/calculate/solve \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{"slug":"microstrip-impedance",
       "inputs":{"substrateHeight":1.51,"dielectricConstant":4.5,"copperThickness":35},
       "solveFor":"traceWidth",
       "target":{"output":"impedance","value":50},
       "grid":0.001}'
GET/api/py/health

Health check endpoint — returns service status.

Auth: None

Response

{ "status": "ok", "service": "rftools-api" }

curl

curl https://rftools.io/api/py/health

Request & Response Schema

CalculateRequest

slugstringrequired

Calculator identifier (e.g. "vswr-return-loss")

inputsRecord<string, number>required

Key-value pairs of input parameters

CalculateResponse

slugstring

Echo of the calculator slug

valuesRecord<string, number>

Computed output values

warningsstring[]

Non-fatal warnings (e.g. out-of-range inputs)

errorsstring[]

Calculation errors (empty on success)

provenanceobject

How the values were computed — see Provenance below

Solve for a Target

POST /api/py/v1/calculate/solve finds the value of one calculator input that brings one of its outputs to a target — the trace width that gives 50 Ω, the gap that gives a target differential impedance — and returns it together with the forward result at exactly that value. One metered call replaces the several forward calls a client would otherwise spend searching by hand.

The search runs over solveFor's stated bounds, narrowed by range where one is given; an input with no stated bound needs range. Where grid is given, the returned value is rounded to the nearest grid multiple inside the range and unrounded carries the exact solution; without a grid, value and unrounded are the same number. Either way, result — exactly what /calculate returns for those inputs, provenance included — is the forward calculator run at exactly the returned value, so what a client displays is still a real forward computation, never the solver's own estimate.

SolveRequest

slugstringrequired

Calculator identifier

inputsRecord<string, number>required

Every input except solveFor. A value given for solveFor is only the search's start point, used to break a tie between more than one solution

solveForstringrequired

A declared numeric input of the calculator

target.outputstringrequired

An output key the calculator returns

target.valuenumberrequired

The value that output should reach

gridnumber

A positive manufacturing grid; the returned value is rounded to its nearest multiple inside the range

range[number, number]

Narrows solveFor's stated bounds; required when it states none

SolveResponse

valuenumber

The solved value, rounded to grid when one was given

unroundednumber

The exact solution, before grid rounding

reachedboolean

Whether the target was actually reached; false still carries the nearest value found, not a guess

evaluationsinteger

How many forward evaluations the search used

warningsstring[]

The solve's own findings — another solution exists in the range, or the target was not reached

resultobject

Exactly what /calculate returns for inputs with solveFor set to value: values, warnings, errors and provenance (see Provenance below)

Cost

One metered call, charged like /calculate after every validation below, and released if the service then fails. A target the search cannot reach still succeeds and is still charged, with reached: false and the nearest value found — the caller asked for something the service delivered.

More than one solution

Where the output crosses the target more than once inside the range, the call returns the crossing nearest the start value — the caller's own value for solveFor, else the calculator's default — and adds a warning naming the other one.

A request that cannot be solved is refused before anything is queued or charged, naming the field at fault:

404The calculator slug does not exist.
400solveFor is not one of the calculator's declared numeric inputs.
400target.output is not one of the calculator's outputs.
400grid is given and is not a finite, positive number.
400Any given value — an input, target.value, or a range bound — is not finite.
400solveFor has no stated bound and the body gives no range, or the given range lies outside the stated bounds or is empty.
400A fixed input exceeds a limit the calculator enforces on its own running time, as for /calculate.
Naming an output as solveFor:
{
  "detail": [
    {
      "param": "solveFor",
      "value": "impedance",
      "reason": "impedance is an output of microstrip-impedance, not a declared input.",
      "allowed": "traceWidth, substrateHeight, dielectricConstant, copperThickness"
    }
  ]
}

KiCad plugin

rftools.io board calculations is a KiCad 10 plugin that reads the open board's stackup and net classes and calls this API for the numbers a layout needs: microstrip, stripline and grounded-CPW single-ended impedance; differential impedance of edge-coupled pairs; the trace width or pair gap that reaches a target impedance (one /calculate/solve call per target — see Solve for a Target above); the IPC-2152 track width for a current; and via impedance, capacitance, inductance and current capacity. Every figure carries its formula reference, whether the inputs are inside the calculator's valid range, and the engine version that computed it.

Requirements

KiCad 10.0 or later, with the KiCad API switched on (Preferences › Plugins › Enable KiCad API). It runs on KiCad's own Python — nothing to install.

Install

In KiCad's Plugin and Content Manager, Manage repositories, add https://antonpogrebenko-public.github.io/rftools-kicad/repository.json, then install "rftools.io board calculations". Repository: github.com/antonpogrebenko-public/rftools-kicad (MIT).

Key

The dialog's Get a free key button opens https://rftools.io/dashboard/?newKey=1&client=kicad-plugin — an ordinary key, as in Get a Key above. Usage appears on the dashboard under the kicad-plugin client label.

Cost

One API call per computed figure; a target width or gap is one solve call, not a search. A figure asked for twice — the two outer layers of a symmetric board — is sent once: repeats within 7 days come from a local cache and cost nothing. Before each run, the dialog states the calls it will use and what remains this month.

What is sent. Each request holds a calculator slug and numbers — widths, heights, εr, copper thickness, a current or a target. It never sends the board file, net or net-class names, layer names, or the project name or path.

Net-class writes. Nothing on the board changes unless you ask: a run previews the proposed track width, differential-pair width and gap for each class before anything is written, and every write can be undone. On KiCad 10.0.6 and earlier the plugin writes nothing — KiCad's API corrupts a net class written through it — and shows the values to enter in Board Setup › Net Classes instead.

Full detail, source and licence (MIT): github.com/antonpogrebenko-public/rftools-kicad.

Provenance

Every result says how it was computed: each calculator result, each batch item and each job result carries one provenance object with the same nine members. The values beside it are unchanged by it. An input outside the range a calculator is stated for is still computed; validRange names it and a warning is added.

methodWhat computed the values: calculator:<slug>, or a job handler's method.
versionThe engine that computed them, <component>@<revision>: api@…, worker@…, mcp@…, web@…. A build that cannot tell its revision says so (api@dev).
formulaRefThe published source of the formula, or null where none is named.
assumptionsWhat the method assumes. Empty means none is stated, not that there are none.
validRangestatus (inside, outside or unknown), the bounds of every input with its unit, the inputs outside them, and a fitted model's validated range and worst-case error where there is one.
computedAtWhen, ISO-8601 in UTC with milliseconds.
inputsThe inputs the values were computed from, after defaults were applied.
seedThe seed of a randomised computation; null for every calculator.
elapsedSecondsHow long the computation took.

A result stored before September 2026 carries only method, version, seed, inputs and elapsedSeconds: read an absent member as unknown, not as empty. New members may be added; none is renamed or removed.

Try It

Interactive Playground

Enter your API key to run queries. Get a free key on your dashboard →

Popular Calculator Slugs

There are 241 calculators available. Here are the most common ones. Browse the full list at /calculators.

SlugCalculator
vswr-return-lossVSWR / Return Loss
microstrip-impedanceMicrostrip Impedance
dbm-wattsdBm / Watts / dBW
free-space-path-lossFree Space Path Loss
skin-depthSkin Depth
wavelength-frequencyWavelength / Frequency
ohms-lawOhm's Law
pcb-trace-widthPCB Trace Width

Async Simulations

The calculators answer in one request. The simulators do not — a full-wave solve runs for minutes to hours — so they are submitted as jobs and polled. Every job type publishes its parameter contract at /api/py/v1/jobs/types: type, unit, range or options, default and requiredness for every parameter, plus the limits that apply to your tier. That document is the same one the browser form is built from and the worker validates against, so a request it accepts is one the whole system accepts. Read it rather than typing parameter names from this page.

MethodPath
GET/api/py/v1/jobs/types

The catalogue: every job type, its tiers, time ceiling, tier-scoped limits and the uploads it takes.

GET/api/py/v1/jobs/types/{jobType}

One job type's parameter contract, as JSON Schema — the same document the browser form and the worker validate against.

POST/api/py/v1/upload

A presigned POST form for an input file. Upload, then pass the returned key in inputFileKeys.

POST/api/py/v1/jobs

Submit a job. Returns 202 with a jobId, or 400 listing every parameter that was refused.

GET/api/py/v1/jobs/{jobId}

Poll status. Carries progress, stage, finishedAt, elapsedSeconds, errorKind and — when complete — a presigned resultUrl.

GET/api/py/v1/jobs

Recent jobs belonging to the caller, including ones submitted with any API key the account holds.

curlSubmit and poll
# 1. read the contract
curl https://rftools.io/api/py/v1/jobs/types/sat_link_budget

# 2. submit
curl -X POST https://rftools.io/api/py/v1/jobs \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{"jobType":"sat_link_budget",
       "params":{"latitude_deg":40.7,"longitude_deg":-74.0,"frequency_ghz":12}}'
# → 202 {"jobId":"...","status":"queued","queuePosition":1,"queueTotal":1}

# 3. poll
curl https://rftools.io/api/py/v1/jobs/JOB_ID \
  -H "Authorization: Bearer YOUR_API_KEY"

Job Types

13 job types. The parameter list of each is at /api/py/v1/jobs/types/{jobType}.

jobTypeSimulator
antenna_simWire Antenna Simulator (NEC-2)
emi_radiatedEMI Radiated Emissions Estimator
eye_diagramEye Diagram from S-Parameters
fdtd_sparamFDTD Transmission Line Simulator
filter_monte_carloRF Filter Monte Carlo Analysis
impedance_matchBroadband Impedance Matching Synthesizer
magnetics_optimizerMagnetics & Transformer Design Optimizer
pdn_impedancePDN Impedance Analyzer & Decoupling Capacitor Optimizer
radar_detectionRadar Detection Performance Monte Carlo
rf_cascadeRF Cascade Budget Analyzer
sat_link_budgetSatellite & Terrestrial Link Budget
smps_control_loopSMPS Control Loop Stability Analyzer
sparam_pipelineS-Parameter Analysis Pipeline

When a Request Is Refused

A submission is checked against its contract before anything is queued or charged. A request outside it is a 400 naming every offending parameter — not the first one reached — and nothing is queued and no allowance is spent. Unknown parameter keys are refused rather than ignored.

{
  "errorKind": "invalid_request",
  "detail": [
    {
      "param": "frequency_ghz",
      "value": 900,
      "reason": "Frequency (frequency_ghz) must be between 0.1 and 100 GHz (got 900 GHz).",
      "allowed": "0.1 to 100 GHz"
    },
    {
      "param": "longitude_deg",
      "value": null,
      "reason": "Site Longitude (longitude_deg) is required.",
      "allowed": null
    }
  ]
}

Error Kinds

Every failure carries errorKind beside its message — on a refused submission, and on a job that failed after being accepted. This is a closed set; branch on it rather than on the wording of the message, which is written for people and may change.

invalid_requestA parameter is missing, of the wrong type, out of range, not one of the declared options, or not a parameter of this job type. The 400 body lists every offending parameter.
too_largeThe request is within the contract but beyond what this tier's lane can run. Reduce the size of the problem.
not_availableThe request asks for a mode or a feature this tier does not carry. The message names what the tier does include.
rate_limitedA concurrent-job limit or a monthly allowance is spent. Carries Retry-After, and resetAt when the allowance is monthly.
timeoutThe job exceeded its type's time ceiling. Nothing is charged for it.
interruptedThe run stopped before it finished — a worker was replaced or a task was stopped. Nothing is charged for it.
result_expiredThe job completed, but its result is no longer stored. The status still reads completed and carries no resultUrl.
transientThe service could not accept or check the request just now. Nothing was queued or charged; try again.
faultThe service failed while computing. The message says which step; nothing is charged for it.

Limits, Duplicates and Lifetimes

Concurrency

3 jobs queued or running at once, per caller. Over it, the API returns 429 with Retry-After and a body carrying pending and limit.

Duplicate Submissions

An identical request from the same caller within 60 s returns the existing job — with its real status, so a job that already failed comes back as failed with its reason. Send Idempotency-Bypass: true to run it again anyway.

Uploads

POST /api/py/v1/upload returns a presigned POST form valid for 5 minutes. Maximum 10 MB per file, enforced by the form itself. Accepted: .s1p .s2p .s3p .s4p .s6p .s8p .snp .csv .txt. The uploaded object is kept for 24 hours, so submit the job that uses it within the day. 20 forms per minute per address.

Results

Two different clocks. resultExpiresAt is when the result itself goes — 30 days on the free tier, kept indefinitely on Pro and API. resultUrlExpiresIn is how long the presigned resultUrl lasts: 15 minutes. Poll again for a fresh link; do not cache the URL. Once the result is gone the status still reads completed, with kind result_expired and no URL.

What Changed for Callers Who Omit a Parameter

Each job type now has one definition of its parameters, and the handlers' own fallbacks are gone. If you send every field, nothing changed. If you omit one of these, the value used is different from before — so the answer is too.

jobTypeParameterWasNow
rf_cascadenfSpec_db10 dB6 dB
rf_cascadeinputPower_dbm−60 dBm−70 dBm
rf_cascadebandwidth_hz1 MHz10 MHz
rf_cascadestages"[]"required — no default
smps_control_looptoleranceDistributionuniformgaussian
filter_monte_carlofreqCutoff1 MHz1 GHz
impedance_matchfreqStart1 MHz800 MHz
impedance_matchfreqStop100 MHz1.2 GHz
antenna_simfreq (instant solve)145 MHz146 MHz
pdn_impedanceportX_mmboard centre (boardWidth_mm / 2)one-third point (boardWidth_mm / 3)
pdn_impedanceportY_mmboard centre (boardLength_mm / 2)one-third point (boardLength_mm / 3)

Unknown parameter keys were previously dropped and answered under a default. They are now refused, and the refusal lists the keys the job type accepts.

For the job types that sample — filter_monte_carlo and the other optimisers — the random seed is derived from the body you submit, so the same design sent with different keys draws a different sample. Omitting a parameter and sending it at its default are the same request to the solver but not the same body, which is why a run from the web form and a run from this API can differ within sampling error. Send randomSeed to pin a run exactly.

Changes in the September 2026 release

Behaviour that changed for an existing integration. If you send every parameter and check errorKind rather than message text, the only differences you should see are the ones below. Later changes, and the date each was announced, are on the API changelog.

  • Unknown parameter keys and out-of-range values are refused at submit with a 400 naming the offending parameter, rather than being dropped or clamped and answered anyway.
  • Eleven parameter defaults changed for a caller who omits the field — see the table above.
  • A spent monthly allowance is now HTTP 402 (errorKind rate_limited) with Retry-After and resetAt, never a 401 and never a plain 429. This covers both the API-key month and the FDTD solve-mode quota.
  • An upload now needs a signed-in session or an API key; an anonymous request gets a 401 before anything is stored. The anonymous free lane loses the two file-input tools (eye_diagram, sparam_pipeline) entirely, since neither runs without an uploaded file.
  • A job submitted with an API key now belongs to the key's owner account: it is authorised by owner, not just by key id or submitting IP, and it appears in the owner's dashboard job listing.
  • A completed result now carries warnings and provenance (method, version, seed and the normalised inputs actually used) alongside the computed values.
  • Every failure — a refused submission and a job that failed after being accepted — now carries errorKind. See the closed set above.
  • A job whose worker dies mid-run — replaced, or its task stopped — is now failed with kind interrupted rather than left stuck, and any charge for it is released.
  • A result is kept for exactly as long as its record says: see resultExpiresAt in the Results card above.
  • The impedance synthesiser's Auto topology now compares all five topologies and returns the best worst-case return loss — it always built an L network before, in both quick mode and the advanced default.
  • The impedance synthesiser now honours maxQ and componentSeries; both were accepted and silently ignored before.
  • The E-series standard-value snap now rounds a mantissa above about 9.55 up to the next decade's first value (9.9 nF → 10 nF) instead of down. This can change one part value in an impedance-match design or a filter Monte Carlo run for the same input.
  • SPICE netlists exported from the impedance page now carry the standard (snapped) component values, not the ideal ones, with a header line naming the series.
  • Power-inductor and coupled-inductor designs are now sized from the inductance and peak current you give them, rather than derived from other inputs; flyback and forward transformers are unchanged. A request whose stated peak current is inconsistent with its stated inductance's ripple comes back with a ripple_exceeds_peak warning.
  • Deploy note: the API validator deployed ahead of the worker during this rollout, so a job queued in that window whose body carried a misspelled key — previously accepted silently under a default — was refused at submit instead of run to completion.

Rate Limits

Monthly allowance, per account

PlanCalls a monthKeysBatch endpoint
Free51 API key—
Pro1003 API keys—
API10,0005 API keysYes

Requests a minute, per address

The request carriesRequests a minute
No credential30
Signed-in session60
API key120

A spent monthly allowance is 402; too many requests in a minute is 429 with a Retry-After header — see Refusals. The device-grant endpoints are held to the no-credential rate whatever they carry.

Error Codes

200Success — result returned in response body
202Accepted — an async job was queued; poll it for the result
400Bad request — the body lists every parameter that was refused
401Unauthorized — no key was sent (the body carries keyUrl), or the key is not one we issued, or was revoked. Never about the allowance
402Payment required — the account's monthly allowance is spent; carries reason, limit, used, resetAt, upgradeUrl and Retry-After
403Forbidden — the job belongs to someone else, or the tier does not carry the mode or the batch endpoint
404Not found — no such calculator slug, job type, or job. Nothing is counted
409Conflict — a device approval at the key limit, or for a code already decided
429Rate limited — too many requests per minute, or too many jobs at once; carries limit, windowSeconds and Retry-After
503Temporarily unavailable — nothing was queued or charged; try again

Ready to integrate?

Get your API key and start making requests in under a minute.10,000 calls a month on the API plan, 120 requests a minute with a key, all 241 calculators.

Or get a free key (5 calls a month) from your dashboard — no credit card required.