API Documentation
Integrate 241 RF & electronics calculators into your scripts, notebooks, and applications via a simple REST API. One endpoint, any calculator.
Quick Start
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).
Call the Endpoint
POST to /api/py/v1/calculate with a slug and inputs.
Use the Results
Parse the JSON response with computed values, warnings, and errors.
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 asXXXX-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_codeis 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_pending | The user has not decided yet. Keep polling at the interval. |
slow_down | Polled faster than the interval. The interval grows by 5 s; the body carries the new interval. |
access_denied | The user denied the code, or the account reached its key limit before the key was collected. Stop polling. |
expired_token | The code expired (10 minutes after it was issued) or its key was already collected. Start again. |
invalid_grant | The device code was issued to another client_id. |
invalid_request / unsupported_grant_type | A 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-Allowance | The account's monthly allowance. |
X-Usage-Used | Calls counted this month, including this one. |
X-Usage-Reset | When the allowance resets (ISO-8601, UTC): the first instant of next month. |
X-Usage-Overage | Present 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
/api/py/v1/calculateRun 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}}'/api/py/v1/calculate/solveFind 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}'/api/py/healthHealth 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
slugstringrequiredCalculator identifier (e.g. "vswr-return-loss")
inputsRecord<string, number>requiredKey-value pairs of input parameters
CalculateResponse
slugstringEcho 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)
provenanceobjectHow 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
slugstringrequiredCalculator identifier
inputsRecord<string, number>requiredEvery 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
solveForstringrequiredA declared numeric input of the calculator
target.outputstringrequiredAn output key the calculator returns
target.valuenumberrequiredThe value that output should reach
gridnumberA 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
valuenumberThe solved value, rounded to grid when one was given
unroundednumberThe exact solution, before grid rounding
reachedbooleanWhether the target was actually reached; false still carries the nearest value found, not a guess
evaluationsintegerHow many forward evaluations the search used
warningsstring[]The solve's own findings — another solution exists in the range, or the target was not reached
resultobjectExactly 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:
404 | The calculator slug does not exist. |
400 | solveFor is not one of the calculator's declared numeric inputs. |
400 | target.output is not one of the calculator's outputs. |
400 | grid is given and is not a finite, positive number. |
400 | Any given value — an input, target.value, or a range bound — is not finite. |
400 | solveFor has no stated bound and the body gives no range, or the given range lies outside the stated bounds or is empty. |
400 | A fixed input exceeds a limit the calculator enforces on its own running time, as for /calculate. |
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.
method | What computed the values: calculator:<slug>, or a job handler's method. |
version | The engine that computed them, <component>@<revision>: api@…, worker@…, mcp@…, web@…. A build that cannot tell its revision says so (api@dev). |
formulaRef | The published source of the formula, or null where none is named. |
assumptions | What the method assumes. Empty means none is stated, not that there are none. |
validRange | status (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. |
computedAt | When, ISO-8601 in UTC with milliseconds. |
inputs | The inputs the values were computed from, after defaults were applied. |
seed | The seed of a randomised computation; null for every calculator. |
elapsedSeconds | How 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
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.
| Slug | Calculator |
|---|---|
vswr-return-loss | VSWR / Return Loss |
microstrip-impedance | Microstrip Impedance |
dbm-watts | dBm / Watts / dBW |
free-space-path-loss | Free Space Path Loss |
skin-depth | Skin Depth |
wavelength-frequency | Wavelength / Frequency |
ohms-law | Ohm's Law |
pcb-trace-width | PCB 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.
| Method | Path |
|---|---|
| GET | /api/py/v1/jobs/typesThe 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/uploadA presigned POST form for an input file. Upload, then pass the returned key in inputFileKeys. |
| POST | /api/py/v1/jobsSubmit 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/jobsRecent jobs belonging to the caller, including ones submitted with any API key the account holds. |
# 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}.
| jobType | Simulator |
|---|---|
antenna_sim | Wire Antenna Simulator (NEC-2) |
emi_radiated | EMI Radiated Emissions Estimator |
eye_diagram | Eye Diagram from S-Parameters |
fdtd_sparam | FDTD Transmission Line Simulator |
filter_monte_carlo | RF Filter Monte Carlo Analysis |
impedance_match | Broadband Impedance Matching Synthesizer |
magnetics_optimizer | Magnetics & Transformer Design Optimizer |
pdn_impedance | PDN Impedance Analyzer & Decoupling Capacitor Optimizer |
radar_detection | Radar Detection Performance Monte Carlo |
rf_cascade | RF Cascade Budget Analyzer |
sat_link_budget | Satellite & Terrestrial Link Budget |
smps_control_loop | SMPS Control Loop Stability Analyzer |
sparam_pipeline | S-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_request | A 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_large | The request is within the contract but beyond what this tier's lane can run. Reduce the size of the problem. |
not_available | The request asks for a mode or a feature this tier does not carry. The message names what the tier does include. |
rate_limited | A concurrent-job limit or a monthly allowance is spent. Carries Retry-After, and resetAt when the allowance is monthly. |
timeout | The job exceeded its type's time ceiling. Nothing is charged for it. |
interrupted | The run stopped before it finished — a worker was replaced or a task was stopped. Nothing is charged for it. |
result_expired | The job completed, but its result is no longer stored. The status still reads completed and carries no resultUrl. |
transient | The service could not accept or check the request just now. Nothing was queued or charged; try again. |
fault | The 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.
| jobType | Parameter | Was | Now |
|---|---|---|---|
rf_cascade | nfSpec_db | 10 dB | 6 dB |
rf_cascade | inputPower_dbm | −60 dBm | −70 dBm |
rf_cascade | bandwidth_hz | 1 MHz | 10 MHz |
rf_cascade | stages | "[]" | required — no default |
smps_control_loop | toleranceDistribution | uniform | gaussian |
filter_monte_carlo | freqCutoff | 1 MHz | 1 GHz |
impedance_match | freqStart | 1 MHz | 800 MHz |
impedance_match | freqStop | 100 MHz | 1.2 GHz |
antenna_sim | freq (instant solve) | 145 MHz | 146 MHz |
pdn_impedance | portX_mm | board centre (boardWidth_mm / 2) | one-third point (boardWidth_mm / 3) |
pdn_impedance | portY_mm | board 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
| Plan | Calls a month | Keys | Batch endpoint |
|---|---|---|---|
| Free | 5 | 1 API key | — |
| Pro | 100 | 3 API keys | — |
| API | 10,000 | 5 API keys | Yes |
Requests a minute, per address
| The request carries | Requests a minute |
|---|---|
| No credential | 30 |
| Signed-in session | 60 |
| API key | 120 |
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 body202Accepted — an async job was queued; poll it for the result400Bad request — the body lists every parameter that was refused401Unauthorized — no key was sent (the body carries keyUrl), or the key is not one we issued, or was revoked. Never about the allowance402Payment required — the account's monthly allowance is spent; carries reason, limit, used, resetAt, upgradeUrl and Retry-After403Forbidden — the job belongs to someone else, or the tier does not carry the mode or the batch endpoint404Not found — no such calculator slug, job type, or job. Nothing is counted409Conflict — a device approval at the key limit, or for a code already decided429Rate limited — too many requests per minute, or too many jobs at once; carries limit, windowSeconds and Retry-After503Temporarily unavailable — nothing was queued or charged; try againReady 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.