API v1

REST API for your JoinOurServer community. JSON in / JSON out. Bearer authentication via API key.

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

No hard limits in dev mode. In production: 1,000 requests/hour per API key.

Authentication

Every request must include your API key as a Bearer token in the Authorization header:

Authorization: Bearer pk_live_xxxxxxxxxxxxxxxx

You'll find your API key in the partner dashboard under Settings β†’ API & Webhooks. You can also regenerate the key there.

πŸ”‘ Licenses / Serial Keys

Each subscription optionally receives a unique license key (format: PH-XXXX-XXXX-XXXX-XXXX) if the associated plan has the β€žGenerate serial number" option enabled (default: on).

Your own software (e.g. a Minecraft plugin, trading tool, desktop app…) can verify this key against the API to check whether the user has a valid, paid subscription β€” and read additional per-subscription data along the way.

POST /licenses/verify
Verify a license key

Checks a license key against your community. The call is scoped to your partner account β€” keys belonging to other partners get a not_found response.

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 (valid)
{
  "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 is the raw avatar hash Discord assigned to the user. discord_avatar_url is a ready-to-use CDN URL you can drop into an <img src="…"> — animated avatars (hash starting with a_) are served as .gif, everything else as .png. If the user hasn't set a custom avatar, the URL resolves to one of Discord's 6 default avatars.

Response (invalid / expired / cancelled)
{
  "valid": false,
  "status": "canceled",       // or "expired" / "past_due" / "refunded"
  "license_key": "PH-ABCD-EF01-2345-6789",
  ... (same sub/plan/member structure)
}
Response (not found)
HTTP/1.1 404 Not Found
{"valid": false, "error": "not_found"}
Tip: Always check valid === true in your app. The status gives additional context as to why a key may be invalid (expired, canceled, past_due, refunded).
GET /licenses/{license_key}
Lookup license key (alternative)

Identical response to POST /licenses/verify β€” handy when you need a GET-based check (e.g. browser extension).

curl https://api.joinourserver.com/v1/licenses/PH-ABCD-EF01-2345-6789 \
  -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxx"
PATCH /subscriptions/{subscription_id}
Attach custom data to a subscription

For each subscription you can set a free-form text field which is returned with every license verify call. Perfect for user-specific data such as level, permissions, feature flags.

Limit: max 100 characters per subscription. Values longer than 100 characters are rejected with HTTP 400.

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
List all members (with status filter)
Query Parameters
ParameterTypeDescription
pageintegerPage (default 1)
limitintegerPer page (default 20, max 100)
statusstringactive, trial, waitlisted, canceled, expired, past_due, pending, rejected
plan_idstringFilter by 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
  }
}
Note: Sensitive fields such as discord_access_token and discord_refresh_token are never returned in responses.
GET /members/{id}
Fetch a single member
curl https://api.joinourserver.com/v1/members/mem_xxx \
  -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxx"
PATCH /members/{id}
Update a member

Allowed fields: name, status, email, metadata.

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

Subscriptions

A member can have multiple subscriptions in parallel (e.g. different plans or the same plan multiple times). Each subscription has its own license key, status and access expiry.

GET /subscriptions
List subscriptions
Query Parameters
statusstringactive, trial, past_due, canceled, expired
member_idstringAll subs of a member
plan_idstringAll subs of a plan
limitintegerDefault 50, max 100
offsetintegerPagination
curl "https://api.joinourserver.com/v1/subscriptions?status=active" \
  -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxx"
GET /subscriptions/{id}
Single subscription
PATCH /subscriptions/{id}
Update a subscription

Allowed fields:

  • custom_data (string, max 100 chars) — free-form text field, returned on license verify
ℹ️ Cancellations not via the API
Subscription cancellations are deliberately not available via the API. They must be triggered either by the member themselves in the portal or by the partner in the dashboard so that the full Mollie-cancel, Discord role cleanup and audit trail run through cleanly.

Payments

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

Note: Amounts are returned in cents. 2999 = €29.99.

ℹ️ Refunds not via the API
Refunds are deliberately not available via the API. They can only be triggered by the partner in the dashboard under Payments β†’ Refund payment.

Plans

GET /plans
List active plans
curl https://api.joinourserver.com/v1/plans \
  -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxx"

Stats

GET /stats
Community statistics
{
  "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 POSTs events to the webhook URL you configure (Settings β†’ API). Every payload is signed via HMAC-SHA256 using your API key (header X-JoinOurServer-Signature: sha256=<hex>).

Events
member.createdNew member created
member.updatedMember data updated
member.deletedMember deactivated
subscription.canceledSubscription cancelled
payment.refundedPayment refunded
Verify signature (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 key = the API key with which the originating action was triggered (e.g. if your webhook backend makes a POST /members call, we sign the resulting member.created webhook with the same key). For system-internal events the signature falls back to the partner's oldest non-revoked key.

Error Handling

Format
{
  "error": {
    "type": "validation_error",
    "code": "invalid_api_key",
    "message": "Invalid or missing API key"
  }
}
HTTP Status Codes
200OK
201Created
204No Content (e.g. on DELETE)
400Bad Request β€” validation failed
401Unauthorized β€” API key missing or invalid
404Not found
405Method not allowed
500Internal server error
πŸ§ͺ API Live Test
Stored in localStorage.
GET
πŸͺ Cookie settings