Vissza az irányítópultra

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égpontMit ad
GET /api/v1discovery: mi ez az API, hol a dokumentáció
POST /api/v1/auth/loginemail + 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.

ScopeMit enged
readminden 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:writerendelés leadása, visszamondása, fizetési mód módosítása
profile:writePATCH /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.code stabil, gépi azonosító (snake_case, angol). Erre köss, ne a message-re: a message embernek szóló magyar szöveg, és bármikor változhat.
  • error.details csak akkor van jelen, ha az adott hibához tartozik többletadat (lásd a táblázatot).

Hibakódok

codeHTTPMikor
unauthorized401hiányzó, nem Bearer alakú, érvénytelen, visszavont vagy lejárt kulcs
forbidden_scope403a kulcsnak nincs meg a végponthoz kellő scope-ja
rate_limited429átlépted a percenkénti keretet; details.retryAfterSeconds
validation_failed400séma-hiba; details = [{ path, message }], vagy hiányzó törzsadat esetén details.missing
not_found404nincs ilyen rendelés / számla / kulcs, vagy ismeretlen végpont
unknown_product400 / 404ismeretlen SKU vagy termékazonosító; details.unknownSkus / details.unknownProductIds
ambiguous_sku400a SKU több termékre illik; details.ambiguousSkus - adj productId-t
variant_required400csoport-terméket próbáltál rendelni; details.variants felsorolja a választható változatokat
coupon_invalid400a kuponkód nem érvényes erre a kosárra
shipping_required400fizikai tétel szállítási mód nélkül; details.availableMethods
invalid_parcelshop400ismeretlen csomagpont-azonosító
cod_requires_physical400utánvét csak fizikai kiszállításhoz
combine_parent_unavailable400az összevonásra jelölt rendelés már nem alkalmas rá
confirm_required400hiányzik a confirmToken (kötelező előnézet)
confirm_token_invalid400lejárt vagy nem illő token; details.reason (malformed / expired / mismatch) + details.currentTotals
insufficient_wallet_balance400kevés a KEP egyenleg; details.balanceEurNet, details.requiredNetEur
wallet_payment_disabled400a KEP egyenleggel fizetés ki van kapcsolva
topup_method_not_allowed400egyenlegfeltöltő rendelésre ez a fizetési mód nem megengedett
same_payment_method400a rendelés már ezen a fizetési módon van
invalid_card400a megadott cardId nem a te mentett kártyád
stripe_unavailable502a kártyás fizetés szolgáltatója nem elérhető
order_not_cancelable409a rendelés ebben az állapotban nem mondható vissza
order_not_changeable409a rendelés fizetési módja már nem módosítható
account_pending403a fiók még jóváhagyásra vár (bejelentkezés)
account_inactive403a fiók le van tiltva (bejelentkezés)
invalid_credentials401rossz email vagy jelszó (bejelentkezés)
internal500vá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égpontlimit alapértéklimit korlátoffset
GET /api/v1/products501-1000, >= 0
GET /api/v1/orders251-1000, >= 0
GET /api/v1/invoices251-1000, >= 0
GET /api/v1/wallet251-1000, >= 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égpontAmit 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, a listNetPriceEur, a priceFromEur, a priceToEur, a yourUnitNetEur, az unitNetEur, a lineNetEur, a netTotalEur, a cartNetEur, a shippingNetEur, a couponDiscountNetEur, a couponDiscountEur, a discountNetEur, a netEur (díj- és szállítási sorokon), a balanceEurNet, a walletBalanceEurNet, az amountEur, a bonusEur, a balanceAfterEur, a walletBalanceAfterEur és a requiredNetEur is. A mezőnév-utótagra ne köss: a netTotalEur és a balanceEurNet sem ...NetEur végű, a taxTotalEur pedig a végén Eur, mégsem bruttó. Pontosan három mezőcsoport nem nettó:
MezőMit hoz
lineGrossEur, grossTotalEur, shipping.grossEur, a payment.fees[] grossEur mezőjebruttó = nettó + ÁFA
taxTotalEur, lineTaxEur, vatEurmaga az ÁFA (grossTotalEur - netTotalEur), nem bruttó összeg
displayGrossTotal, feeDeltaEurbruttó: 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-rate rate / eurToHuf / hufToEur / referenceRate mezője nem összeg, hanem szorzó, a yourDiscountPercent és a discountPercent pedig százalék - ezekre a fenti szabály nem vonatkozik.
  • A terméklista ára NEM tartalmazza a szerződéses kedvezményedet. A GET /products netPriceEur és listNetPriceEur mező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 + yourDiscountPercent mezőjéről (a variants[] elemein is), 1 db-ra; és a POST /orders/preview lines[] tömbjének unitNetEur mező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 lista netPriceEur-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-rate vé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:

  1. Az images mindig tömb, soha nem `null`. Kép nélküli terméken üres tömb ([]).
  2. 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 a position értékek nem feltétlenül folytonosak és nem feltétlenül 0-ról indulnak.
  3. Az url abszolú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 legacy https://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.
  4. Az alt string vagy null; 300 karakternél hosszabb szöveg csonkolva érkezik.
  5. Nincs külön `image` vagy `imageUrl` skalármező egyik végponton sem.
  6. A változat (variant) saját képeket kap, nem örökli a csoportét; a termék-részletező group hivatkozá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, phone
  • shipping: ugyanaz, email né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:

paymentMethodMit jelent
bacsátutalás: a rendelés on-hold, díjbekérő készül, kulcs a beérkezés után
cheque8 napos fizetési határidő (+5% díj a nettó termékösszegre)
codutánvét, csak fizikai kiszállításnál (+1,5 EUR)
walletKEP egyenleg, azonnal terhelődik
stripementett 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:

typeMit jelent
topupegyenlegfeltöltés
paymentrendelés kifizetése az egyenlegből
refundvisszatérítés az egyenlegre
bonusfeltöltéshez járó bónusz
adjustmentké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.