Documentation
Integrating Keyflow
Keyflow issues and validates your software's licence keys. Two ways to use it, and the first needs no code at all.
Getting started — 5 minutes
Create your account
On the sign-up page, or through the API. The response contains your API key: it is shown only once.
curl -X POST https://appkeyflow.com/v1/auth/signup \
-H "Content-Type: application/json" \
-d '{"email":"toi@exemple.fr","password":"un-mot-de-passe-solide"}'
{
"user": { "id": "bb60bdd1-…", "email": "toi@exemple.fr",
"subscription_status": "free" },
"api_key": {
"token_prefix": "kf_live_BBrZRi",
"token": "kf_live_BBrZRiTv50MnbcD09Hk0LCwn5ciXCAhhd5jjpQIGOFI"
}
}
Declare your product
curl -X POST https://appkeyflow.com/v1/products \
-H "X-API-Key: kf_live_…" -H "Content-Type: application/json" \
-d '{"name":"Mon Logiciel","default_activation_limit":1}'
The slug is derived from the name if you do not supply one. That is what you will use everywhere afterwards.
Issue your first key
curl -X POST https://appkeyflow.com/v1/licenses \
-H "X-API-Key: kf_live_…" -H "Content-Type: application/json" \
-d '{"product_slug":"mon-logiciel","customer_email":"acheteur@exemple.fr"}'
{
"id": "c06ee4ba-…",
"key": "P914-F9Q3-J01F-WHP1",
"key_masked": "P914-****-****-WHP1",
"status": "active",
"activation_limit": 1,
"activations_used": 0
}
key_masked. Keyflow keeps an encrypted copy: to retrieve it you must go through POST /v1/licenses/{id}/reveal.Main route — keys go out on their own
You point your payment provider's webhook at a Keyflow address. On every completed purchase the key is issued and shown to your buyer. You write nothing: no server, no email, no scheduled job.
What you do, once
- In Keyflow, Sales tab: create a sales channel on your product.
- In Stripe, create an endpoint pointing at the address shown, subscribed to
checkout.session.completed. - Paste that endpoint's
whsec_into Keyflow. - Set your payment's return URL to:
https://appkeyflow.com/r/{CHECKOUT_SESSION_ID}
Stripe replaces {CHECKOUT_SESSION_ID} with the sale's identifier. Your buyer lands on a page showing their key, right after paying.
whsec_ asked for here is the one from your account, where your customers pay. It has nothing to do with your Keyflow subscription.Selling several offers of the same software
Create one sales channel per offer, on the same product, overriding the machine count: one for “1 seat”, one for “3 seats”. Nothing else to configure.
If a sale fails
The Sales tab shows the issue counter and the last error. A sale that exceeds your plan's limits is issued anyway — your customer paid, they should not bear the cost — then flagged.
Alternative route — from your own server
If you already have a backend, or if you sell somewhere other than Stripe, call the API after each payment. That is five lines in your order handler:
import requests
def livrer_licence(email_acheteur):
reponse = requests.post(
"https://appkeyflow.com/v1/licenses",
headers={"X-API-Key": CLE_API},
json={"product_slug": "mon-logiciel", "customer_email": email_acheteur},
timeout=10,
)
reponse.raise_for_status()
return reponse.json()["key"] # → "P914-F9Q3-J01F-WHP1"
const reponse = await fetch("https://appkeyflow.com/v1/licenses", {
method: "POST",
headers: { "X-API-Key": CLE_API, "Content-Type": "application/json" },
body: JSON.stringify({ product_slug: "mon-logiciel",
customer_email: emailAcheteur }),
});
const { key } = await reponse.json(); // → "P914-F9Q3-J01F-WHP1"
It is then up to you to send that key to your customer. With the automatic route, Keyflow does it for you.
Validating the key inside your software
This is the only public call: no API key, since it comes from your customer's machine.
curl -X POST https://appkeyflow.com/v1/validate \
-H "Content-Type: application/json" \
-d '{"key":"P914-F9Q3-J01F-WHP1","device_id":"poste-bureau-01"}'
{
"valid": true,
"reason": "ok",
"message": "Licence valide.",
"license": {
"product_slug": "mon-logiciel", "product_name": "Mon Logiciel",
"status": "active", "expires_at": null,
"activation_limit": 1, "activations_used": 1
}
}
/validate always answers 200, even when it refuses. The verdict is in valid, the cause in reason.This is deliberate: a 403 would be indistinguishable from a network outage, and your software could not tell “licence refused” from “server unreachable”. A real failure shows up as a 5xx, or as no answer at all.
The eight possible reasons
ok— validmalformed_key— invalid format, refused without querying the databasenot_found— unknown keyrevoked— revoked for goodsuspended— suspended, reversibleexpired— past its dateproduct_mismatch— key belongs to another productactivation_limit_reached— all machine slots are taken
Constraints
device_id— 8 characters minimum. Below that, the API answers 422.- It must be stable across restarts, otherwise every launch consumes a machine slot.
- Optional fields:
device_label,platform,app_version,product_slug.
Refusal: one machine too many
{
"valid": false,
"reason": "activation_limit_reached",
"message": "Limite d'activations atteinte (1/1 machines). Libere une machine avant d'en activer une nouvelle.",
"license": { "status": "active", "activation_limit": 1, "activations_used": 1 }
}
Refusal: revoked licence
{
"valid": false,
"reason": "revoked",
"message": "Cette licence a ete revoquee. Contacte le support.",
"license": { "status": "revoked", "activation_limit": 1, "activations_used": 1 }
}
The SDKs — what they save you writing
Calling /validate by hand works, but leaves you three problems: building a stable machine identifier, not cutting your customer off when their connection drops, and stopping them editing a local cache to extend their licence. The SDKs handle all three.
Python
from keyflow import Keyflow
licences = Keyflow(
base_url="https://appkeyflow.com",
product_slug="mon-logiciel",
app_name="MonLogiciel",
grace_days=7,
)
resultat = licences.check(cle_saisie_par_le_client)
if not resultat.allowed:
print(resultat.message) # message prêt à afficher
raise SystemExit(1)
JavaScript · Node and Electron
const { Keyflow } = require("./keyflow");
const licences = new Keyflow({
baseUrl: "https://appkeyflow.com",
productSlug: "mon-logiciel",
appName: "MonLogiciel",
graceDays: 7,
});
const resultat = await licences.check(cleSaisie);
if (!resultat.allowed) { console.error(resultat.message); process.exit(1); }
What they do for you
- Stable machine identifier —
MachineGuidon Windows,IOPlatformUUIDon macOS,/etc/machine-idon Linux. Hashed with your product before sending: Keyflow never receives the raw identifier. - Offline tolerance — the last verdict is cached. Adjustable through
grace_days, 7 days by default. At0, a connection is required at every start. - Signed cache — with HMAC. Editing the file to extend a licence invalidates the signature.
release()— frees the current machine, for a customer changing computer.
C# · .NET 8, WPF, WinForms
using Keyflow;
using var licences = new KeyflowClient(new KeyflowOptions
{
BaseUrl = "https://appkeyflow.com",
ProductSlug = "mon-logiciel",
AppName = "MonLogiciel",
GraceDays = 7,
});
var resultat = await licences.CheckAsync(cleSaisie);
if (!resultat.Allowed) { MessageBox.Show(resultat.Message); return; }
No NuGet package to install: HttpClient and System.Text.Json are part of the runtime. Copy the file into your project.
Reference
Every management route expects the X-API-Key header. Only POST /v1/validate is public.
| Endpoint | Purpose |
|---|---|
POST /v1/auth/signup | Create an account, receive the first API key |
GET /v1/me | Check your token |
POST /v1/products | Declare a product |
GET /v1/products | List your products |
POST /v1/licenses | Issue a key |
GET /v1/licenses | List, filter, paginate |
GET /v1/licenses/{id} | Read one |
PATCH /v1/licenses/{id} | Change limit, expiry, notes |
POST /v1/licenses/{id}/revoke | Revoke, permanent |
POST /v1/licenses/{id}/suspend | Suspend, reversible |
POST /v1/licenses/{id}/restore | Reactivate |
POST /v1/licenses/{id}/reveal | Recover a lost key |
GET /v1/licenses/{id}/activations | Machine tracking |
POST /v1/activations/release | Free a machine |
POST /v1/validate | Validate — public |
Machine tracking
{
"items": [{
"id": "8c652356-…",
"device_fingerprint": "c078bdb4",
"device_label": null, "platform": null, "app_version": null,
"first_seen_at": "2026-08-29T01:25:29.787498Z",
"last_seen_at": "2026-08-29T01:25:29.787498Z",
"last_ip": "127.0.0.1",
"released_at": null, "is_active": true
}],
"total": 1, "limit": 50, "offset": 0
}
device_label, platform and app_version stay null until your software sends them at validation time. The SDKs fill them in.
Error format
{ "error": { "code": "validation_error",
"message": "device_id: String should have at least 8 characters" } }
Usual codes: 401 token missing or invalid, 404 not found, 409 conflict, 422 malformed request, 429 rate exceeded, 402 subscription to settle, 403 plan limit reached.
The last two call for different actions: pay, or delete something.
Rate limits
/v1/validate— 60 requests/minute per key, 300/minute per IP address
The full OpenAPI schema is available in the interactive reference, once signed in.
Worth knowing
- Your customers are never cut off. Even if your Keyflow subscription falls into arrears,
/validatekeeps answering for licences already sold. Only the issuing of new keys is suspended. - Revocation stays open even on a suspended account: a security control never depends on a payment.
- Key format:
XXXX-XXXX-XXXX-XXXX, alphabet withoutI,L,OorUto avoid mistyping. Reading tolerates lowercase, spaces, andOfor0.