API, MCP és CLI
A KeyPro fiókod gépi felülete: nyilvános REST API (https://keypro.hu/api/v1), MCP szerver és parancssori eszköz. Rendelés leadása és visszamondása, fizetési mód módosítása, rendelések / számlák / termékkulcsok lekérdezése és a profil kezelése - saját szkriptből vagy AI ügynökből (ChatGPT, Claude, Gemini, Codex, OpenCode, Antigravity).
Előre megírt prompt AI ügynöknek
Másold be az ügynöködnek (ChatGPT, Claude, Gemini, Codex, OpenCode). Leírja a hitelesítést, a kötelező előnézet-megerősítés rendelési folyamatot, a termékkép-szerződést, a stabil hibakódokat és a REST hívás módját.
# KeyPro CLI - AI agent guide
Magyar: ez a KeyPro.hu B2B licencshop parancssori eszkoze; az alabbi angol
utmutato AI-agenteknek (ChatGPT, Claude, Gemini, Codex, OpenCode) szol.
## Setup
1. The user needs a KeyPro account (approved reseller) and an API key. Easiest:
`keypro setup` - interactive wizard (asks the server, then API key [default]
or email+password). Alternatives:
- `keypro login` (email + password, mints + stores a fresh key)
- a key made on the website under "API, MCP és CLI" (https://keypro.hu/api), stored via
`keypro config set api-key kp_live_...`, the KEYPRO_API_KEY env var
(recommended for agents), or ~/.config/keypro/config.json
2. API base URL: production is the default. For the dev site use `keypro setup`,
KEYPRO_API_BASE=https://dev.keypro.hu, or `keypro config set api-base ...`.
3. Verify with: `keypro whoami --json`
## Output contract
- Every command supports `--json`: machine-readable data on stdout.
- Errors go to stderr; in --json mode they are JSON with a stable
`error.code` (snake_case English). Key on `code`, not on the Hungarian
`message`.
- Exit codes: 0 success, 1 API/business error, 2 usage error, 3 auth error.
## Ordering flow (IMPORTANT)
Ordering is a two-step preview + confirm flow to prevent accidental orders:
1. `keypro order preview --item SKU=QTY --payment bacs --json`
Returns priced lines, fees, shipping, totals and a `confirmToken`
(valid 15 minutes, bound to items + payment method + gross total).
ALWAYS show the totals to the user before ordering.
2. `keypro order create --item SKU=QTY --payment bacs --yes --json`
Without `--yes` the command only prints the preview and exits with
code 1. With `--yes` it re-runs the preview and submits with the fresh
confirmToken. If prices changed between preview and create, the server
rejects with `confirm_token_invalid` and returns the new totals in
`error.details` - re-run preview and show the user the new total.
3. Retries: pass `--idempotency-key <any-unique-string>` - the same key
never creates a second order (the response has `idempotentReplay: true`).
Payment methods (`--payment`):
- `bacs` bank transfer: order goes on-hold, a proforma invoice
(dijbekero) is issued; keys are delivered after payment arrives.
- `cheque` 8-day payment terms (+5% fee on net product total).
- `cod` cash on delivery (physical shipments only, +1.5 EUR fee).
- `wallet` KEP balance (net total deducted immediately).
- `card` saved bank card (Stripe, off-session). If the bank requires
3DS or there is no saved card, the response contains `payment.paymentUrl`
- give this link to the user to open in a browser (valid ~1 hour).
Select a specific card with `--card pm_...` (see `keypro cards list`).
Physical products need `--shipping gls_hd|gls_parcelshop|combine_free`;
for gls_parcelshop also `--parcelshop <ID>`
(search: `keypro parcelshops search <city|zip>`).
## Variable products (tiers, editions)
Some catalog entries are GROUPS (`type: "variable"`): a family of tiers or
editions. A group is NOT orderable - its listed price is only the MINIMUM of
its variants, so ordering it would undercharge.
- `keypro products get <group>` returns `notPurchasable: true` plus a
`variants` array; every entry has its own `productId`, `sku`,
`attributes` (e.g. device count, pack size) and `yourUnitNetEur`.
`keypro products search` also lists the variants of a group row, but with
FEWER fields (no `yourUnitNetEur`): its `netPriceEur` is the catalog price,
so read the caller's own price from `keypro products get` or the order
preview.
- Order a VARIANT, never the group: `--item <variant-sku>=<qty>` or
`--item id:<variant-productId>=<qty>`.
- If a group slips into an order, the server rejects it with
`variant_required`; `error.details.variants` lists the selectable variants,
so you can retry in a single hop.
- If a SKU matches more than one product, the server rejects it with
`ambiguous_sku` (`error.details.ambiguousSkus`) instead of guessing - pass
the `productId` instead.
## Product images
Every product AND every variant entry carries an `images` array: ordered,
always present (empty when the product has no picture, never null). Each entry
is `{ url, alt, position }`, the url is ABSOLUTE and directly fetchable, and
the FIRST element is the featured image AMONG THE SERVED ONES: a stored image
the shop cannot serve is left out of the array (the server logs it), so on such
a product the next image becomes the first. There is no separate `image` field.
- `keypro products images <sku|id>` prints ONE absolute URL per line and
nothing else, so mirroring the pictures into your own webshop is one line:
`keypro products images 123 | xargs -n1 curl -O`.
With `--json` it returns the structured array.
- A product with no image prints nothing and still exits 0: a missing picture
is not an error, so a `set -e` mirroring loop is never stopped by it.
- `keypro products get <sku|id>` shows the image count and the featured URL;
its `--json` (and that of `products search`) carries the whole array.
- An older shop deployment that does not send the field yet degrades to
"no images"; the CLI never fails on it.
## Queries
- `keypro products search <query>` / `keypro products get <sku|id>` /
`keypro products images <sku|id>`
- `keypro rate` - current EUR/HUF rate the shop uses (net prices are stored in
EUR; HUF price = round(EUR * rate) to whole forint, EUR to 2 decimals)
- `keypro order list [--status <status>]` / `keypro order get <id>`
- `keypro order cancel <id>` - cancel an UNPAID order (bacs / stripe / cod;
NOT 8-day cheque or already-paid orders)
- `keypro order change-payment <id> --payment <method> [--yes]` - change an
unpaid order's payment method (preview first; wallet/stripe move money)
- `keypro keys list [--order <id>]` - delivered license keys
- `keypro invoices list [--order <id>]` / `keypro invoices get <id>`
(each invoice has a public `downloadUrl` PDF link)
- `keypro wallet` / `keypro wallet transactions` - KEP balance + history
- `keypro profile get` / `keypro profile set billing.city=Budapest ...`
(sections: contact.*, billing.*, shipping.*)
- `keypro cards list` - saved cards (add new cards on the website only)
## MCP server mode
Register the CLI as a native MCP server for any MCP client (Claude, ChatGPT, Codex, Gemini, OpenCode, Antigravity):
claude mcp add keypro -- npx -y @keypro/cli mcp
Auth comes from KEYPRO_API_KEY / config; there is no login tool over MCP.
The keypro_order_create tool requires the confirmToken from
keypro_order_preview - same safety flow as the CLI.
## REST API without the CLI
The CLI and the MCP server are thin wrappers over a public REST API, so an
agent that cannot install npm packages can call it directly.
- Base URL: `https://keypro.hu/api/v1` (dev: `https://dev.keypro.hu/api/v1`).
- Auth: `Authorization: Bearer kp_live_...` on every endpoint except
`GET /api/v1` (discovery) and `POST /api/v1/auth/login`.
- Envelope: `{ "ok": true, "data": ... }` or
`{ "ok": false, "error": { "code", "message", "details"? } }`. Key on
`error.code`, never on the Hungarian `message`.
- Scopes: `read`, `orders:write`, `profile:write`. No implicit widening -
every endpoint demands exactly one. The `admin` scope is never issued as an
API key and satisfies nothing under /api/v1.
- Rate limit: 120 requests / minute per key (HTTP 429 + `Retry-After`).
- Paging: ONLY four endpoints take `limit` (1-100) + `offset`:
`GET /products`, `GET /orders`, `GET /invoices`, `GET /wallet`. Only
`GET /products` returns a `total`; on the other three page until the array
is shorter than `limit`. Every OTHER list endpoint (`GET /keys`,
`GET /cards`, `GET /license-keys`, `GET /shipping/parcelshops`,
`GET /orders/{id}/keys`) IGNORES those parameters silently and returns the
whole list at once - never page them, it is an endless loop.
- Money is EUR as a JSON number. EVERY amount is NET, with three named
exceptions: the `...GrossEur` fields plus `displayGrossTotal` and
`feeDeltaEur` are gross, and `taxTotalEur` / `lineTaxEur` / `vatEur` are
the VAT itself, not a gross amount. Dates are ISO 8601 strings.
- Prices are net EUR, and `GET /products` returns CATALOG prices
(`netPriceEur`, `listNetPriceEur`) WITHOUT the caller's contracted
discount. The caller's own unit price is `yourUnitNetEur` on
`GET /products/{key}` (and on its `variants[]`); the BINDING amount is
always the one returned by `POST /orders/preview`.
- Ordering is the same two-step flow as in the CLI:
`POST /orders/preview` -> `confirmToken` (15 min) -> `POST /orders`,
optionally with an `Idempotency-Key` header.
- `images` behaves exactly as described above on every product and variant
object of `GET /products` and `GET /products/{key}`.
Full field-level reference (every endpoint, every response field, every error
code): the `API.md` shipped in this package, and https://keypro.hu/api
## Error codes
unauthorized, forbidden_scope, rate_limited, validation_failed, not_found,
unknown_product, variant_required, ambiguous_sku, coupon_invalid,
shipping_required, invalid_parcelshop, cod_requires_physical,
combine_parent_unavailable, insufficient_wallet_balance,
wallet_payment_disabled, topup_method_not_allowed, same_payment_method,
confirm_required, confirm_token_invalid, invalid_card, stripe_unavailable,
order_not_cancelable, order_not_changeable, account_pending, account_inactive,
invalid_credentials, network_error, internal
`network_error` is produced by the CLI itself (the shop was unreachable); all
the others come from the server.
Ugyanez a szöveg jön a parancssorból is: keypro agent-docs.
KeyPro REST API (/api/v1)
A KeyPro.hu B2B szoftverlicenc-webshop nyilvános REST API-ja. Ugyanazt a felületet szolgálja ki, amit a `@keypro/cli` parancssori eszköz és a beépített MCP szerver használ - a CLI és az MCP nem külön rendszer, hanem ennek az API-nak a burkolója.
- Ha AI ügynököt kötsz be, a CLI/MCP út egyszerűbb:
keypro agent-docs(vagy a csomagban lévőAGENTS.md) leírja a teljes folyamatot. - Ha saját szkriptből, más nyelvből vagy közvetlenül HTTP-vel hívnál, ez a dokumentum a hivatkozás.
Ez a fájl az API leírásának egyetlen forrása: a repóban él, innen kerül a publikus keypro-cli repóba, az npm csomagba és a keypro.hu /api oldalára.
Alap-URL
https://keypro.hu/api/v1
A fejlesztői példány (https://dev.keypro.hu) külön adatbázison fut, saját kulcsokkal, és HTTP Basic Auth mögött áll - éles integrációt ne oda köss.
Minden válasz application/json, UTF-8 - egy kivétellel: ha egy létező úton rossz HTTP metódust hívsz, a keretrendszer válaszol HTTP 405-tel, üres törzzsel és `Content-Type` nélkül, tehát ott nincs error.code, amire kötni lehetne. A törzs feldolgozása előtt mindig nézd meg a státuszkódot.
Hitelesítés
Minden végpont (kettő kivételével) API kulcsot vár az Authorization fejlécben:
Authorization: Bearer kp_live_...
Kulcsot a fiókod [/api](https://keypro.hu/api) oldalán készíthetsz (korábbi neve: /mcp-cli), vagy géppel a POST /api/v1/auth/login végponttal. A nyers kulcs csak a létrehozáskor látszik egyszer; a szerver csak a hasheket tárolja.
Auth nélkül hívható:
| Végpont | Mit ad |
|---|---|
GET /api/v1 | discovery: mi ez az API, hol a dokumentáció |
POST /api/v1/auth/login | email + jelszó → friss API kulcs |
Scope-ok
Egy kulcs egy vagy több jogosultságot kap. Nincs implicit kiterjesztés: az orders:write nem ad read-et és fordítva; minden végpont pontosan egy scope-ot követel.
| Scope | Mit enged |
|---|---|
read | minden olvasó végpont, az előnézetek (POST /orders/preview, POST /orders/{id}/payment/preview) és a fiók bármely API kulcsának visszavonása (DELETE /keys/{id}) |
orders:write | rendelés leadása, visszamondása, fizetési mód módosítása |
profile:write | PATCH /profile |
Az admin scope nem kérhető API kulcsként: a webes kulcskezelő nem is ajánlja fel, és /api/v1 alatt egyetlen végpontot sem elégít ki. Az admin hozzáférés külön felület (admin MCP, OAuth vagy szerveren tárolt token).
> A `read` scope nem ártalmatlan. A GET /keys kilistázza a fiók ÖSSZES > API kulcsának azonosítóját, a DELETE /keys/{id} pedig bármelyiket > visszavonja - nem csak azt, amelyikkel hitelesítettél. Egy read kulcs > tehát le tud tiltani egy orders:write kulcsot, azaz meg tudja bénítani a > fiók összes integrációját. Ha egy harmadik félnek adsz ki kulcsot, ezzel > számolj.
Rate limit
Kulcsonként 120 kérés / 60 másodperc. Túllépésnél HTTP 429, error.code = "rate_limited", Retry-After fejléc, és error.details.retryAfterSeconds.
A bejelentkezés külön, szigorúbb korláton van: 5 kísérlet / perc (IP + email párra) és 20 kísérlet / óra (IP-re).
Válasz-boríték
Minden válasz ugyanaz a két alak közül az egyik.
Siker:
{ "ok": true, "data": { } }Hiba:
{ "ok": false, "error": { "code": "unknown_product", "message": "Nincs ilyen termék ...", "details": {} } }error.codestabil, gépi azonosító (snake_case, angol). Erre köss, ne amessage-re: amessageembernek szóló magyar szöveg, és bármikor változhat.error.detailscsak akkor van jelen, ha az adott hibához tartozik többletadat (lásd a táblázatot).
Hibakódok
code | HTTP | Mikor |
|---|---|---|
unauthorized | 401 | hiányzó, nem Bearer alakú, érvénytelen, visszavont vagy lejárt kulcs |
forbidden_scope | 403 | a kulcsnak nincs meg a végponthoz kellő scope-ja |
rate_limited | 429 | átlépted a percenkénti keretet; details.retryAfterSeconds |
validation_failed | 400 | séma-hiba; details = [{ path, message }], vagy hiányzó törzsadat esetén details.missing |
not_found | 404 | nincs ilyen rendelés / számla / kulcs, vagy ismeretlen végpont |
unknown_product | 400 / 404 | ismeretlen SKU vagy termékazonosító; details.unknownSkus / details.unknownProductIds |
ambiguous_sku | 400 | a SKU több termékre illik; details.ambiguousSkus - adj productId-t |
variant_required | 400 | csoport-terméket próbáltál rendelni; details.variants felsorolja a választható változatokat |
coupon_invalid | 400 | a kuponkód nem érvényes erre a kosárra |
shipping_required | 400 | fizikai tétel szállítási mód nélkül; details.availableMethods |
invalid_parcelshop | 400 | ismeretlen csomagpont-azonosító |
cod_requires_physical | 400 | utánvét csak fizikai kiszállításhoz |
combine_parent_unavailable | 400 | az összevonásra jelölt rendelés már nem alkalmas rá |
confirm_required | 400 | hiányzik a confirmToken (kötelező előnézet) |
confirm_token_invalid | 400 | lejárt vagy nem illő token; details.reason (malformed / expired / mismatch) + details.currentTotals |
insufficient_wallet_balance | 400 | kevés a KEP egyenleg; details.balanceEurNet, details.requiredNetEur |
wallet_payment_disabled | 400 | a KEP egyenleggel fizetés ki van kapcsolva |
topup_method_not_allowed | 400 | egyenlegfeltöltő rendelésre ez a fizetési mód nem megengedett |
same_payment_method | 400 | a rendelés már ezen a fizetési módon van |
invalid_card | 400 | a megadott cardId nem a te mentett kártyád |
stripe_unavailable | 502 | a kártyás fizetés szolgáltatója nem elérhető |
order_not_cancelable | 409 | a rendelés ebben az állapotban nem mondható vissza |
order_not_changeable | 409 | a rendelés fizetési módja már nem módosítható |
account_pending | 403 | a fiók még jóváhagyásra vár (bejelentkezés) |
account_inactive | 403 | a fiók le van tiltva (bejelentkezés) |
invalid_credentials | 401 | rossz email vagy jelszó (bejelentkezés) |
internal | 500 | váratlan szerverhiba |
Lapozás
Négy végpont lapozható, és csak ezek fogadják a limit + offset query paramétert (a válaszban vissza is küldik mindkettőt):
| Végpont | limit alapérték | limit korlát | offset |
|---|---|---|---|
GET /api/v1/products | 50 | 1-100 | 0, >= 0 |
GET /api/v1/orders | 25 | 1-100 | 0, >= 0 |
GET /api/v1/invoices | 25 | 1-100 | 0, >= 0 |
GET /api/v1/wallet | 25 | 1-100 | 0, >= 0 |
`total` mezőt egyedül a `GET /products` ad (a teljes szűrt találathalmaz mérete a lapozás előtt). A másik három lapozható lista darabszámot nem küld: ott addig lapozz, amíg a tömb rövidebb nem lesz a limit-nél.
A többi listázó végpont NEM lapozható, és a limit / offset paramétert CSENDBEN figyelmen kívül hagyja: egyben adja vissza a teljes listát, tehát a fenti "lapozz, amíg rövidebb" recept rajtuk végtelen ciklus lenne (a tömb sosem lesz rövidebb, mert mindig ugyanaz jön vissza).
| Nem lapozható végpont | Amit ad |
|---|---|
GET /api/v1/keys | { keys } |
GET /api/v1/cards | { stripeEnabled, cards } |
GET /api/v1/license-keys | { products } |
GET /api/v1/shipping/parcelshops | { truncated, parcelshops } |
GET /api/v1/orders/{id}/keys | { orderId, orderStatus, keys, licenses } |
A GET /shipping/parcelshops a hosszú találatlistát csonkolja, és ezt a saját truncated: true mezőjével jelzi - ilyenkor szűkíts a q paraméterrel, ne lapozz.
Pénznem, árak, kerekítés
- Minden összeg EUR, JSON
number-ként (nem string), két tizedesre kerekítve. - Alapszabály: minden EUR-összeg NETTÓ. Így nettó a
netPriceEur, alistNetPriceEur, apriceFromEur, apriceToEur, ayourUnitNetEur, azunitNetEur, alineNetEur, anetTotalEur, acartNetEur, ashippingNetEur, acouponDiscountNetEur, acouponDiscountEur, adiscountNetEur, anetEur(díj- és szállítási sorokon), abalanceEurNet, awalletBalanceEurNet, azamountEur, abonusEur, abalanceAfterEur, awalletBalanceAfterEurés arequiredNetEuris. A mezőnév-utótagra ne köss: anetTotalEurés abalanceEurNetsem...NetEurvégű, ataxTotalEurpedig a végénEur, mégsem bruttó. Pontosan három mezőcsoport nem nettó:
| Mező | Mit hoz |
|---|---|
lineGrossEur, grossTotalEur, shipping.grossEur, a payment.fees[] grossEur mezője | bruttó = nettó + ÁFA |
taxTotalEur, lineTaxEur, vatEur | maga az ÁFA (grossTotalEur - netTotalEur), nem bruttó összeg |
displayGrossTotal, feeDeltaEur | bruttó: a displayGrossTotal a kért currency szerinti bruttó végösszeg (HUF-nál egész forint), a feeDeltaEur a bruttó végösszeg változása a fizetési mód váltásakor (előjeles) |
- A
GET /exchange-raterate/eurToHuf/hufToEur/referenceRatemezője nem összeg, hanem szorzó, ayourDiscountPercentés adiscountPercentpedig százalék - ezekre a fenti szabály nem vonatkozik. - A terméklista ára NEM tartalmazza a szerződéses kedvezményedet. A
GET /productsnetPriceEuréslistNetPriceEurmezője a katalógus-ár (akciós, illetve listaár), a hívó személyétől függetlenül. - A TE árad két helyről jön: a
GET /products/{key}yourUnitNetEur+yourDiscountPercentmezőjéről (avariants[]elemein is), 1 db-ra; és aPOST /orders/previewlines[]tömbjénekunitNetEurmezőjéről. - Kötelező érvényű összeg mindig az előnézeté (
POST /orders/preview): csak ott van benne a mennyiségi sáv, a kupon, a fizetési mód díja és a szállítás. Aki a listanetPriceEur-jából árazza a saját ügyfelét, szisztematikusan MAGASABB árat mutat, mint amit a rendelés ténylegesen felszámít. - HUF megjelenítéshez a
GET /exchange-ratevégponton kapott árfolyammal szorozz, és egész forintra kerekíts. - Dátumok ISO 8601 UTC stringek (
createdAt,deliveredAt, ...).
Termékképek - az images szerződés
Ugyanaz az alak minden felületen (terméklista sorai, a lista variants[] elemei, a termék-részletező, annak variants[] elemei):
"images": [
{ "url": "https://keypro.hu/uploads/products/b1224aa9ee630329e0806b459dbccbeee7fc08cf2a8126ca20c7d1602fda0e3d.jpg", "alt": "Office 2024", "position": 0 }
]A szerződés pontjai:
- Az
imagesmindig tömb, soha nem `null`. Kép nélküli terméken üres tömb ([]). images[0]a főkép - a kiszolgálhatók közül. Egy tárolt, de ki nem szolgálható kép kimarad a tömbből (a szerver naplózza), tehát ilyenkor a következő kép lesz az első, és apositionértékek nem feltétlenül folytonosak és nem feltétlenül 0-ról indulnak.- Az
urlabszolút és közvetlenül letölthető, bejelentkezés nélkül. Az URL ALAKJÁRA viszont semmi nem garantált. Ma minden sor a bolt saját képtárára mutat (https://keypro.hu/uploads/products/<sha256>.<kiterjesztés>; 2026-08-09-i mérés: 113 sorból 113), de a legacyhttps://keypro.hu/wp-content/uploads/<év>/<hó>/...alak továbbra is érvényes és bármikor előfordulhat - a bolt tárolhat külső abszolút címet is. Ne szűrj URL-mintára, a kapott címet töltsd le. - Az
altstringvagynull; 300 karakternél hosszabb szöveg csonkolva érkezik. - Nincs külön `image` vagy `imageUrl` skalármező egyik végponton sem.
- A változat (variant) saját képeket kap, nem örökli a csoportét; a termék-részletező
grouphivatkozás-objektuma szándékosan nem hoz képet.
Végpontok
GET /api/v1
Discovery, auth nélkül. data: name, version, docs, auth.
POST /api/v1/auth/login
Auth nélkül. Email + jelszó → friss API kulcs.
Törzs: email (kötelező), password (kötelező), name (kulcs neve, alapértelmezés "CLI login", max 80), scopes (alapértelmezés: mind a három kiadható scope).
data: token (a nyers kulcs - csak itt látszik), keyId, prefix, scopes, name.
Hibák: invalid_credentials, account_pending, account_inactive, rate_limited.
GET /api/v1/me - scope: read
data: id, email, companyName, firstName, role, walletBalanceEurNet, key: { id, prefix, name, scopes }.
GET /api/v1/profile - scope: read
data: { profile }. A profil alakja:
- gyökér:
id,email,role,companyName,taxNumber,firstName,phone,website,noteOnInvoice billing:firstName,lastName,company,address1,address2,city,postcode,state,country,email,phoneshipping: ugyanaz,emailnélkül
PATCH /api/v1/profile - scope: profile:write
Törzs: a lapos mezőnevek (firstName, phone, website, companyName, taxNumber, billingCity, shippingPostcode, ... ) részhalmaza, plusz a noteOnInvoice és licenseDocsIncludeKeys logikai kapcsolók. Üres string ("") → null. A bejelentkezési email itt nem módosítható.
data: updated és a friss profile. Az updated a kérésben ELFOGADOTT mezők neve (amit ismert mezőként küldtél), nem a ténylegesen megváltozott értékeké: ha ugyanazt az értéket küldöd vissza, a mező akkor is szerepel benne. Változás-detektálásra a friss profile-t hasonlítsd a korábbihoz. Ha egyetlen ismert mezőt sem küldtél: validation_failed.
GET /api/v1/products - scope: read
Query: q (max 200), category (kategória-slug, az alkategóriákkal együtt), on_sale (true/false), sort (popularity | name | price_asc | price_desc | newest), include_variants (true/false), limit (alap 50), offset.
data: total, limit, offset, products[]. Egy termék:
id, slug, sku, name, type (simple | variable), groupProductId, listNetPriceEur, netPriceEur, priceFromEur, priceToEur, onSale, isVirtual, isLicensed, fulfillmentType (digital | oem_sticker | key_card | subscription), stock: { status, available, label } (status: always | unknown | unlimited | in_stock | low | out), variantCount, variants[], category: { slug, name } | null, images[].
Az itteni `netPriceEur` / `listNetPriceEur` a KATALÓGUS-ár, a te szerződéses kedvezményed nélkül (lásd a "Pénznem, árak, kerekítés" szakaszt). A te egységárad a GET /products/{key} yourUnitNetEur mezőjén, a kötelező érvényű összeg pedig a POST /orders/preview válaszán jön.
A variants[] mindig jelen van: variable típusú soron a publikált változatokkal, minden más soron üres tömbként - include_variants nélkül is. Elemei: productId, sku, name, attributes, netPriceEur, images[]. Az include_variants=true azt kapcsolja be, hogy a változat-sorok ÖNÁLLÓ találatként is megjelenjenek a products[] tömbben (különben csak a csoport-sor jön).
GET /api/v1/products/{key} - scope: read
A {key} lehet numerikus termékazonosító, slug vagy cikkszám (ebben a sorrendben próbálja).
data: a lista mezőin túl shortDescription, `yourUnitNetEur`, yourDiscountPercent, notPurchasable, variantAttributes, group: { productId, slug, name } | null, és a bővebb variants[] (productId, slug, sku, name, attributes, listNetPriceEur, netPriceEur, onSale, yourUnitNetEur, yourDiscountPercent, isVirtual, fulfillmentType, stock, images[]).
A lista három mezője viszont HIÁNYZIK innen - ez nem a lista bővebb változata, hanem egy másik alak: nincs category, nincs priceFromEur és nincs variantCount. Ha kategória kell, a GET /products soráról vedd; a változatok száma itt a variants.length.
Az itteni `netPriceEur` / `listNetPriceEur` is a KATALÓGUS-ár - ugyanaz, amit a GET /products ad -, és ugyanez áll a variants[] elemeinek netPriceEur / listNetPriceEur mezőjére. A szerződéses kedvezményedet kizárólag a yourUnitNetEur (+ yourDiscountPercent) hordozza, a termék gyökerén és minden változat-soron külön. A rendelésre kötelező érvényű összeg továbbra is a POST /orders/preview válasza.
notPurchasable: true = csoport-termék, közvetlenül nem rendelhető (a listaára a változatai minimuma). Mindig változatot rendelj.
Hiba: unknown_product (404).
POST /api/v1/orders/preview - scope: read
A rendelés kötelező első lépése. Beárazza a kosarat, és kiad egy confirmToken-t.
Törzs: items[] (1-50 elem, elemenként sku vagy productId, plusz qty), paymentMethod (bacs | cheque | cod | wallet | stripe), shippingMethodId (gls_hd | gls_parcelshop | combine_free), parcelshopId, combineWithOrderId, couponCode, currency (EUR | HUF, alap EUR), billing, shipping, taxNumber, internalReference, cardId.
data: lines[] (productId, sku, name, qty, unitNetEur, lineNetEur, lineGrossEur, discountPercent), payment: { method, label, fees: [{ label, netEur, grossEur }] }, shipping: { id, label, netEur, grossEur, parcelshop } | null, coupon: { code, discountNetEur } | null, totals: { cartNetEur, couponDiscountNetEur, shippingNetEur, netTotalEur, taxTotalEur, grossTotalEur }, stock: { lines[], backordered, message }, currency, eurRate, displayGrossTotal, wallet: { balanceEurNet, sufficient } | null, billing, shippingAddress, confirmToken, confirmTokenExpiresAt.
A confirmToken 15 percig él, és a tételekhez + fizetési módhoz + bruttó végösszeghez van kötve.
POST /api/v1/orders - scope: orders:write
Törzs: ugyanaz, mint az előnézeté, plusz a kötelező confirmToken. Opcionális Idempotency-Key fejléc: ugyanazzal a kulccsal soha nem jön létre második rendelés.
A kulcs első 100 karaktere számít, a hosszabbat a szerver hiba nélkül, CSENDBEN levágja. Két különböző kulcs, ami az első 100 karakterében megegyezik (pl. közös prefix + a végén eltérő azonosító), ugyanarra a rendelésre dedupál: a második kérésre nem jön létre új rendelés, HTTP 200 érkezik idempotentReplay: true-val. Használj rövid, ELÖL eltérő kulcsot (pl. UUID-t).
Válasz: HTTP 201 új rendelésnél, 200 idempotens ismétlésnél.
data: order (rendelés-részletező), invoices[], deliveredKeyCount, payment: { method, charged, paymentUrl?, declineCode?, walletBalanceAfterEur?, note }, idempotentReplay.
Ha payment.paymentUrl érkezik (kártya + 3DS vagy nincs mentett kártya), azt a linket kell böngészőben megnyitni; kb. 1 óráig érvényes, utána a rendelés automatikusan `cancelled` státuszba kerül. A rendelés sora MEGMARAD (a GET /orders listában is ott lesz, a GET /orders/{id} továbbra is kiszolgálja) - ne not_found-ra várj.
Ha ár változott az előnézet óta: confirm_token_invalid, és error.details.currentTotals már az új összegeket hozza - futtasd újra az előnézetet.
Fizetési módok:
paymentMethod | Mit jelent |
|---|---|
bacs | átutalás: a rendelés on-hold, díjbekérő készül, kulcs a beérkezés után |
cheque | 8 napos fizetési határidő (+5% díj a nettó termékösszegre) |
cod | utánvét, csak fizikai kiszállításnál (+1,5 EUR) |
wallet | KEP egyenleg, azonnal terhelődik |
stripe | mentett bankkártya (off-session) |
GET /api/v1/orders - scope: read
Query: status (pending, processing, on-hold, completed, cancelled, refunded, failed, prepared-shipping, shipped, under-delivery), limit, offset.
data: limit, offset, orders[]. Egy sor: id, number, status, statusLabel, paymentMethod, paymentMethodLabel, currency, eurRate, netTotalEur, grossTotalEur, couponCode, createdAt, itemNames[].
GET /api/v1/orders/{id} - scope: read
data: order, invoices[], paymentUrl (csak nyitott kártyás fizetésnél, egyébként null).
A rendelés-részletező a lista mezőin túl: items[] (id, productId, name, qty, unitNetEur, lineNetEur, lineTaxEur), billing, shipping, shippingMethod, glsParcelshop, couponDiscountEur, taxNumber, internalReference. (Az itemNames a részletezőn nincs.)
Idegen rendelés not_found-ot ad, nem 403-at.
POST /api/v1/orders/{id}/cancel - scope: orders:write
Kifizetetlen rendelés visszamondása (átutalás / kártya / utánvét; a 8 napos cheque és a már kifizetett rendelés nem). Törzs nincs.
data: order, invoices[], cancelled: true, alreadyCancelled, note. Hiba: order_not_cancelable (409).
POST /api/v1/orders/{id}/payment/preview - scope: read
Törzs: newMethod. data: currentMethod, newMethod, newTotals: { netTotalEur, grossTotalEur }, feeDeltaEur, fees[], confirmToken, confirmTokenExpiresAt, wallet, note.
POST /api/v1/orders/{id}/payment - scope: orders:write
Törzs: newMethod, confirmToken (kötelező), cardId (opcionális, pm_...). data: order, invoices[], payment: { method, status, charged, paymentUrl, declineCode, walletBalanceAfterEur, note }.
Figyelem: a wallet és a stripe irány valódi pénzt mozgat.
GET /api/v1/orders/{id}/keys - scope: read
data: orderId, orderStatus, keys[] (productId, productName, keyValue, activationCount, deliveredAt), licenses[] (licenseServiceId, productId, productName, statusLabel, keyValue, activationCount).
GET /api/v1/license-keys - scope: read
A fiók összes kiszállított termékkulcsa, termékenként csoportosítva.
data: products[] → { productId, productName, keys: [{ keyValue, orderId, orderNumber, deliveredAt }] }.
GET /api/v1/invoices - scope: read
Query: order_id, limit, offset. data: limit, offset, invoices[].
Egy bizonylat: id, orderId, orderNumber, type (proforma | prepayment | final | invoice | delivery_note | correction | storno), typeLabel, number, status (draft | finalized | sent | paid | cancelled), statusLabel, netTotalEur, vatEur, grossTotalEur, downloadUrl, createdAt.
A downloadUrl abszolút, tokennel védett publikus PDF-link (kulcs nélkül is letölthető, a token maga a jogosultság); null, ha még nincs bizonylat-fájl.
GET /api/v1/invoices/{id} - scope: read
data: { invoice }. Idegen bizonylat not_found.
GET /api/v1/keys - scope: read
A fiók API kulcsai. data: { keys: [{ id, prefix, name, scopes, lastUsedAt, expiresAt, revokedAt, createdAt }] }. Nyers tokent soha nem ad vissza.
DELETE /api/v1/keys/{id} - scope: read
Kulcs visszavonása (a sor auditálhatóság miatt megmarad, revokedAt-tel). data: { revoked: true, keyId }. Hiba: not_found (ha az {id} nem a te fiókod kulcsa, vagy már vissza van vonva).
A művelet NEM idempotens: a szerver csak AKTÍV kulcsot talál meg, tehát egy már visszavont kulcs második törlése is 404 not_found. Aki hálózati hiba után újrapróbál, ezt a 404-et sikerként kezelje - a kulcs ilyenkor már nem él.
A fiók BÁRMELY kulcsa visszavonható vele, nem csak az, amelyikkel hívtad - és mivel a GET /keys (szintén read) kilistázza az összes kulcs-azonosítót, egy read scope-ú kulcs le tud tiltani egy orders:write kulcsot is. A visszavonás nem visszafordítható: az új kulcsot a /api oldalon vagy a POST /auth/login végponttal kell kiváltani.
GET /api/v1/wallet - scope: read
Query: limit, offset. data: balanceEurNet, limit, offset, transactions[] (id, type, typeLabel, amountEur (előjeles), bonusEur, balanceAfterEur, orderId, orderNumber, description, createdAt).
A type öt értéket vehet fel - aki négyre írt switch-et, elesik az ötödiken:
type | Mit jelent |
|---|---|
topup | egyenlegfeltöltés |
payment | rendelés kifizetése az egyenlegből |
refund | visszatérítés az egyenlegre |
bonus | feltöltéshez járó bónusz |
adjustment | kézi korrekció (adminisztrátori könyvelés) |
GET /api/v1/cards - scope: read
data: stripeEnabled, cards[] (id, brand, last4, expMonth, expYear, isDefault). Új kártyát csak a weboldalon lehet rögzíteni.
GET /api/v1/exchange-rate - scope: read
data: base (EUR), quote (HUF), rate, eurToHuf, hufToEur, referenceRate, markupPct, source, rounding: { HUF: 0, EUR: 2 }, note.
GET /api/v1/shipping/parcelshops - scope: read
Query: q (város vagy irányítószám, max 200), type (parcel-shop | parcel-locker | all, alap all).
data: truncated, parcelshops[] (id, name, type, postcode, city, address).
Ismeretlen végpont
Bármi más /api/v1 alatt: HTTP 404, error.code = "not_found", auth nélkül is. Ez a nem létező UTAKRA vonatkozik (GET / POST / PATCH / PUT / DELETE metódussal).
Létező úton rossz metódus más: arra a keretrendszer HTTP 405-öt ad, üres törzzsel és Content-Type nélkül - nincs error.code, és a res.json() ott hibára fut. Például GET /api/v1/orders/preview vagy POST /api/v1/products így válaszol. Egy ismeretlen úton az OPTIONS HTTP 204-et ad.
Példa: keresés, előnézet, rendelés
KEY="kp_live_..."
BASE="https://keypro.hu/api/v1"
# 1. termék keresése (a fokep: .products[0].images[0].url)
curl -s -H "Authorization: Bearer $KEY" \
"$BASE/products?q=office&limit=5"
# 2. elonezet - innen jon a confirmToken
curl -s -X POST -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"items":[{"sku":"OFFICE2024","qty":2}],"paymentMethod":"bacs"}' \
"$BASE/orders/preview"
# 3. rendeles a friss tokennel
curl -s -X POST -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: sajat-egyedi-azonosito-1" \
-d '{"items":[{"sku":"OFFICE2024","qty":2}],"paymentMethod":"bacs","confirmToken":"..."}' \
"$BASE/orders"Kapcsolat
Integrációs kérdés: i@keypro.hu - weben: keypro.hu/api
Rendelés előtt az ügynök mindig előnézetet kér (összegekkel), és csak megerősítő tokennel rendel — így véletlen rendelés nem történhet.