For the complete documentation index, see llms.txt. This page is also available as Markdown.

Real-Time Synchronization: Calls from User Events

If your product is a web service, call SCIM in real time when users are created, updated, or deactivated to keep MaiAgent organization members synchronized with your system at all times. Includes a re

Use this approach when your product is itself a web service with its own user management, such as an ISV SaaS product or an enterprise-built portal, and user creation, updates, and deactivation all occur in your system. The most direct solution is to call the SCIM provisioning API at that moment. This keeps MaiAgent member status synchronized with your system in real time without waiting for a scheduled job.

1. Client

If your product is itself a web service with its own user management, the most direct solution is to call SCIM when a user is created, updated, or deactivated. This keeps member status synchronized with your system in real time. You can use the following client directly. Its four methods correspond to four events. Save the returned MaiAgent member ID in your user table so subsequent updates and deactivation do not require another lookup. This flow has been tested in the MaiAgent dev environment: create → create again and receive the same ID → update the name and role → deactivate → reactivate.

"""maiagent_members.py: Synchronize MaiAgent organization members in real time from your user events (SCIM)."""
import os
import urllib.parse

import requests

USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User"
GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group"
PATCH_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:PatchOp"


class MaiAgentMembers:
    def __init__(self, scim_url=None, token=None):
        self.base = (scim_url or os.environ["MAIAGENT_SCIM_URL"]).rstrip("/")
        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {token or os.environ['MAIAGENT_SCIM_TOKEN']}",
            "Content-Type": "application/scim+json",
            "Accept": "application/scim+json",
        })

    def _request(self, method, path, **kwargs):
        res = self.session.request(method, f"{self.base}{path}", timeout=30, **kwargs)
        res.raise_for_status()
        return res.json() if res.content else None

    def _find(self, resource, attr, value):
        query = urllib.parse.quote(f'{attr} eq "{value}"')
        resources = self._request("GET", f"/{resource}?filter={query}").get("Resources", [])
        return resources[0] if resources else None

    def ensure_group(self, name):
        """Create the role if it does not exist (it will have no permissions, so add them in the admin console). If it already exists (created in the admin console), PATCH it once with the same name to create its SCIM mapping. Return the role ID."""
        group = self._find("Groups", "displayName", name)
        if group is None:
            return self._request("POST", "/Groups", json={"schemas": [GROUP_SCHEMA], "displayName": name})["id"]
        self._request("PATCH", f"/Groups/{group['id']}", json={
            "schemas": [PATCH_SCHEMA],
            "Operations": [{"op": "replace", "path": "displayName", "value": name}],
        })
        return group["id"]

    # --- User events -----------------------------------------------------

    def on_user_created(self, external_id, email, name, groups=()):
        """Return the MaiAgent member ID. Save it in your user table. If a member with the same email already exists, take ownership of that member instead of creating a duplicate."""
        for group in groups:
            self.ensure_group(group)
        payload = self._payload(external_id, email, name, groups)
        existing = self._find("Users", "externalId", external_id) or self._find("Users", "userName", email)
        if existing:
            return self._request("PUT", f"/Users/{existing['id']}", json=payload)["id"]
        return self._request("POST", "/Users", json=payload)["id"]

    def on_user_updated(self, member_id, external_id, email, name, groups=()):
        """Synchronize the complete record (name and role changes). Email cannot be changed through SCIM: MaiAgent ignores a different email passed here. See the event mapping table."""
        for group in groups:
            self.ensure_group(group)
        self._request("PUT", f"/Users/{member_id}", json=self._payload(external_id, email, name, groups))

    def on_user_deactivated(self, member_id):
        """Deactivate instead of deleting on departure or suspension. Conversation and quota records are retained, and API keys are revoked immediately."""
        self._set_active(member_id, False)

    def on_user_reactivated(self, member_id):
        self._set_active(member_id, True)

    # --- Internal --------------------------------------------------------

    def _set_active(self, member_id, active):
        self._request("PATCH", f"/Users/{member_id}", json={
            "schemas": [PATCH_SCHEMA],
            "Operations": [{"op": "replace", "path": "active", "value": active}],
        })

    @staticmethod
    def _payload(external_id, email, name, groups):
        return {
            "schemas": [USER_SCHEMA],
            "userName": email,
            "externalId": external_id,
            "displayName": name,
            "active": True,
            "groups": [{"display": g} for g in groups],
        }

2. Connect the client to your events

For example, add it at the end of a Django signal, Rails callback, or your user service's create, update, and delete methods:

3. Initial rollout: Batch-provision existing users

Event synchronization only processes changes that occur after it is connected. Users who already existed must be provisioned once. You do not need a separate tool—simply run on_user_created for each existing user. It is idempotent: a matching externalId or email takes ownership of the existing account instead of creating a duplicate. It also writes the MaiAgent member ID back to your user table, so subsequent updates and deactivation can use it directly:

  • Speed: At the rate limit, the process handles about one person per second, or 1,000 people in about 20 minutes. Run it as a background task, not inside a request.

  • Restartable: Only users whose maiagent_member_id is still empty are selected. If the process fails, rerun it to continue. Running it again for an existing user simply returns the same ID.

  • Test with a few users first: Add [:5] to process five users, then verify their names and roles under Member Management in the admin console before processing everyone.

  • Order: Complete the backfill before enabling event synchronization. If a user changes during the backfill, call the event synchronization method for that user once after it is enabled; the result will be the same.

  • No-code alternative: Export existing users as a list and run the roster synchronization tool once. Then retrieve each member ID with filter=externalId eq "…" and save it. For large numbers of users, the loop above requires less effort.

4. Practical recommendations

  • Create roles and configure their permissions in the admin console first: Members created through SCIM are not assigned to the default role, and roles created automatically by ensure_group have no permissions. Create each role first, including MaiGPT access, and use the same name so members can use it immediately after they are created.

  • Process events asynchronously and retry failures: Put calls in your task queue, such as Celery, Sidekiq, or SQS, instead of the main request thread. Retry if the SCIM endpoint occasionally returns 429 or 5xx. All four methods are idempotent, so repeated execution has no side effects.

  • Map “delete” in your system to “deactivate” in MaiAgent: Do not actually delete the member. If the user returns, call on_user_reactivated directly; their conversation history will still be available.

  • Reconcile once a day: Events can be missed during deployments or queue failures. Schedule the roster synchronization tool to run once a day as a safeguard. It fills in any missed differences. Both approaches use the same externalId, so they do not conflict.

5. Event mapping table

Event in your system
Call
Result in MaiAgent

Create user

on_user_created

Creates an account (without a password) and member, assigns roles, and returns the member ID

Change name or department

on_user_updated

Synchronizes the complete name and role record

Change email

Cannot be changed through SCIM (the change is ignored)

An administrator must change it in the admin console, or call on_user_deactivated for the old member and then on_user_created with the new email (history does not carry over)

Suspend, delete, or offboard

on_user_deactivated

Deactivates the member: revokes API keys, invalidates role permissions, and causes token issuance to return 404; data is retained

Reinstate

on_user_reactivated

Reactivates the member and restores roles

For error codes and handling instructions, see the common error reference for the roster synchronization tool. Both tools use the same API.

Last updated

Was this helpful?