certapi

Send requests with the client certificate already in your Windows store

Some internal APIs don't take a password; they ask for a certificate. certapi picks yours from CurrentUser\My or LocalMachine\My and lets Windows sign the TLS handshake, so the private key never leaves the store and never has to be exportable. A .pfx or .pem file works too.

The seal of a certapi doctor run against https://client.badssl.com/, one ring per stage: url 5 ms, proxy 49 ms, dns 68 ms, tcp 36 ms, tls 397 ms, http 88 ms; TLS 1.2 with the client certificate presented; the server answered HTTP/1.1 200 OK.

The seal of a certapi doctor run against https://client.badssl.com/, one ring per stage: url 11 ms, proxy 66 ms, dns 55 ms, tcp 42 ms, tls 660 ms, http 90 ms; TLS 1.2 with no client certificate presented; the server answered HTTP/1.1 400 Bad Request.

A real certapi doctor run against client.badssl.com, a public test server that asks for a client certificate. One ring per stage, outside in, with the time each took. The certificate is BadSSL's published test one, passed with --cert-file; --cert "CN=…" takes yours from the store. The same URL with no certificate. Every stage connects, the TLS handshake included, and the server answers 400 Bad Request: “No required SSL certificate was sent”.

Install in PowerShell. No installer, no admin rights.

mkdir "$env:LOCALAPPDATA\certapi" -Force; cd "$env:LOCALAPPDATA\certapi"curl.exe -LO https://github.com/Real-Fruit-Snacks/certapi/releases/latest/download/certapi.execurl.exe -LO https://github.com/Real-Fruit-Snacks/certapi/releases/latest/download/ApiTester.App.exe

certapi.exe is the command line and ApiTester.App.exe the desktop app, both from the latest release with the .NET runtime inside. Add the folder to your PATH; certapi update later replaces both, each checked against the SHA-256 GitHub publishes. With Scoop: scoop install https://github.com/Real-Fruit-Snacks/certapi/releases/latest/download/certapi.json

Some endpoints don't take a password

They ask the browser to choose a certificate and complete a mutual-TLS handshake. Most API clients want that certificate and its private key as files on disk, which enterprise and smart-card certificates deliberately don't allow. certapi uses the certificate where it already is, and the operating system does the signing.

The certificate is optional. With none selected certapi is an ordinary HTTP client, so the same collections, environments and assertions work against anything else too. I wrote it for my own internal APIs; it assumes Windows 10 or 11 and nothing else.

Which certificate does this server want?

certapi doctor connects one stage at a time and names the stage that broke. At the TLS stage it reads the authorities the server will accept client certificates from, and checks them against every certificate in your store. This is the run the seal draws, exactly as certapi printed it.

PS> certapi doctor https://client.badssl.com/ --cert-file badssl.com-client.p12 --cert-password badssl.com
certapi doctor · https://client.badssl.com/

  [ok  ] url      https://client.badssl.com:443/  (5 ms)
  [ok  ] proxy    DIRECT (system proxy says direct)  (49 ms)
  [ok  ] dns      client.badssl.com → 1 address  (68 ms)
           104.154.89.105
  [ok  ] tcp      connected to client.badssl.com:443  (36 ms)
  [ok  ] tls      handshake succeeded (Tls12)  (397 ms)
           protocol: Tls12
           cipher: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
           server certificate: CN=*.badssl.com (issued by CN=YR1, O=Let's Encrypt, C=US, expires 2026-12-28)
           chain (4 certificates):
             1. CN=*.badssl.com  F95E561C27728A1FF8461E7F6C89CB0225674E83
             2. CN=YR1, O=Let's Encrypt, C=US  CBCFD09CE3CD22D99D94E58F92E9898FBEE51A5E
             3. CN=Root YR, O=ISRG, C=US  AB9D0263244DD0326EB67015705A667E79CFE998
             4. CN=ISRG Root X1, O=Internet Security Research Group, C=US  CABD2A79A1076A31F21D253635CB039D4329A5E8
           client certificate presented: CN=BadSSL Client Certificate, O=BadSSL, L=San Francisco, S=California, C=US1
           the server accepts client certificates from 1 authority/authorities:2
             CN=BadSSL Client Root Certificate Authority, O=BadSSL, L=San Francisco, S=California, C=US
           NONE of your 5 certificate(s) are issued by any of those authorities3
           the certificate you chose IS issued by one of them
  [ok  ] http     HTTP/1.1 200 OK  (88 ms)

All stages passed in 643 ms.
  1. The certificate certapi sent. Here it came from BadSSL's test file; with --cert "CN=…" it is one from your store, and Windows signs with its key.
  2. Read from the server's certificate request during the handshake: the issuers it will take a client certificate from.
  3. Every certificate in this machine's store, checked against that list. None of these five fit; the one passed on the command line does.

Without --cert-file, the same run reads client certificate presented: none and ends with HTTP/1.1 400 Bad Request. Run 5 November 2026 with certapi 1.96.1 on Windows 11.

certapi on the command line

The status line goes to stderr and the body to stdout, so a script can pipe one and read the other. Exit codes: 0 success, 1 failure, 2 usage, 3 data.

PS> certapi send https://client.badssl.com/
400 Bad Request · 262 B · 680 ms · TLS 1.2
<title>400 No required SSL certificate was sent</title> …

PS> certapi send https://client.badssl.com/ --cert-file badssl.com-client.p12 --cert-password badssl.com
200 OK · 687 B · 777 ms · TLS 1.2 · client cert presented

Both against BadSSL's public test server, the body cut to its title.

# one request, certificate from the store
certapi send https://internal.corp/api/orders --cert "CN=matt"
# log in, capture the token, reuse it
certapi send https://internal.corp/auth --capture token=access_token --workspace team.json
# a collection as a pass/fail suite (exit 1 on any failure)
certapi run "internal api" --env Prod --json
# the bytes, as framed and as received
certapi send https://internal.corp/api/orders --wire
# gRPC behind mutual TLS
certapi grpc call api.internal:5001 my.pkg.Greeter/SayHello -d '{"name":"Ada"}'
# stream, bench, mock, MCP
certapi sse https://internal.corp/events --max-events 5
certapi bench https://api.internal/health -n 500 -c 20
certapi mock --har session.har --port 8770
certapi mcp --cert "CN=matt" --allow internal.corp

Hosts here are examples. certapi <command> --help lists every flag, curl habits like -k, -i and send -d @file work, and a mistyped flag names the one you meant. The CLI reference has them on one page.

For an app that can't present a certificate

certapi serve is a local gateway that adds your certificate on the way out. Point the app at the port. It fronts one site or many: named sites, a certificate and settings per site, one origin each with --vhost, logins across hosts with --browser, a whole gateway kept in a profile. --check prints the table without binding anything. Requests to hosts you didn't allow are refused.

PS> certapi serve --port 8819 --cert-file badssl.com-client.p12 --cert-password badssl.com --upstream badssl=https://client.badssl.com
listening on http://127.0.0.1:8819/
  SITE    OPEN                           UPSTREAM                   CERT                       SETTINGS
  badssl  http://127.0.0.1:8819/badssl/  https://client.badssl.com  BadSSL Client Certificate  -
  every site: cert BadSSL Client Certificate · no token · loopback only
GET    /badssl/  -> 200  4787 ms

PS> curl.exe -s -o NUL -w "%{http_code} %{size_download} B" http://127.0.0.1:8819/badssl/
200 687 B

One engine, two front ends

The desktop app and certapi share one engine, one request model and one workspace on disk, so a request built in the window runs from a script.

Certificates and networks
The Windows certificate store (your Personal store or the machine's), picked by subject, issuer or serial, or a .pfx/.pem file; a smart-card PIN supplied ahead for headless jobs. Server pins by leaf, issuer or key; a private root from a file with --ca-file; revocation checking. System proxy, PAC, SOCKS, bypass lists, --offline.
Requests
Tabs, collections, environments with {{variables}}, response captures, request chains, assertions, data-driven runs from CSV/JSON, response diffing against a baseline. Import cURL, Postman, OpenAPI, HAR; export OpenAPI, HAR, markdown notes.
Diagnostics
doctor connects one stage at a time and names the one that broke, including the CAs the server accepts client certificates from, matched against yours. --trace, --wire and --frames show what the stack did, down to the plaintext bytes. No driver, no admin.
Gateway and mock
serve adds your certificate for apps that can't. mock is a standing test server over HTTP, HTTPS or mTLS, or a replay of a captured HAR.
Auth
Bearer, Basic, OAuth 2.0 (client credentials, password, refresh, auth-code with PKCE, device code), Windows Integrated (Kerberos/NTLM), browser session capture, and automatic token reuse per host. Secrets are DPAPI-encrypted at rest.
Automation
run turns a collection into a pass/fail suite with exit codes for CI. bench reports p50/p90/p99 latency over a pooled connection. grpc calls services via reflection or a descriptor set. mcp exposes all of it to an AI agent behind a host allowlist.

The desktop app

certapi desktop app: a GET to /api/orders on a local mock server, the Pretty response view showing the JSON it echoed back, and 200 OK, 249 bytes, 5 ms in the status bar
Request editor, response views (Pretty, Raw, Headers, Diagnostics, Network, Rendered, Diff) and the History, Collections and Chains sidebar, against certapi mock.

Tested on

  • Windows 11 23H2 · .NET 9 SDK 9.0.320 or later
  • Windows Server 2022 · headless certapi only
  • GitHub Actions windows-latest on every push: build with warnings as errors, then the test suite
  • Not tested: Windows 10 before 22H2, ARM64 Windows. Probably works, no promises.

Known issues

  • The Rendered view needs the Edge WebView2 runtime. Windows 11 ships it; without it that tab says so and the rest works.
  • certapi serve does not proxy gRPC.
  • --protoset needs a descriptor set you already have; certapi does not compile .proto files.
  • --http3 is Windows 11 / Server 2022 and direct connections only.
  • Through a proxy, TLS version, cipher and certificate-presented details are not visible; --no-proxy brings them back.
  • Over the plaintext gateway, __Host-/__Secure- cookies cannot work; serve --tls is the fix.
  • serve --rewrite-body reaches URLs written into a page, not ones a script assembles at run time; --vhost needs no rewriting at all.
  • A proxy that itself requires a client certificate is not supported.
  • --pac and --cert-pin are Windows-only; --sni works on direct connections only.

The full list is in the README.

Shortcuts in the app

Ctrl+EnterSend the request
EscCancel a request in flight
Ctrl+LFocus the URL box
Ctrl+SSave the response
Ctrl+T / Ctrl+WNew tab / close tab
Ctrl+HShow or hide the sidebar
F5Refresh the certificate list
F1Help and reference