{
  "name": "zBank API",
  "version": "1.0",
  "base": "http://\u003cserver\u003e:5922",
  "auth": "Website login: player runs /bank code ingame (6-digit one-time code, 5 min), website exchanges it via POST /api/login {code} for the player\u0027s API key. API requests use the key (header X-API-Key, ?key\u003d, or JSON key). A key only sees/moves the owner\u0027s accounts. External sites: let the player paste their key (shown in the web dashboard help panel).",
  "currency": "Coins (integer, long). Exchange rate: 1 diamond \u003d 100 coins (deposit/withdraw only ingame).",
  "accountFee": "Creating an extra account costs 3200 coins (\u003d 32 diamonds), destroyed (no refund on close). Ingame: 32 diamonds from inventory (or 3200 coins from main account). Via API: 3200 coins from default account.",
  "ibanFormat": "ZW + 10 digits, e.g. ZW1002345678. Case-insensitive.",
  "concepts": [
    "Every player auto-gets a default (main) account with an IBAN on first join.",
    "Players can own unlimited extra accounts. Creating one costs 3200 coins (\u003d 32 diamonds), destroyed, no refund. Closing only works for empty, non-default accounts.",
    "One account per player is \u0027active\u0027 (used by ingame commands /bank einzahlen|auszahlen|senden without account argument).",
    "Subscriptions (Abos) are recurring transfers, checked every ~20 seconds by the server scheduler.",
    "ABO MODES: mode\u003drealtime bills every \u003cinterval\u003e SECONDS of real time. mode\u003dmcdays bills every \u003cinterval\u003e MINECRAFT DAYS (1 MC day \u003d 24000 ticks of world game time). Use GET /api/time to read the current day (dayNumber), clock and gameTime and plan billing.",
    "GUTSCHEINE (vouchers): POST /api/gutschein/create locks coins from your account and returns a 6-char code (32-symbol alphabet, no 0/O/1/I/L). Anyone with the code redeems it once via POST /api/gutschein/redeem into their own account. Anti-guessing: redeem is rate-limited to 5 tries/min per IP (HTTP 429). Unused vouchers can be cancelled (refund) via POST /api/gutschein/cancel.",
    "ABO RUNS: runs\u003d-1 means an endless subscription (classic abo). runs\u003dN means exactly N installment payments (Ratenzahlung), then it auto-deactivates. After 3 missed payments (insufficient funds) any abo auto-cancels.",
    "ABO IDENTITY: every abo has a numeric id AND a unique human-readable code like ABO-7X2K9P. Cancel accepts either. Always show the code to users.",
    "NEXT DUE: realtime abos store nextDueEpoch (unix ms), mcdays abos store nextDueTick (world game time). After each billing the next due date is pushed forward from the CURRENT time (no backlog catch-up).",
    "All money movements are stored per account as transactions with types TRANSFER, DEPOSIT, WITHDRAW, SUBSCRIPTION, CREDIT, SYSTEM.",
    "APP KEYS: in addition to the free master key (full access), players can buy app keys for 10000 coins each (destroyed, charged from the linked account). An app key is bound to ONE account: it can read that account and transfer FROM it, nothing else. Master-only endpoints reject app keys with 403.",
    "OAUTH (Anmelden mit zBank, like Sign in with Google): external site shows a button linking to GET /oauth?app\u003dNAME\u0026redirect\u003dURL\u0026state\u003dRANDOM. The player approves on the zBank page (must be logged in there), zBank redirects back to redirect?zbank_code\u003dZB-...\u0026state\u003dRANDOM. The site backend verifies via POST /api/oauth/verify {code} and receives {verified, uuid, name, mainIban}. Codes are single-use, 10 min valid. The site MUST check that the returned state matches the one it sent (CSRF protection) and SHOULD check that redirect URLs it uses are its own."
  ],
  "payButton": {
    "steps": [
      "1. Shop builds the /pay link with its IBAN, amount, reason, redirect and a random state.",
      "2. Player opens it (must be logged in on zBank), picks account, pays.",
      "3. zBank redirects to redirect?status\u003dok\u0026tx\u003dTXID\u0026state\u003dRANDOM (or status\u003dcancelled).",
      "4. Shop backend checks state, then verifies the TX in its own account history via GET /api/history with the SHOP api key."
    ],
    "html": "\u003ca href\u003d\"http://\u003cserver\u003e:5922/pay?to\u003dSHOP_IBAN\u0026amount\u003d50\u0026reason\u003dShop\u0026redirect\u003dhttps://shop.de/paid\u0026state\u003dRANDOM\"\u003eZahle mit zBank\u003c/a\u003e"
  },
  "endpoints": [
    {
      "method": "GET",
      "path": "/api/stats",
      "auth": false,
      "params": {},
      "description": "Public totals: totalAccounts, totalTransactions, totalMoney, totalPlayers, richestBalance, diamondRate, accountFeeCoins, activeAbos."
    },
    {
      "method": "GET",
      "path": "/api/me",
      "auth": true,
      "params": {
        "key": "api key"
      },
      "description": "Player {uuid, name, accounts[]}."
    },
    {
      "method": "GET",
      "path": "/api/accounts",
      "auth": true,
      "params": {
        "iban": "optional single IBAN",
        "key": "api key"
      },
      "description": "Own accounts."
    },
    {
      "method": "GET",
      "path": "/api/history",
      "auth": true,
      "params": {
        "iban": "required",
        "limit": "optional, default 50, max 500",
        "key": "api key"
      },
      "description": "Balance + newest-first transactions."
    },
    {
      "method": "POST",
      "path": "/api/transfer",
      "auth": true,
      "params": {
        "key": "api key",
        "reason": "optional",
        "amount": "coins \u003e 0",
        "from": "own IBAN",
        "to": "any IBAN"
      },
      "description": "Returns {ok, transaction}. Errors: FROM_NOT_FOUND, TO_NOT_FOUND, NO_PERMISSION, INSUFFICIENT_FUNDS, INVALID_AMOUNT, SAME_ACCOUNT."
    },
    {
      "method": "POST",
      "path": "/api/konto/nutzen",
      "auth": true,
      "params": {
        "iban": "own IBAN or name",
        "key": "MASTER key"
      },
      "description": "Sets the active account."
    },
    {
      "method": "POST",
      "path": "/api/gutschein/create",
      "auth": true,
      "params": {
        "amount": "coins \u003e 0",
        "from": "own IBAN (debited+locked at once)",
        "key": "MASTER key or scoped key of FROM account"
      },
      "description": "Creates a 6-char voucher code (e.g. 3AWF35). Returns {ok, id, code, amount}."
    },
    {
      "method": "POST",
      "path": "/api/gutschein/redeem",
      "auth": true,
      "params": {
        "key": "MASTER key",
        "code": "voucher code",
        "to": "own IBAN to credit"
      },
      "description": "Redeems once. Rate-limited (5/min/IP) against guessing. Returns {ok, amount, to}. Errors: NOT_FOUND, ALREADY_USED."
    },
    {
      "method": "GET",
      "path": "/api/gutschein/list",
      "auth": true,
      "params": {
        "key": "MASTER key"
      },
      "description": "Own vouchers (codes hidden once redeemed)."
    },
    {
      "method": "POST",
      "path": "/api/gutschein/cancel",
      "auth": true,
      "params": {
        "code": "voucher code",
        "key": "MASTER key"
      },
      "description": "Cancels unused voucher, refunds source account."
    },
    {
      "method": "POST",
      "path": "/api/konto/schliessen",
      "auth": true,
      "params": {
        "iban": "own IBAN or name",
        "key": "MASTER key"
      },
      "description": "Closes an empty, non-default account. Fee not refunded."
    },
    {
      "method": "POST",
      "path": "/api/iban/create",
      "auth": true,
      "params": {
        "key": "api key",
        "name": "account name"
      },
      "description": "Creates account, charges 3200 coins fee from default account. Returns {ok, account, feeCoins}."
    },
    {
      "method": "POST",
      "path": "/api/abo/create",
      "auth": true,
      "params": {
        "key": "api key",
        "mode": "realtime|mcdays",
        "reason": "optional",
        "interval": "seconds (realtime) or MC days, may be fractional e.g. 0.5",
        "amount": "coins \u003e 0",
        "runs": "optional, -1 \u003d endless abo, N \u003d N installments",
        "from": "own IBAN",
        "to": "any IBAN"
      },
      "description": "Returns {ok, abo} with unique abo.code like ABO-7X2K9P. Source must belong to key owner."
    },
    {
      "method": "GET",
      "path": "/api/abo/list",
      "auth": true,
      "params": {
        "key": "api key"
      },
      "description": "Own subscriptions (active and cancelled), each with id, code, nextDueEpoch/nextDueTick, runsLeft, missed."
    },
    {
      "method": "POST",
      "path": "/api/abo/cancel",
      "auth": true,
      "params": {
        "key": "api key",
        "id": "abo id OR abo code"
      },
      "description": "Deactivates an abo."
    },
    {
      "method": "POST",
      "path": "/api/kredit/create",
      "auth": true,
      "params": {
        "runs": "2-120 rates",
        "key": "api key",
        "amount": "100-50000",
        "mode": "realtime|mcdays",
        "reason": "optional",
        "interval": "sec|MC-days",
        "from": "own IBAN/name for repayments AND payout"
      },
      "description": "Takes a loan: instant CREDIT payout + installment abo to the central bank (5% interest included in rates, rounded up). Max 3 active credits. Returns {ok, abo, perRate, total}."
    },
    {
      "method": "GET",
      "path": "/api/kredit/list",
      "auth": true,
      "params": {
        "key": "api key"
      },
      "description": "Own credits with remaining debt."
    },
    {
      "method": "POST",
      "path": "/api/kredit/payoff",
      "auth": true,
      "params": {
        "id": "abo id or code",
        "from": "optional own IBAN/name",
        "key": "api key"
      },
      "description": "Pays off remaining debt at once."
    },
    {
      "method": "POST",
      "path": "/api/apikey/rotate",
      "auth": true,
      "params": {
        "key": "current MASTER key"
      },
      "description": "Revokes it, returns a fresh master key. Update stored keys!"
    },
    {
      "method": "POST",
      "path": "/api/apikey/create",
      "auth": true,
      "params": {
        "key": "MASTER key",
        "name": "app name",
        "account": "own IBAN to link AND charge"
      },
      "description": "Buys an app key for 10000 coins (destroyed, taken from the linked account). The key only controls that account (read + transfer-from). Returns {ok, id, key, name, accountIban}."
    },
    {
      "method": "GET",
      "path": "/api/apikey/list",
      "auth": true,
      "params": {
        "key": "MASTER key"
      },
      "description": "Own keys with scope."
    },
    {
      "method": "POST",
      "path": "/api/apikey/revoke",
      "auth": true,
      "params": {
        "key": "MASTER key",
        "id": "key id"
      },
      "description": "Revokes one of your keys."
    },
    {
      "method": "GET",
      "path": "/pay",
      "auth": false,
      "params": {
        "reason": "optional",
        "amount": "coins",
        "state": "optional shop state",
        "to": "shop IBAN",
        "redirect": "shop callback URL"
      },
      "description": "Pay-with-zBank page (HTML). Player picks own account, pays via their key, redirected to redirect?status\u003dok\u0026tx\u003dID\u0026state\u003d.. (or status\u003dcancelled). Shop verifies via its own /api/history."
    },
    {
      "method": "GET",
      "path": "/api/time",
      "auth": false,
      "params": {},
      "description": "Public MC time: {gameTime, dayIndex (0-based), dayNumber (1-based), timeOfDay (0-23999 ticks), clock (HH:MM), ticksPerDay: 24000}. Use dayNumber + clock to plan mcdays abos."
    },
    {
      "method": "POST",
      "path": "/api/login",
      "auth": false,
      "params": {
        "code": "6-digit code from ingame /bank code"
      },
      "description": "Exchanges a login code for {ok, key, uuid, name}. Code is single-use, 5 min valid."
    },
    {
      "method": "POST",
      "path": "/api/oauth/approve",
      "auth": true,
      "params": {
        "app": "requesting app name",
        "key": "api key (logged-in player)"
      },
      "description": "Creates one-time OAuth code ZB-... (10 min). Used by the /oauth consent page."
    },
    {
      "method": "POST",
      "path": "/api/oauth/verify",
      "auth": false,
      "params": {
        "code": "ZB-... from redirect"
      },
      "description": "For site backends. Returns {verified, uuid, name, mainIban, app}. Single-use."
    },
    {
      "method": "GET",
      "path": "/oauth",
      "auth": false,
      "params": {
        "redirect": "https URL of the site callback",
        "app": "app name shown to user",
        "state": "random string for CSRF check"
      },
      "description": "Consent page (HTML). Redirects to redirect?zbank_code\u003d..\u0026state\u003d.. on approval."
    },
    {
      "method": "GET",
      "path": "/api/docs",
      "auth": false,
      "params": {},
      "description": "This spec as JSON."
    },
    {
      "method": "GET",
      "path": "/docs",
      "auth": false,
      "params": {},
      "description": "Human-readable docs page."
    }
  ],
  "transactionTypes": [
    "TRANSFER",
    "DEPOSIT",
    "WITHDRAW",
    "SUBSCRIPTION",
    "CREDIT",
    "SYSTEM"
  ],
  "oauthButton": {
    "steps": [
      "1. Site shows the button. state \u003d random per login (store in session).",
      "2. Player clicks, approves on zBank, gets redirected to redirect?zbank_code\u003dZB-...\u0026state\u003dRANDOM123.",
      "3. Site checks state matches, then POSTs {code} to /api/oauth/verify.",
      "4. Site creates its session for {uuid, name, mainIban}. Never expose codes or keys in frontend code."
    ],
    "html": "\u003ca href\u003d\"http://\u003cserver\u003e:5922/oauth?app\u003dMeinShop\u0026redirect\u003dhttps://meinshop.de/zbank-callback\u0026state\u003dRANDOM123\"\u003eAnmelden mit zBank\u003c/a\u003e"
  },
  "examples": {
    "transfer": {
      "method": "POST",
      "path": "/api/transfer",
      "body": {
        "key": "zwb_...",
        "reason": "Shop",
        "amount": 50,
        "from": "ZW1000000001",
        "to": "ZW1000000002"
      }
    },
    "daily_abo_mcdays": {
      "method": "POST",
      "path": "/api/abo/create",
      "body": {
        "key": "zwb_...",
        "mode": "mcdays",
        "reason": "Miete",
        "interval": 1,
        "amount": 10,
        "runs": -1,
        "from": "ZW1000000001",
        "to": "ZW1000000002"
      }
    },
    "hourly_abo_realtime": {
      "method": "POST",
      "path": "/api/abo/create",
      "body": {
        "key": "zwb_...",
        "mode": "realtime",
        "reason": "Zinsen",
        "interval": 3600,
        "amount": 5,
        "runs": 24,
        "from": "ZW1000000001",
        "to": "ZW1000000002"
      }
    }
  },
  "errors": "All errors are {ok:false, error:\u0027...\u0027}. HTTP codes: 200 ok, 400 bad request, 401 bad key, 403 no access, 404 not found, 405 wrong method. CORS is open (*)."
}