Kiges ID API

Complete REST API for integrating Kiges ID authentication and organization management into your SaaS application. Allows your users to authenticate via Kiges ID, select organizations, manage members, roles, and more.

Overview

The Kiges ID API is organized around REST. It uses OAuth 2.0 Bearer tokens for authentication, returns JSON responses, and uses standard HTTP response codes.

Base URL

https://id.kiges.cc/api

Authentication

OAuth 2.0 Bearer token via Authorization header

Content Type

application/json for all request and response bodies

OIDC Discovery

/.well-known/openid-configuration

Authentication

All API endpoints require an OAuth 2.0 access token obtained through the Authorization Code flow with PKCE. Include the token in the Authorization header.

Getting an Access Token

1. Redirect the user to the authorization endpoint:
https://id.kiges.cc/oauth/authorize

2. After the user authorizes, exchange the code at the token endpoint:
https://id.kiges.cc/api/oauth/token

Authorization Code Exchange
POST https://id.kiges.cc/api/oauth/token

Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&redirect_uri=https://yourapp.com/callback
&code=AUTHORIZATION_CODE
&code_verifier=PKCE_CODE_VERIFIER
Token Response
{
    "token_type": "Bearer",
    "expires_in": 31536000,
    "access_token": "eyJ0eXAiOiJKV1Q...",
    "refresh_token": "def502003b4e..."
}

Using the Token

HTTP Header
Authorization: Bearer eyJ0eXAiOiJKV1Q...

Scopes

Request specific scopes during the OAuth authorization to control what data your application can access. Include scopes as a space-separated list in the scope parameter.

ScopeDescriptionGrants Access To
openidOpenID ConnectUser's unique stable identifier (sub)
profileUser profilename, surname, birthday, gender, phone, language
emailEmail addressemail, email_verified
organizationsOrganization listUser's organization memberships in /user and /userinfo
org:readRead org dataOrganization API read endpoints (list, show, members, roles)
org:manageManage orgOrganization API write endpoints (create, update, delete, invite)
Recommended scopes for SaaS integration: openid profile email organizations org:read
Add org:manage only if your SaaS needs to create/modify organizations or members on behalf of the user.
Example Authorization URL
https://id.kiges.cc/oauth/authorize?
    client_id=YOUR_CLIENT_ID
    &redirect_uri=https://yourapp.com/callback
    &response_type=code
    &scope=openid profile email organizations org:read
    &state=random_csrf_token
    &code_challenge=PKCE_CHALLENGE
    &code_challenge_method=S256

Error Handling

The API uses standard HTTP status codes and returns error details in JSON format.

CodeMeaningDescription
200OKRequest succeeded
201CreatedResource was created
204No ContentSuccessful deletion, no response body
400Bad RequestInvalid request parameters
401UnauthorizedMissing or invalid access token
403ForbiddenInsufficient scope or organization permission
404Not FoundResource does not exist
409ConflictResource already exists (duplicate member, etc.)
422UnprocessableValidation error — see errors field
429Too Many RequestsRate limit exceeded
Error Response Example
{
    "error": "insufficient_scope",
    "message": "The access token does not have the 'org:read' scope."
}

// Validation Error (422)
{
    "message": "The given data was invalid.",
    "errors": {
        "name": ["The name field is required."]
    }
}

Rate Limits

API requests are rate-limited to prevent abuse. The default limit is 60 requests per minute per token. Rate limit information is returned in response headers:

HeaderDescription
X-RateLimit-LimitMaximum requests per window
X-RateLimit-RemainingRequests remaining in current window
Retry-AfterSeconds until the rate limit resets (only on 429)

User Context

GET /api/v1/me

Returns the authenticated user's profile along with their organization memberships, roles, and permissions. This is the primary endpoint your SaaS should call after authentication to get everything needed to set up the user's session.

GET /api/v1/me
Scope: openid or profile
+ org:read for organizations
Response 200
{
    "data": {
        "uuid": "550e8400-e29b-41d4-a716-446655440000",
        "name": "John",
        "surname": "Doe",
        "email": "john@example.com",
        "email_verified": true,
        "organizations": [
            {
                "uuid": "org-uuid-1",
                "name": "Acme Corp",
                "slug": "acme-corp",
                "role": "admin",
                "permissions": ["manage_members", "manage_roles", "invite_members", ...],
                "members_count": 42
            }
        ]
    }
}

GET /api/user

Returns user profile fields based on granted scopes. Lighter than /v1/me — returns only what your scopes allow.

GET /api/user

Fields returned depend on scopes: profile returns name, surname, email, birthday, gender, phone, language. organizations / org:read adds the organizations array with role and permissions.

GET /api/userinfo

Standard OpenID Connect UserInfo endpoint. Returns claims based on granted scopes using OIDC standard field names (given_name, family_name, email_verified, etc.).

GET /api/userinfo
Scope: openid

Organizations

List Organizations

Returns all organizations the authenticated user belongs to.

GET /api/v1/organizations
Scope: org:read
Response 200
{
    "data": [
        {
            "uuid": "550e8400-...",
            "name": "Acme Corp",
            "slug": "acme-corp",
            "description": "Our company workspace",
            "is_active": true,
            "members_count": 42,
            "role": "admin",
            "permissions": ["manage_members", "view_members", ...],
            "created_at": "2026-01-15T10:30:00+00:00"
        }
    ]
}

Create Organization

POST /api/v1/organizations
Scope: org:manage

Request Body

FieldTypeRequiredDescription
namestringRequiredOrganization name (max 255 chars)
descriptionstringOptionalDescription (max 1000 chars)
Response 201
{
    "data": {
        "uuid": "new-org-uuid",
        "name": "My Organization",
        "slug": "my-organization",
        "description": "A workspace for my team",
        "created_at": "2026-03-19T12:00:00+00:00"
    }
}

Get Organization

Returns full organization details including roles, members, and your permissions. Requires membership.

GET /api/v1/organizations/{uuid}
Scope: org:read

Path Parameters

ParamDescription
uuidOrganization UUID

Update Organization

PUT /api/v1/organizations/{uuid}
Scope: org:manage
Permission: manage_organization

Request Body

FieldTypeRequiredDescription
namestringOptionalNew name
descriptionstringOptionalNew description

Delete Organization

Permanently deletes an organization. Only the owner can perform this action.

DELETE /api/v1/organizations/{uuid}
Scope: org:manage
Owner only

Returns 204 No Content on success.

Members

List Members

GET /api/v1/organizations/{uuid}/members
Scope: org:read
Response 200
{
    "data": [
        {
            "user_uuid": "user-uuid-1",
            "email": "john@example.com",
            "name": "John",
            "surname": "Doe",
            "role": "admin",
            "role_display_name": "Admin",
            "permissions": ["manage_members", ...],
            "joined_at": "2026-01-20T09:00:00+00:00"
        }
    ]
}

Get Single Member

GET /api/v1/organizations/{uuid}/members/{userUuid}
Scope: org:read

Returns the specified member's profile, role, and permissions within the organization.

Add Member

POST /api/v1/organizations/{uuid}/members
Scope: org:manage
Permission: manage_members

Request Body

FieldTypeRequiredDescription
emailstringRequiredEmail of a registered Kiges ID user
rolestringOptionalRole name to assign (default: member)

Returns 201 with the new member data. Returns 404 if user not found, 409 if already a member.

Update Member Role

PUT /api/v1/organizations/{uuid}/members/{userUuid}
Scope: org:manage
Permission: manage_members

Request Body

FieldTypeRequiredDescription
rolestringRequiredNew role name for the member
The organization owner's role cannot be changed.

Remove Member

DELETE /api/v1/organizations/{uuid}/members/{userUuid}
Scope: org:manage
Permission: remove_members

Returns 204 No Content on success. The organization owner cannot be removed.

Roles

List Roles

GET /api/v1/organizations/{uuid}/roles
Scope: org:read
Response 200
{
    "data": [
        {
            "id": 1,
            "name": "owner",
            "display_name": "Owner",
            "permissions": ["manage_organization", "manage_members", ...],
            "is_system": true,
            "is_default": false,
            "members_count": 1
        }
    ]
}

Default System Roles

RolePermissions
ownerAll permissions
adminAll except manage_organization
memberview_members, invite_members
viewerview_members

Available Permissions

PermissionDescription
manage_organizationUpdate org name, description, settings
manage_membersAdd members, change roles
manage_rolesCreate, edit, delete custom roles
invite_membersSend and manage invitations
remove_membersRemove members from the organization
view_membersView the member list
view_audit_logsAccess the audit log
manage_ssoConfigure SAML SSO settings
manage_scimConfigure SCIM provisioning
manage_billingManage billing and subscriptions

Get Role

GET /api/v1/organizations/{uuid}/roles/{roleId}
Scope: org:read

Create Custom Role

POST /api/v1/organizations/{uuid}/roles
Scope: org:manage
Permission: manage_roles

Request Body

FieldTypeRequiredDescription
namestringRequiredUnique role identifier (will be slugified)
display_namestringRequiredHuman-readable role name
permissionsstring[]RequiredArray of permission strings
Request Example
{
    "name": "project-manager",
    "display_name": "Project Manager",
    "permissions": ["manage_members", "invite_members", "view_members", "view_audit_logs"]
}

Update Role

PUT /api/v1/organizations/{uuid}/roles/{roleId}
Scope: org:manage
Permission: manage_roles
System roles (owner, admin, member, viewer) cannot be modified.

Delete Role

DELETE /api/v1/organizations/{uuid}/roles/{roleId}
Scope: org:manage
Permission: manage_roles

Returns 204 No Content on success. Cannot delete system roles or roles with assigned members.

Invitations

List Invitations

GET /api/v1/organizations/{uuid}/invitations
Scope: org:read
Permission: invite_members
Response 200
{
    "data": [
        {
            "uuid": "inv-uuid-1",
            "email": "jane@example.com",
            "role": "member",
            "role_display_name": "Member",
            "status": "pending",
            "invited_by": "John Doe",
            "expires_at": "2026-03-26T12:00:00+00:00",
            "created_at": "2026-03-19T12:00:00+00:00"
        }
    ]
}

Create Invitation

Sends an email invitation to join the organization. If the email belongs to a registered Kiges ID user, they'll receive a clickable link. The invitation expires after 7 days.

POST /api/v1/organizations/{uuid}/invitations
Scope: org:manage
Permission: invite_members

Request Body

FieldTypeRequiredDescription
emailstringRequiredEmail address to invite
rolestringOptionalRole to assign (default: member)

Cancel Invitation

DELETE /api/v1/organizations/{uuid}/invitations/{invitationUuid}
Scope: org:manage
Permission: invite_members

Only pending invitations can be cancelled.

Permissions & Audit

Check Permissions

Check the authenticated user's permissions within an organization. Useful for your SaaS to determine what UI elements or actions to show.

GET /api/v1/organizations/{uuid}/permissions
Scope: org:read

Query Parameters

ParamTypeDescription
permissionsstringComma-separated list of permissions to check (e.g., manage_members,invite_members)
Response 200
// GET /api/v1/organizations/{uuid}/permissions?permissions=manage_members,manage_billing

{
    "data": {
        "is_member": true,
        "role": "admin",
        "permissions": ["manage_members", "manage_roles", "invite_members", ...],
        "checked": {
            "manage_members": true,
            "manage_billing": false
        }
    }
}

Audit Logs

Retrieve the organization's audit log. Supports filtering and pagination.

GET /api/v1/organizations/{uuid}/audit-logs
Scope: org:read
Permission: view_audit_logs

Query Parameters

ParamTypeDescription
eventstringFilter by event type (e.g., member.added)
fromdateStart date (YYYY-MM-DD)
todateEnd date (YYYY-MM-DD)
per_pageintegerItems per page (max 100, default 25)
pageintegerPage number

Organization Notifications

Route signed events from Kiges services to organization-owned destinations: Telegram, Slack, Discord, Google Chat, Microsoft Teams, generic webhooks, and email. Deliveries are queued on the notifications queue with automatic retries.

7 channels

Telegram, Slack, Discord, Google Chat, Teams, Webhook, Email

Routing rules

Filter by service, team, event wildcard, and minimum severity

Service dispatch

HMAC-signed POST /api/v1/notifications/dispatch for Kiges services

Management API

OAuth-authenticated CRUD for destinations, rules, and delivery logs

Permission: Management endpoints require manage_notifications in the target organization. See ORGANIZATION_NOTIFICATIONS.md for full setup and payload details.

Service Event Dispatch

POST /api/v1/notifications/dispatch

Kiges services dispatch signed notification events. No user OAuth token — use service HMAC headers instead.

Headers

HeaderDescription
X-Kiges-ServiceService code (e.g. kiges-pay)
X-Kiges-TimestampUnix timestamp
X-Kiges-Signature-256sha256=<hmac> over timestamp.raw_body

Body

FieldTypeDescription
organization_uuiduuid requiredTarget organization
service_codestring requiredMust match X-Kiges-Service
eventstring requiredEvent name (supports wildcard rules)
severitystringdebug|info|warning|error|critical
titlestring requiredShort headline
messagestring requiredHuman-readable body
urlurlOptional deep link
metadataobjectOptional structured data

Returns 202 Accepted with matched_destinations and delivery_ids.

Notification Destinations

GET /api/v1/organizations/{uuid}/notification-endpoints
Permission: manage_notifications
POST /api/v1/organizations/{uuid}/notification-endpoints

Create a destination. Supported type values: telegram, slack, discord, google_chat, microsoft_teams, webhook, email. Webhook-based types require webhook_url; email requires emails; generic webhook accepts optional webhook_secret.

POST /api/v1/organizations/{uuid}/notification-endpoints/{endpointUuid}/reconnect

Regenerate a Telegram /kiges_connect token.

Also: GET/PUT/DELETE .../notification-endpoints/{endpointUuid}. Discover channel metadata via GET /api/v1/notification-channel-types.

Notification Rules

GET /api/v1/organizations/{uuid}/notification-rules
POST /api/v1/organizations/{uuid}/notification-rules

Body: name, notification_endpoint_uuid, optional organization_team_id, saas_service_id, events array, minimum_severity.

Notification Deliveries

GET /api/v1/organizations/{uuid}/notification-deliveries

Paginated delivery log. Filter with status and event. Test routing via POST /api/v1/organizations/{uuid}/notifications/test.

Integration Guide

Complete OAuth 2.0 Flow

Here is the full flow for integrating Kiges ID into your SaaS application.

1. Build Authorization URL (JavaScript)
// Generate PKCE challenge
async function generatePKCE() {
    const verifier = crypto.randomUUID() + crypto.randomUUID();
    const encoder = new TextEncoder();
    const data = encoder.encode(verifier);
    const hash = await crypto.subtle.digest('SHA-256', data);
    const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
        .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
    return { verifier, challenge };
}

const { verifier, challenge } = await generatePKCE();
sessionStorage.setItem('pkce_verifier', verifier);

const authUrl = new URL('https://id.kiges.cc/oauth/authorize');
authUrl.searchParams.set('client_id', 'YOUR_CLIENT_ID');
authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/callback');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid profile email organizations org:read');
authUrl.searchParams.set('state', crypto.randomUUID());
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');

window.location.href = authUrl.toString();
2. Exchange Code for Token (PHP/Laravel)
// routes/web.php callback handler
Route::get('/callback', function (Request $request) {
    // Exchange authorization code for access token
    $response = Http::asForm()->post('https://id.kiges.cc/api/oauth/token', [
        'grant_type'    => 'authorization_code',
        'client_id'     => config('services.kiges.client_id'),
        'client_secret' => config('services.kiges.client_secret'),
        'redirect_uri'  => config('services.kiges.redirect'),
        'code'          => $request->code,
    ]);

    if (!$response->ok()) {
        return redirect('/login')->withErrors('Authentication failed');
    }

    $tokens = $response->json();
    $accessToken = $tokens['access_token'];

    // Fetch user profile + organizations in one call
    $userData = Http::withToken($accessToken)
        ->get('https://id.kiges.cc/api/v1/me')
        ->json('data');

    // Create or update user in your database
    $user = User::updateOrCreate(
        ['kiges_uuid' => $userData['uuid']],
        [
            'name'         => $userData['name'],
            'surname'      => $userData['surname'],
            'email'        => $userData['email'],
            'kiges_token'  => $accessToken,
            'kiges_refresh' => $tokens['refresh_token'],
        ]
    );

    // Sync organizations
    foreach ($userData['organizations'] ?? [] as $orgData) {
        $org = Organization::updateOrCreate(
            ['kiges_uuid' => $orgData['uuid']],
            [
                'name' => $orgData['name'],
                'slug' => $orgData['slug'],
            ]
        );
        $org->members()->updateOrCreate(
            ['user_id' => $user->id],
            ['role' => $orgData['role'], 'permissions' => $orgData['permissions']]
        );
    }

    Auth::login($user);
    return redirect('/dashboard');
});
2b. Exchange Code for Token (Node.js/Express)
// Express callback route
app.get('/callback', async (req, res) => {
    const { code, state } = req.query;

    // Exchange code for token
    const tokenRes = await fetch('https://id.kiges.cc/api/oauth/token', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: new URLSearchParams({
            grant_type: 'authorization_code',
            client_id: process.env.KIGES_CLIENT_ID,
            client_secret: process.env.KIGES_CLIENT_SECRET,
            redirect_uri: process.env.KIGES_REDIRECT_URI,
            code,
        }),
    });

    const tokens = await tokenRes.json();

    // Fetch user profile + organizations
    const meRes = await fetch('https://id.kiges.cc/api/v1/me', {
        headers: { Authorization: `Bearer ${tokens.access_token}` },
    });
    const { data: userData } = await meRes.json();

    // Create session with user data + organizations
    req.session.user = {
        uuid: userData.uuid,
        name: userData.name,
        email: userData.email,
        organizations: userData.organizations,
        accessToken: tokens.access_token,
    };

    res.redirect('/dashboard');
});

Organization Switching in Your SaaS

After authentication, the user may belong to multiple organizations. Here's how to implement organization switching in your SaaS.

Frontend: Organization Selector (React)
function OrganizationSwitcher({ organizations, currentOrg, onSwitch }) {
    return (
        <select
            value={currentOrg?.uuid || ''}
            onChange={(e) => onSwitch(e.target.value)}
        >
            {organizations.map(org => (
                <option key={org.uuid} value={org.uuid}>
                    {org.name} ({org.role})
                </option>
            ))}
        </select>
    );
}

// When organization changes, re-fetch data scoped to that org
async function switchOrganization(orgUuid) {
    // Store selected org in session/state
    setCurrentOrg(orgUuid);

    // Fetch org details + members from Kiges ID API
    const orgRes = await fetch(`/api/kiges/organizations/${orgUuid}`);
    const { data } = await orgRes.json();

    // Check the user's permissions to show/hide UI elements
    const canManage = data.your_permissions.includes('manage_members');
    setCanManageMembers(canManage);
}
Backend Proxy: Forward Org Requests (Laravel)
// In your SaaS backend, proxy requests to Kiges ID API
Route::middleware('auth')->prefix('api/kiges')->group(function () {

    Route::get('/organizations', function (Request $request) {
        return Http::withToken($request->user()->kiges_token)
            ->get('https://id.kiges.cc/api/v1/organizations')
            ->json();
    });

    Route::get('/organizations/{uuid}', function (Request $request, $uuid) {
        return Http::withToken($request->user()->kiges_token)
            ->get("https://id.kiges.cc/api/v1/organizations/{$uuid}")
            ->json();
    });

    Route::get('/organizations/{uuid}/members', function (Request $request, $uuid) {
        return Http::withToken($request->user()->kiges_token)
            ->get("https://id.kiges.cc/api/v1/organizations/{$uuid}/members")
            ->json();
    });

    Route::get('/organizations/{uuid}/permissions', function (Request $request, $uuid) {
        $perms = $request->query('permissions', '');
        return Http::withToken($request->user()->kiges_token)
            ->get("https://id.kiges.cc/api/v1/organizations/{$uuid}/permissions", [
                'permissions' => $perms,
            ])
            ->json();
    });
});
Best Practice: Store the user's Kiges ID access token securely in your database. When the user switches organizations in your SaaS, make API calls on their behalf using this token. The Kiges ID API automatically enforces membership and permissions.

Webhooks

Overview

Kiges ID sends real-time HTTP POST notifications (webhooks) to your application whenever important events occur. Webhooks are delivered asynchronously with automatic retries on failure.

Automatic Setup

Configure your webhook URL when creating an OAuth client. A signing secret is auto-generated.

Signed Payloads

Every delivery is signed with HMAC-SHA256 so you can verify authenticity.

Automatic Retries

Failed deliveries retry up to 5 times with exponential backoff (30s, 2m, 10m, 30m, 1h).

Delivery Logs

Full delivery history with status, response, and manual retry via API.

Supported Events

EventTriggered When
user.updatedA user updates their profile (name, email, etc.)
organization.createdA new organization is created
organization.updatedOrganization name or description changes
organization.deletedAn organization is deleted
member.addedA user joins an organization (direct add, invitation, or invite link)
member.updatedA member's role is changed
member.removedA member is removed from an organization
role.createdA new custom role is created
role.updatedA role's permissions or display name changes
role.deletedA custom role is deleted
invitation.createdAn invitation is sent
invitation.acceptedAn invitation is accepted
invitation.declinedAn invitation is declined
invitation.cancelledAn invitation is cancelled by an admin
Event discovery: You can also get the list of all supported events by calling GET /api/v1/webhook-events (authenticated).

Delivery Headers

Each webhook request includes these custom headers:

HeaderDescription
X-Kiges-EventThe event type (e.g., member.added)
X-Kiges-DeliveryUnique delivery UUID (for idempotency)
X-Kiges-Signature-256HMAC-SHA256 signature: sha256=<hex>
X-Kiges-TimestampUnix timestamp of the delivery attempt
User-AgentKigesID-Webhook/1.0
Content-Typeapplication/json

Signature Verification

Every webhook delivery is signed with your client's webhook secret using HMAC-SHA256. Always verify the signature before processing the payload.

The signature is computed as: HMAC-SHA256(timestamp + "." + raw_body, webhook_secret)

PHP Verification
// In your SaaS webhook endpoint
Route::post('/webhooks/kiges', function (Request $request) {
    $secret = config('services.kiges.webhook_secret');
    $timestamp = $request->header('X-Kiges-Timestamp');
    $signature = $request->header('X-Kiges-Signature-256');
    $body = $request->getContent();

    // Verify signature
    $expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);

    if (!hash_equals($expected, $signature)) {
        return response('Invalid signature', 403);
    }

    // Optional: reject old timestamps (prevent replay attacks)
    if (abs(time() - (int) $timestamp) > 300) {
        return response('Timestamp too old', 403);
    }

    $event = $request->header('X-Kiges-Event');
    $deliveryId = $request->header('X-Kiges-Delivery');
    $payload = $request->json();

    // Process the event
    match ($event) {
        'member.added'   => $this->handleMemberAdded($payload),
        'member.removed' => $this->handleMemberRemoved($payload),
        'member.updated' => $this->handleMemberUpdated($payload),
        'organization.updated' => $this->handleOrgUpdated($payload),
        'role.updated'   => $this->handleRoleUpdated($payload),
        default          => null, // Ignore unknown events
    };

    return response('OK', 200);
});
Node.js Verification
const crypto = require('crypto');

app.post('/webhooks/kiges', express.raw({ type: 'application/json' }), (req, res) => {
    const secret = process.env.KIGES_WEBHOOK_SECRET;
    const timestamp = req.headers['x-kiges-timestamp'];
    const signature = req.headers['x-kiges-signature-256'];
    const body = req.body.toString();

    const expected = 'sha256=' + crypto
        .createHmac('sha256', secret)
        .update(timestamp + '.' + body)
        .digest('hex');

    if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
        return res.status(403).send('Invalid signature');
    }

    const event = req.headers['x-kiges-event'];
    const payload = JSON.parse(body);

    switch (event) {
        case 'member.added':
            handleMemberAdded(payload);
            break;
        case 'member.removed':
            handleMemberRemoved(payload);
            break;
        case 'organization.updated':
            handleOrgUpdated(payload);
            break;
        // ... handle other events
    }

    res.status(200).send('OK');
});
Python Verification
import hmac
import hashlib
import time
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = 'your_webhook_secret'

@app.route('/webhooks/kiges', methods=['POST'])
def handle_webhook():
    timestamp = request.headers.get('X-Kiges-Timestamp', '')
    signature = request.headers.get('X-Kiges-Signature-256', '')
    body = request.get_data(as_text=True)

    expected = 'sha256=' + hmac.new(
        WEBHOOK_SECRET.encode(),
        f'{timestamp}.{body}'.encode(),
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(expected, signature):
        abort(403, 'Invalid signature')

    if abs(time.time() - int(timestamp)) > 300:
        abort(403, 'Timestamp too old')

    event = request.headers.get('X-Kiges-Event')
    payload = request.get_json()

    if event == 'member.added':
        handle_member_added(payload)
    elif event == 'member.removed':
        handle_member_removed(payload)
    elif event == 'organization.updated':
        handle_org_updated(payload)

    return 'OK', 200

Payload Examples

member.added

Response Body
{
    "event": "member.added",
    "timestamp": "2026-03-19T14:30:00+00:00",
    "organization": {
        "uuid": "550e8400-e29b-41d4-a716-446655440000",
        "name": "Acme Corp",
        "slug": "acme-corp"
    },
    "member": {
        "user_uuid": "user-uuid-123",
        "email": "john@example.com",
        "name": "John",
        "surname": "Doe",
        "role": "member",
        "permissions": ["view_members", "invite_members"]
    },
    "data": {
        "role": "member",
        "source": "invitation"
    }
}

member.updated (role change)

Response Body
{
    "event": "member.updated",
    "timestamp": "2026-03-19T14:35:00+00:00",
    "organization": {
        "uuid": "550e8400-...",
        "name": "Acme Corp",
        "slug": "acme-corp"
    },
    "member": {
        "user_uuid": "user-uuid-123",
        "email": "john@example.com",
        "name": "John",
        "surname": "Doe",
        "role": "admin",
        "permissions": ["manage_members", "manage_roles", ...]
    },
    "data": {
        "old_role": "member",
        "new_role": "admin"
    }
}

organization.updated

Response Body
{
    "event": "organization.updated",
    "timestamp": "2026-03-19T15:00:00+00:00",
    "organization": {
        "uuid": "550e8400-...",
        "name": "Acme Corporation",
        "slug": "acme-corp",
        "description": "Updated description",
        "is_active": true
    },
    "data": {
        "name": "Acme Corporation"
    }
}

role.created

Response Body
{
    "event": "role.created",
    "timestamp": "2026-03-19T15:10:00+00:00",
    "organization": {
        "uuid": "550e8400-...",
        "name": "Acme Corp",
        "slug": "acme-corp"
    },
    "role": {
        "id": 5,
        "name": "project-manager",
        "display_name": "Project Manager",
        "permissions": ["manage_members", "invite_members", "view_members"]
    }
}

invitation.accepted

Response Body
{
    "event": "invitation.accepted",
    "timestamp": "2026-03-19T16:00:00+00:00",
    "organization": {
        "uuid": "550e8400-...",
        "name": "Acme Corp",
        "slug": "acme-corp"
    },
    "invitation": {
        "uuid": "inv-uuid-123",
        "email": "jane@example.com",
        "role": "member"
    }
}

List Webhook Deliveries

GET /api/v1/organizations/{uuid}/webhooks
Scope: org:read
Permission: manage_organization

Query Parameters

ParamTypeDescription
eventstringFilter by event type (e.g., member.added)
statusstringFilter by status: pending, success, or failed
per_pageintegerItems per page (max 100, default 25)

Get Webhook Delivery

Returns full details of a webhook delivery, including the payload, response body, and error information.

GET /api/v1/organizations/{uuid}/webhooks/{deliveryUuid}
Scope: org:read
Permission: manage_organization
Response 200
{
    "data": {
        "uuid": "del-uuid-123",
        "event": "member.added",
        "url": "https://yourapp.com/webhooks/kiges",
        "payload": { ... },
        "status": "success",
        "status_code": 200,
        "response_body": "OK",
        "error_message": null,
        "attempt": 1,
        "max_attempts": 5,
        "next_retry_at": null,
        "completed_at": "2026-03-19T14:30:01+00:00",
        "created_at": "2026-03-19T14:30:00+00:00"
    }
}

Retry Failed Delivery

Manually retry a failed webhook delivery. Resets the attempt counter and immediately queues a new delivery attempt.

POST /api/v1/organizations/{uuid}/webhooks/{deliveryUuid}/retry
Scope: org:manage
Permission: manage_organization
Response 200
{
    "message": "Webhook delivery queued for retry",
    "data": {
        "uuid": "del-uuid-123",
        "status": "pending"
    }
}

Full Integration Examples

Python / Flask

Python (requests)
import requests

KIGES_API = 'https://id.kiges.cc/api'

def get_user_with_orgs(access_token):
    """Fetch user profile and organizations in one call."""
    r = requests.get(f'{KIGES_API}/v1/me', headers={
        'Authorization': f'Bearer {access_token}',
        'Accept': 'application/json',
    })
    r.raise_for_status()
    return r.json()['data']

def get_org_members(access_token, org_uuid):
    """Fetch all members of an organization."""
    r = requests.get(f'{KIGES_API}/v1/organizations/{org_uuid}/members', headers={
        'Authorization': f'Bearer {access_token}',
        'Accept': 'application/json',
    })
    r.raise_for_status()
    return r.json()['data']

def check_user_permissions(access_token, org_uuid, permissions):
    """Check if the user has specific permissions in an organization."""
    r = requests.get(
        f'{KIGES_API}/v1/organizations/{org_uuid}/permissions',
        params={'permissions': ','.join(permissions)},
        headers={
            'Authorization': f'Bearer {access_token}',
            'Accept': 'application/json',
        }
    )
    r.raise_for_status()
    return r.json()['data']['checked']

# Usage
user = get_user_with_orgs(token)
for org in user['organizations']:
    print(f"Org: {org['name']}, Role: {org['role']}")
    perms = check_user_permissions(token, org['uuid'], ['manage_members', 'manage_billing'])
    print(f"  Can manage members: {perms['manage_members']}")
    print(f"  Can manage billing: {perms['manage_billing']}")

cURL Examples

cURL
# Get user profile + organizations
curl -H "Authorization: Bearer YOUR_TOKEN" \
     https://id.kiges.cc/api/v1/me

# List all organizations
curl -H "Authorization: Bearer YOUR_TOKEN" \
     https://id.kiges.cc/api/v1/organizations

# Get organization details
curl -H "Authorization: Bearer YOUR_TOKEN" \
     https://id.kiges.cc/api/v1/organizations/ORG_UUID

# List members
curl -H "Authorization: Bearer YOUR_TOKEN" \
     https://id.kiges.cc/api/v1/organizations/ORG_UUID/members

# Check specific permissions
curl -H "Authorization: Bearer YOUR_TOKEN" \
     "https://id.kiges.cc/api/v1/organizations/ORG_UUID/permissions?permissions=manage_members,manage_billing"

# Invite a new member
curl -X POST \
     -H "Authorization: Bearer YOUR_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"email":"jane@example.com","role":"member"}' \
     https://id.kiges.cc/api/v1/organizations/ORG_UUID/invitations

# Create a custom role
curl -X POST \
     -H "Authorization: Bearer YOUR_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"name":"editor","display_name":"Editor","permissions":["view_members","invite_members"]}' \
     https://id.kiges.cc/api/v1/organizations/ORG_UUID/roles