How to Provision CAPTCHA API Keys for Your Customers

provision captcha api keys - How to Provision CAPTCHA API Keys for Your Customers

If you run CapSkip for other people, you can provision CAPTCHA API keys the moment a customer pays, straight from your billing webhook. Remote Key Management exposes three POST endpoints that add, list and delete keys on the running instance, and a new key is live on the very next solving request. No restart, no manual step, and nothing sitting between a cleared payment and a customer who can start solving. This post covers minting a key on subscribe, revoking it on cancel, keeping your key list reconciled with your billing system, and the one networking mistake this setup makes easy.

What you need

  • CapSkip running on a Windows machine you control. That box is your solving service, and it is the only machine that needs the application installed. Your customers never install anything.
  • Server mode, so those customers can actually reach it. Local mode binds to the loopback address and serves that device only, while Server mode binds to your network or public IP so other machines can connect. Both are described under connection settings, and a static public IP is worth having before you hand the address to anyone.
  • One visit to the CapSkip window to generate the admin token. That is the only step that happens in the interface, and you never repeat it.
  • Somewhere to run the calls from: your billing webhook handler, your dashboard backend, or a terminal while you are testing.

Worth stating plainly before the code, because it is the part that makes this model work at all. The keys you are handing out are yours to mint. They are not credits you bought and are reselling, and there is no per-solve meter behind them. CapSkip runs on your hardware, so a hundred customer keys cost exactly what one costs.

Step 1: turn on Remote Key Management

The switch is in Settings, under API Key Validation, then Advanced, then Remote Key Management. Turn it on and press the Generate button.

The token is shown once and stored only as a salted hash, so there is nothing to read back later. Lose it and you generate a new one, which invalidates the old immediately. Treat that as the revoke button, because there is no separate one. Put it in the same place your billing system keeps its other secrets, not in your application code.

The endpoints live on the same host and port that already answers your solving requests. If your customers talk to port 8080, so does the admin API. That is convenient and it is also the thing you have to be careful about, which is its own section further down.

While the feature is off, every admin path returns a plain 404 with no body. That is the same response an unknown path gets, and it is deliberate: someone probing the port cannot tell the difference between an instance that has this disabled and one that has never heard of it.

Step 2: mint a key when a customer subscribes

All three endpoints are POST, take a JSON body, and require an Authorization header carrying the token as a bearer credential. The add call needs a name. Send only the name and CapSkip generates the key value for you, which is what you want here: the customer never picks their own credential.

# No install needed. Name the key after the customer, not after the plan.
curl -X POST http://YOUR_SERVER:8080/admin/keys/add \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" -d '{"name":"cust_10482"}'

# Response carries the value to show the customer once:
# {"errorId":0,"key":{"name":"cust_10482","key":"..."}}

Name it after something stable in your own system, like the customer id or the subscription id. Names are unique and that rule is enforced on add, so the name becomes a reliable handle for every later operation. Naming keys after plans or dates instead is the mistake that makes cancellation day painful.

That uniqueness rule is also your idempotency guard. Payment providers retry webhooks, and a retried subscribe event comes back as ERROR_KEY_EXISTS with a 409 rather than quietly creating a second key for the same customer. Treat that 409 as success in your handler and the replay problem disappears.

The value comes back exactly once, in that response. Show it to the customer or store it wherever your dashboard reads from, because listing the keys later is an admin operation and not something you want to run to recover one customer’s credential.

Step 3: revoke it when they cancel

Deleting takes exactly one selector, either the key value or the name. Because you named keys after customers, the name is the one to use:

# By name, which is unambiguous because names are unique.
curl -X POST http://YOUR_SERVER:8080/admin/keys/delete \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" -d '{"name":"cust_10482"}'

# By value works too, if that is what your records hold:
#   -d '{"key":"the-old-value"}'

Revocation takes effect on the next solving request, so access ends the instant the call returns. From then on that customer’s calls fail with ERROR_KEY_DOES_NOT_EXIST, which is a clear enough signal that their integration can surface it as an expired subscription rather than an outage.

Decide deliberately when to fire this. Deleting on the cancellation event cuts access immediately, even though the customer has usually paid through the end of the period. Deleting on the period-end event is what most people actually want. A failed payment is a third case: a short grace window before revocation causes far fewer support tickets than a key that vanishes on a card retry.

Step 4: reconcile the list against your billing system

The list call takes an empty object and returns every key the instance currently accepts.

# The audit: exactly who can solve right now.
curl -X POST http://YOUR_SERVER:8080/admin/keys/list \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" -d '{}'
{
  "errorId": 0,
  "keys": [
    { "name": "cust_10482", "key": "..." },
    { "name": "cust_10515", "key": "..." }
  ]
}

An errorId of 0 means success, the same convention the solving endpoints use. Run this on a schedule and diff the names against your active subscriptions. A key with no matching subscription is someone still solving for free, and a subscription with no key is a customer whose provisioning webhook was dropped and who is probably about to open a ticket. Both are silent failures until you look, and this call is the only place that truth lives.

Wiring it into a subscription webhook

Put together, the whole integration is two handlers and an idempotency rule:

# pip install requests
import requests

BASE = "http://YOUR_SERVER:8080"
AUTH = {"Authorization": "Bearer YOUR_TOKEN"}

def admin(path, body):
    r = requests.post(f"{BASE}/admin/keys/{path}", json=body,
                      headers=AUTH, timeout=10)
    # 409 on add means the key already exists, which is what a
    # retried webhook looks like. Treat it as success, not failure.
    if r.status_code == 409:
        return None
    r.raise_for_status()
    return r.json()

def on_subscription_active(customer_id):
    created = admin("add", {"name": f"cust_{customer_id}"})
    return created["key"]["key"] if created else None

def on_subscription_ended(customer_id):
    admin("delete", {"name": f"cust_{customer_id}"})

Handle the None from a replayed subscribe deliberately. It means the key exists but you cannot see its value any more, so if you failed to store it the first time, the recovery is to delete and re-add rather than to list. Storing the value when you first receive it avoids the whole situation.

Do not leave the admin API on the customer-facing port

This is the section that matters most for this setup, because the convenience of one host and port cuts both ways. Your customers need to reach the solving endpoints. The admin endpoints are on that same port, protected by nothing but the bearer token, and there is no transport check, so on a custom port that token travels in cleartext HTTP. Anyone positioned to watch the traffic can read it, and anyone holding it can mint themselves a key.

Put a reverse proxy in front of the instance and split the two audiences:

  • Publish only the solving paths to the internet, over HTTPS terminated at the proxy, and return a 404 for anything under the admin path.
  • Reach the admin endpoints from your own backend over a private route: the loopback address if your billing service runs on the same Windows box, otherwise a private network, a VPN, or an SSH tunnel.

The rule of thumb is that the admin token should never leave your infrastructure, and the port it talks to should never be one a customer can reach. Never expose the admin path to the open internet, with or without the feature enabled.

One more boundary worth knowing, since customer dashboards are the obvious thing to build here. Browsers cannot call these endpoints, because admin responses carry no CORS headers by design. Your dashboard has to go through your own backend, which is where the token belongs anyway.

Where the keys actually get written

Keys go wherever your keys already live. Direct Input and From File are separate lists, and the API reads and writes whichever one is currently selected. Switching the source in the settings window changes what the endpoints see, so confirm which mode you are in before you start wondering why a key you just provisioned is not in the list.

File mode has one behaviour worth knowing in advance: the first write rewrites a plain-text keys file as JSON. Nothing is lost, but the format changes permanently, so back the file up first if anything else reads it. If no file is selected at all, a write cannot land and you get ERROR_ADMIN_STORE_FAILURE with a 500. That is almost always what a 500 here means.

Lists created before this feature existed can still hold duplicate names, because uniqueness is only enforced on add. Delete by name against one of those returns ERROR_KEY_NAME_AMBIGUOUS and changes nothing. Delete it by value instead and the ambiguity disappears.

Windows shell quoting, if you are testing from the box itself

CapSkip is a Windows application, so the machine you are testing from is often Windows too. Command Prompt does not treat the single quote as a quote character. The copied command sends the quote marks as part of the JSON body, the parser rejects it, and you get ERROR_ADMIN_BAD_REQUEST on a command that looks perfectly correct.

ShellEmpty bodyWith fields
Command Prompt-d "{}"-d "{\"name\":\"prod\"}"
PowerShell, using curl.exe-d '{}'-d '{\"name\":\"prod\"}'
Git Bash, macOS, Linux-d '{}'-d '{"name":"prod"}'

In PowerShell, call curl.exe by its full name. Plain curl there is an alias for Invoke-WebRequest, which takes different arguments entirely and will fail in a way that has nothing to do with the API.

Error codes

StatusCodeCause
404plain textFeature off, or unknown path. Deliberately indistinguishable.
401ERROR_ADMIN_UNAUTHORIZEDToken missing, malformed, or wrong.
405ERROR_ADMIN_METHOD_NOT_ALLOWEDYou sent something other than a POST.
400ERROR_ADMIN_BAD_REQUESTBody is not valid JSON, add has no name, or delete has both selectors or neither.
409ERROR_KEY_EXISTSThat name or value is already in use. On a subscribe handler this is a replayed webhook.
409ERROR_KEY_NAME_AMBIGUOUSDelete by name matched several. Delete by value instead.
404ERROR_KEY_NOT_FOUNDDelete matched nothing. Usually an already-processed cancellation.
500ERROR_ADMIN_STORE_FAILURECould not save. Usually File mode with no file selected.

These sit alongside the solving error codes rather than replacing them, and the full set is in the CapSkip API documentation.

FAQ

How fast does a new key actually start working?

On the next solving request. Nothing is cached and nothing needs reloading, so a customer who receives their key from your checkout page can use it in the same minute. Revocation is just as immediate in the other direction, which is why the timing of your cancellation handler is a policy decision rather than a technical one.

Does each customer key cost me anything?

No. CapSkip runs on hardware you own with no per-solve quota, so a key is a label for a caller rather than a billing identity. Mint one per customer, or several per customer if you want to separate their environments, and delete them just as freely. The CAPTCHA solving SDK reads whichever key you hand it, so how you divide them up is entirely your design.

Can I meter or rate limit a customer through this API?

Not through this API. It adds, lists and deletes keys, and no other setting is readable or changeable through it. If your plans differ by volume, count requests at the reverse proxy you are already putting in front of the instance, keyed on the API key the customer sends. The key identifies the caller, and your proxy decides what that caller is allowed to do.

I lost the admin token. What now?

Generate a new one in the same settings panel. The old token stops working immediately, so there is no cleanup step and no window where both are valid. Your customers are unaffected: their keys are untouched and they keep solving throughout. The only thing that breaks is your own provisioning until you update the secret your webhook handler reads.

The shortest version

Turn on Remote Key Management once and keep the token with your other billing secrets. Add a key named after the customer when their subscription goes active, treat the 409 on a replay as success, and delete by that same name when the subscription ends. List on a schedule and diff it against your active subscribers, because that is the only place the real answer lives. Then put a proxy in front and keep the admin path off the port your customers can see. Do that and captcha bypass becomes something you can resell on your own hardware, provisioned as fast as your checkout can fire a webhook.