API v1

REST API für deine JoinOurServer-Community. JSON in / JSON out. Bearer-Authentifizierung per API-Key.

Base URL
https://api.joinourserver.com/v1
Rate Limits

Keine harten Limits im Dev-Modus. In Production: 1000 requests/hour pro API-Key.

Authentifizierung

Jede Anfrage muss deinen API-Key als Bearer-Token im Authorization-Header mitschicken:

Authorization: Bearer pk_live_xxxxxxxxxxxxxxxx

Dein API-Key findest du im Partner-Dashboard unter Einstellungen → API & Webhooks. Du kannst den Schlüssel dort auch regenerieren.

🔑 Licenses / Serial Keys

Jede Subscription erhält optional einen eindeutigen License-Key (Format: PH-XXXX-XXXX-XXXX-XXXX), wenn der zugehörige Plan die Option „Seriennummer generieren" aktiviert hat (Standard: an).

Deine eigene Software (z.B. ein Minecraft-Plugin, Trading-Tool, Desktop-App…) kann diesen Key gegen die API verifizieren, um zu prüfen ob der User eine gültige, bezahlte Subscription hat — und dabei zusätzliche Daten pro Subscription auslesen.

POST /licenses/verify
Verify a license key

Prüft einen License-Key gegen deine Community. Der Call ist auf deinen Partner-Account geschränkt — Keys anderer Partner werden mit not_found beantwortet.

Request Body
{
  "license_key": "PH-ABCD-EF01-2345-6789"
}
Example Request
curl -X POST https://api.joinourserver.com/v1/licenses/verify \
  -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"license_key":"PH-ABCD-EF01-2345-6789"}'
Response (gültig)
{
  "valid": true,
  "status": "active",
  "license_key": "PH-ABCD-EF01-2345-6789",
  "custom_data": "user_42/level_gold",
  "subscription": {
    "id": "sub_xxx",
    "status": "active",
    "billing_type": "recurring",
    "created_at": "2026-04-14 11:30:00",
    "next_payment_date": "2026-05-14",
    "access_expires_at": null,
    "canceled_at": null
  },
  "plan": {
    "id": "plan_premium",
    "name": "Premium",
    "price": 29.99,
    "interval": "1 month",
    "billing_type": "recurring"
  },
  "member": {
    "id": "mem_xxx",
    "discord_id": "322101351830126594",
    "discord_username": "Chacky",
    "discord_email": "user@example.com",
    "discord_avatar": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
    "discord_avatar_url": "https://cdn.discordapp.com/avatars/322101351830126594/a1b2c3d4e5f60718293a4b5c6d7e8f90.png",
    "name": "Chacky",
    "status": "active"
  }
}

discord_avatar ist der rohe Avatar-Hash, den Discord für den User vergeben hat. discord_avatar_url liefert die fertige CDN-URL als <img src="…">-tauglichen Link — animierte Avatare (Hash beginnt mit a_) werden als .gif ausgeliefert, alle anderen als .png. Hat der User keinen eigenen Avatar gesetzt, zeigt die URL auf eines der 6 Discord-Default-Avatare.

Response (ungültig / abgelaufen / gekündigt)
{
  "valid": false,
  "status": "canceled",       // or "expired" / "past_due" / "refunded"
  "license_key": "PH-ABCD-EF01-2345-6789",
  ... (same sub/plan/member structure)
}
Response (nicht gefunden)
HTTP/1.1 404 Not Found
{"valid": false, "error": "not_found"}
Tipp: Prüfe in deiner App immer valid === true. Der status gibt dir zusätzlich Kontext warum ein Key eventuell ungültig ist (expired, canceled, past_due, refunded).
GET /licenses/{license_key}
Lookup license key (alternative)

Identische Response wie POST /licenses/verify — praktisch wenn du einen GET-basierten Check brauchst (z.B. Browser-Extension).

curl https://api.joinourserver.com/v1/licenses/PH-ABCD-EF01-2345-6789 \
  -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxx"
PATCH /subscriptions/{subscription_id}
Custom-Daten an einer Subscription anhängen

Pro Subscription kannst du ein freies Text-Feld setzen, das bei jeder License-Verify-Anfrage zurückgegeben wird. Perfekt für User-spezifische Daten wie Level, Permissions, Feature-Flags.

Limit: Max 100 Zeichen pro Subscription. Werte länger als 100 Zeichen werden mit HTTP 400 abgelehnt.

Request Body
{
  "custom_data": "tier=gold;seats=5;feature_ai=1"
}
Response
{
  "id": "sub_xxx",
  "custom_data": "tier=gold;seats=5;feature_ai=1",
  "status": "active",
  "license_key": "PH-ABCD-EF01-2345-6789",
  ...
}

Members

GET /members
Liste aller Members (mit Status-Filter)
Query Parameters
ParameterTypBeschreibung
pageintegerSeite (Default 1)
limitintegerPro Seite (Default 20, max 100)
statusstringactive, trial, waitlisted, canceled, expired, past_due, pending, rejected
plan_idstringFilter nach Plan
Example
curl "https://api.joinourserver.com/v1/members?status=active&limit=50" \
  -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxx"
Response
{
  "members": [
    {
      "id": "mem_1234567890abcdef",
      "discord_id": "322101351830126594",
      "discord_username": "Chacky",
      "discord_email": "user@example.com",
      "discord_avatar": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
      "discord_avatar_url": "https://cdn.discordapp.com/avatars/322101351830126594/a1b2c3d4e5f60718293a4b5c6d7e8f90.png",
      "name": "Chacky",
      "status": "active",
      "plan_id": "plan_premium",
      "access_expires_at": null,
      "created_at": "2026-04-14 11:30:00",
      "metadata": {}
    }
  ],
  "pagination": {
    "total": 156,
    "page": 1,
    "per_page": 50,
    "total_pages": 4
  }
}
Hinweis: Sensible Felder wie discord_access_token und discord_refresh_token werden nie in Responses zurückgegeben.
GET /members/{id}
Einzelnen Member abrufen
curl https://api.joinourserver.com/v1/members/mem_xxx \
  -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxx"
PATCH /members/{id}
Member aktualisieren

Erlaubte Felder: name, status, email, metadata.

{
  "status": "active",
  "metadata": {"notes": "VIP customer"}
}

Subscriptions

Ein Member kann mehrere Subscriptions parallel haben (z.B. verschiedene Pläne oder denselben Plan mehrfach). Jede Subscription hat einen eigenen License-Key, Status und Access-Expiry.

GET /subscriptions
Subscriptions auflisten
Query Parameters
statusstringactive, trial, past_due, canceled, expired
member_idstringAlle Subs eines Members
plan_idstringAlle Subs eines Plans
limitintegerDefault 50, max 100
offsetintegerPagination
curl "https://api.joinourserver.com/v1/subscriptions?status=active" \
  -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxx"
GET /subscriptions/{id}
Einzelne Subscription
PATCH /subscriptions/{id}
Subscription aktualisieren

Erlaubte Felder:

  • custom_data (string, max 100 Zeichen) — frei nutzbares Text-Feld, wird bei License-Verify zurückgegeben
ℹ️ Kündigungen nicht über die API
Subscription-Kündigungen sind absichtlich nicht über die API möglich. Sie müssen entweder vom Member selbst im Portal oder vom Partner im Dashboard ausgelöst werden, damit der vollständige Mollie-Cancel, Discord-Rollen-Cleanup und Audit-Trail sauber durchlaufen.

Payments

GET /payments
Zahlungsliste
curl https://api.joinourserver.com/v1/payments \
  -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxx"

Hinweis: Beträge werden in Cent zurückgegeben. 2999 = 29,99 €.

ℹ️ Refunds nicht über die API
Refunds sind absichtlich nicht über die API möglich. Sie können nur vom Partner im Dashboard unter Zahlungen → Zahlung erstatten ausgelöst werden.

Plans

GET /plans
Aktive Pläne auflisten
curl https://api.joinourserver.com/v1/plans \
  -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxx"

Stats

GET /stats
Community-Statistiken
{
  "monthly_revenue_cents": 125000,
  "monthly_revenue": 1250.00,
  "active_members": 42,
  "platform_fee": 31.25,
  "platform_fee_percentage": 2.5,
  "conversion_rate": 68.5
}

Outgoing Webhooks

JoinOurServer POSTet Events an deine konfigurierte Webhook-URL (Settings → API). Jede Payload ist mit HMAC-SHA256 über deinen API-Key signiert (Header X-JoinOurServer-Signature: sha256=<hex>).

Events
member.createdNeuer Member angelegt
member.updatedMember-Daten aktualisiert
member.deletedMember deaktiviert
subscription.canceledSubscription gekündigt
payment.refundedZahlung erstattet
Signatur prüfen (Node.js)
function verifyWebhookSignature(payload, signature, apiKey) {
    const expected = 'sha256=' + crypto
        .createHmac('sha256', apiKey)
        .update(payload)
        .digest('hex');
    return crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expected)
    );
}

HMAC-Schlüssel = der API-Key, mit dem die ursprüngliche Aktion ausgelöst wurde (z.B. wenn dein Webhook-Backend einen POST /members macht, signieren wir den daraus resultierenden member.created-Webhook mit demselben Key). Bei systeminternen Events fällt die Signatur auf den ältesten nicht-widerrufenen Key des Partners zurück.

Error Handling

Format
{
  "error": {
    "type": "validation_error",
    "code": "invalid_api_key",
    "message": "Invalid or missing API key"
  }
}
HTTP Status Codes
200OK
201Created
204No Content (z.B. bei DELETE)
400Bad Request — Validierung fehlgeschlagen
401Unauthorized — API-Key fehlt oder ungültig
404Nicht gefunden
405Methode nicht erlaubt
500Interner Serverfehler
🧪 API Live-Test
Wird im localStorage gespeichert.
GET
🍪 Cookie-Einstellungen