Xentree Connect lets a partner platform (an intranet, an HR system, or a learning management system) send its users into Xentree with one click, without a second password. The partner signs a short-lived token with its own private key, the user's browser posts it to Xentree, and Xentree verifies it against the public key the organization administrator registered.How it works#
Connect is a signed-JWT launch with account binding. It is not OIDC, LTI or SAML: there is no authorization-code exchange and no discovery document.1.
Register. An organization administrator registers an integration in Xentree: client ID, issuer, audience and the partner's public keys (JWKS).
2.
Launch. A signed-in user of the partner platform clicks a link. The partner signs an assertion (a JWT) and the browser posts it to POST /connect/v1/launch/{client_id}.
3.
Verify. Xentree checks the signature, issuer, audience, expiry and organization, rejects a reused jti, and stores a one-time ticket that is valid for 90 seconds (300 seconds when consent is needed).
4.
Consent (first launch only). Xentree answers 303 See Other to /connect/consent?ticket=…. The user signs in to their existing Xentree account and agrees. Xentree then binds the partner's sub to that Xentree user.
5.
Session. Xentree claims the ticket and sets HttpOnly session cookies. The user lands on /learn.
6.
Later launches. While the binding is active and the consent text is current, the consent page shows nothing: the ticket is claimed immediately and the user passes straight through in one click.
Xentree does not create user accounts from a launch. The user must already have a Xentree account. The email and name claims, if present, are ignored. Identity comes from the Xentree sign-in during consent.
Membership. If the user is not yet a member of the organization, they are added only when the integration has auto_enroll turned on ("Use a seat automatically on first Connect"). They join as a plain member and, if the integration has a default_team_id, join that team. With auto_enroll off, a non-member is refused with 403.
Seats. With auto_enroll, one seat is taken. On a plan with a seat limit, a full organization is refused with 403 and error: "seat_limit_reached". On a plan with unlimited seats the check always passes.
Revocation. A binding can be revoked by the user, by an organization administrator, or by the partner through the deprovision endpoint. Revoking ends every session issued from that binding.
Entitlement. Connect must be enabled for the organization (connect_entitlement). Otherwise launch and admin calls return 403.
Base URLs#
Launch, consent and deprovision endpoints are served on the host where Xentree is served for the organization: the main application host, or the organization's verified custom domain. A ticket is bound to the host that issued it, and a custom-domain host only accepts integrations of its own organization.Endpoint reference#
POST /connect/v1/launch/{client_id}#
Starts a launch. Called by the user's browser, as a form post. It is not a bearer-token endpoint.| Item | Value |
|---|
| Authentication | The signed assertion in the form body. No Authorization header. |
| Content type | application/x-www-form-urlencoded |
| Rate limit | 120 requests per minute per IP address |
| Name | Description |
|---|
client_id | The Client ID of the registered integration |
| Name | Type | Required | Description |
|---|
assertion | string | Yes | The signed JWT described below |
| Field | Rule |
|---|
alg | RS256 or ES256. Other algorithms are rejected. |
kid | Required. Must match a key in the registered JWKS. |
| Claim | Required | Rule |
|---|
iss | Yes | Exactly equal to the integration's Issuer |
aud | Yes | Exactly equal to the integration's Audience |
sub | Yes | Stable, unique identifier of the user in the partner system. Do not use a value that changes, such as an email address. |
organization_id | Yes | The Xentree organization ID that owns the integration. A different value returns 403. |
iat | Yes | Issue time (seconds since epoch) |
exp | Yes | Expiry (seconds since epoch). Use 60 seconds or less. No clock-skew leeway is applied. |
jti | Yes | Unique per token. A jti can be used once. |
Partner browser Xentree
| POST /connect/v1/launch/{client_id} |
| assertion=<JWT> -------------------------> | verify, store one-time ticket
| <---- 303 Location: /connect/consent?ticket=T
| POST /connect/v1/consent-context {ticket_id} | who is asking, is consent needed
| POST /connect/v1/session/claim |
| {ticket_id, consent} ---------------------> | bind (first time), consume ticket
| <---- 200 {"redirect":"/learn"} + Set-Cookie (HttpOnly access and refresh cookies)
| Status | Meaning |
|---|
303 | Success. Location is /connect/consent?ticket=…. The ticket is opaque and never contains user data. |
401 | Invalid Connect launch assertion: bad signature, unknown kid, wrong iss or aud, missing required claim, or expired |
403 | Connect is not enabled for this organization; Connect assertion organization mismatch; or the integration is not permitted on this custom domain |
404 | Connect client not found: unknown client_id, or the integration is inactive |
409 | Connect launch assertion was already used: the jti was seen before |
429 | Rate limit exceeded |
503 | Replay protection is temporarily unavailable. Retry. |
POST /connect/v1/consent-context#
Used by the consent page. Peeks at a ticket without consuming it.Request body: {"ticket_id": "<ticket>"}. Response: organization_name, policy_url, consent_required, auto_enroll. Errors: 401 (ticket invalid or expired), 403 (ticket belongs to another host).POST /connect/v1/session/claim#
Used by the consent page. Claims the one-time ticket and issues the session cookies.Request body: {"ticket_id": "<ticket>", "consent": true}.| Status | Meaning |
|---|
200 | {"redirect": "/learn"} and HttpOnly session cookies |
401 | Ticket invalid, expired or already claimed; or the user is not signed in to Xentree when consent is needed |
403 | The user is not an organization member and auto_enroll is off; the ticket belongs to another host; or seat_limit_reached |
409 | consent was not granted when it is required, or the identity binding is already active |
POST /connect/v1/deprovision/{client_id}#
Offboarding from the partner platform. Called server to server by the partner.Content type application/x-www-form-urlencoded, form field assertion.
The assertion follows the same rules as launch and must also carry "purpose": "deprovision". A launch assertion cannot be replayed to remove a user.
Idempotent. It revokes the user's binding, ends every session issued from it, and emits a connect.consent.revoked webhook event.
If the user's role is plain member and no other active binding connects them to the organization, they are also removed from the organization and its teams, and the seat is released. Administrators and managers are never removed by an external system.
| Status | Meaning |
|---|
200 | `{"status": "...", "seat_released": true |
401 | Invalid assertion |
403 | Assertion purpose is not deprovision, or organization mismatch |
404 | Unknown client_id or inactive integration |
409 | jti already used |
User and administrator binding endpoints#
| Method | Path | Authentication | Purpose |
|---|
| GET | /connect/v1/me/bindings | Bearer token | The signed-in user's active connections |
| DELETE | /connect/v1/me/bindings/{binding_id} | Bearer token | The user withdraws their own connection (204) |
| DELETE | /connect/v1/organizations/{organization_id}/bindings/{binding_id} | Bearer token, organization administrator | Revoke a user's connection (204). Add ?remove_member=true to also release the seat of a plain member. |
Administrator endpoints#
Base path: /api/admin/organizations/{org_id}/connect-clients. All calls need a bearer token of an organization administrator, and the organization needs the Connect entitlement (403 otherwise).| Method | Path | Purpose |
|---|
| GET | / | List integrations |
| POST | / | Register an integration (201) |
| GET | /{client_record_id} | Read an integration, including its public JWKS |
| PATCH | /{client_record_id} | Update fields. Sending jwks_json rotates the keys. |
| POST | /{client_record_id}/toggle-active | Activate or deactivate. A deactivated integration refuses new launches. |
| GET | /{client_record_id}/bindings | Users currently connected through this integration (up to 500) |
client_record_id is the id returned by the API, not the client_id string.| Field | Type | Required | Description |
|---|
name | string (1 to 255) | Yes | Display name |
client_id | string (1 to 128) | Yes | Public identifier, unique across Xentree. Used in the launch URL. |
issuer | string (1 to 255) | Yes | Must equal the assertion iss |
audience | string (1 to 255) | Yes | Must equal the assertion aud |
jwks_json | string | Yes | The partner's public key set, as JSON text. See below. |
description | string | No | Free text |
policy_url | string (max 512) | No | Link to the partner's data policy, shown on the consent screen. Only absolute http or https URLs are shown. |
auto_enroll | boolean | No (default false) | "Use a seat automatically on first Connect". When true, a user who consents and is not yet a member joins the organization and takes a seat. |
default_team_id | string | No | A team of the same organization. Users added by auto_enroll join it. |
Errors: 400 (jwks_json is not valid JSON, has no keys array or no keys; or default_team_id is not a team of this organization), 403 (not an administrator, or Connect not enabled), 409 (client_id already exists), 503 (CONNECT_ADMIN_DATABASE_UNAVAILABLE).Partner webhooks#
Each integration can subscribe up to 3 HTTPS endpoints to signed events (erpi.session.completed, connect.consent.revoked). Base path: /api/admin/organizations/{org_id}/connect-clients/{client_record_id}/webhooks.| Method | Path | Purpose |
|---|
| GET | /events | Event catalog |
| GET, POST | / | List, create. The signing secret is shown once, in the create response. |
| GET, PATCH, DELETE | /{webhook_id} | Read, update, delete |
| POST | /{webhook_id}/reactivate | Re-enable after failures |
| POST | /{webhook_id}/rotate-secret | New secret. The previous one stays valid for 24 hours. |
| POST | /{webhook_id}/test | Send a test ping (202) |
| GET | /{webhook_id}/deliveries | Delivery log |
| GET | /{webhook_id}/deliveries/{delivery_id} | One delivery |
| POST | /{webhook_id}/deliveries/{delivery_id}/replay | Send a delivery again |
Receiving endpoints must use HTTPS on port 443 with a public domain name. Redirects count as failures.Integration guide for partner developers#
1. Create a key pair#
Generate an RSA 2048 (or EC P-256) key pair. Keep the private key on your server. Never send it to Xentree.2. Publish the public key as a JWKS#
Give the Xentree organization administrator a JSON Web Key Set that holds public keys only. Every key must have a kid; the token header kid must match it.{
"keys": [
{
"kty": "RSA",
"kid": "partner-2026-10",
"use": "sig",
"alg": "RS256",
"n": "<base64url modulus>",
"e": "AQAB"
}
]
}
{
"keys": [
{
"kty": "EC",
"kid": "partner-ec-2026-10",
"use": "sig",
"alg": "ES256",
"crv": "P-256",
"x": "<base64url x>",
"y": "<base64url y>"
}
]
}
Rules: the object must contain a non-empty keys array, and Xentree reads only the pasted JSON. It does not fetch a jwks_uri. To rotate, add the new key (keep the old one until all in-flight launches finish) and send the new JSON with a PATCH or Rotate JWKS in the administrator UI.3. Agree on the values#
| Value | Where it goes |
|---|
client_id | In the launch URL, and registered in Xentree |
issuer | Assertion iss, and registered in Xentree |
audience | Assertion aud, and registered in Xentree |
| Organization ID | Assertion organization_id (ask your Xentree administrator) |
4. Python (Flask)#
5. Node.js (Express)#
6. Deprovision a user#
Sign a second assertion with the same claims plus "purpose": "deprovision" and post it from your server:Checklist#
Sign with RS256 or ES256, and include the kid header.
iss, aud and organization_id match the registration exactly.
sub is stable and unique per user.
exp is at most a minute away, and every token has a fresh jti.
The launch is a form POST from the user's browser, never a GET link and never a call from your server.
Your server clock is synchronized. No skew is tolerated.
Every user who launches already has a Xentree account.
Modified at 2026-10-24 07:50:45