Partner API
The Partner API gives MSPs and resellers programmatic control over the organisations they manage — no manual portal work required. It is the primary way to automate customer onboarding, user provisioning, and subscription management.
Base path: /api/v1/partner/
Auth: PAT, Keycloak service account JWT, or first-party admin JWT
Requirement: The calling organisation must be a reseller (reseller_depth >= 1)
Authentication
Use a service account for automated provisioning, or a PAT for scripts.
curl https://api.eunifyer.com/api/v1/partner/orgs \
-H "Authorization: Bearer $TOKEN"Required scopes per operation are listed in each section below.
Organisations
List organisations
GET /api/v1/partner/orgsScopes: partner:orgs:read
Returns: CursorPage[OrgSummary]
Query parameters: q (search name), status (active/suspended), cursor, limit
curl "https://api.eunifyer.com/api/v1/partner/orgs?limit=50&status=active" \
-H "Authorization: Bearer $TOKEN"Create an organisation
POST /api/v1/partner/orgsScopes: partner:orgs:write
Supports: Idempotency-Key
curl -X POST https://api.eunifyer.com/api/v1/partner/orgs \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "Acme Corp",
"slug": "acme-corp",
"billing_email": "billing@acme.example.com",
"plan_slug": "business",
"admin_email": "admin@acme.example.com",
"admin_first_name": "Jane",
"admin_last_name": "Smith"
}'The API provisions the organisation, assigns the plan, and sends an admin invitation. Response is 201 Created with the new org details.
Get an organisation
GET /api/v1/partner/orgs/{org_id}Scopes: partner:orgs:read
Update an organisation
PATCH /api/v1/partner/orgs/{org_id}Scopes: partner:orgs:write
Updatable fields: name, billing_name, billing_email, logo_url, mail_domain
Suspend / activate
POST /api/v1/partner/orgs/{org_id}/suspend
POST /api/v1/partner/orgs/{org_id}/activateScopes: partner:orgs:manage
Suspended orgs cannot log in. All data is retained. Activate to restore access.
Users
List users
GET /api/v1/partner/orgs/{org_id}/usersScopes: partner:users:read
Returns: CursorPage[UserSummary]
Includes both active users and pending invitations.
Invite a user
POST /api/v1/partner/orgs/{org_id}/usersScopes: partner:users:write
Supports: Idempotency-Key
curl -X POST https://api.eunifyer.com/api/v1/partner/orgs/{org_id}/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"email": "alice@acme.example.com",
"first_name": "Alice",
"last_name": "Chen",
"role_slug": "member"
}'An invitation email is sent to the user. Returns 201 Created.
Get / update / delete a user
GET /api/v1/partner/orgs/{org_id}/users/{user_id}
PATCH /api/v1/partner/orgs/{org_id}/users/{user_id}
DELETE /api/v1/partner/orgs/{org_id}/users/{user_id}Scopes: partner:users:read / partner:users:write / partner:users:manage
User lifecycle
POST /api/v1/partner/orgs/{org_id}/users/{user_id}/suspend
POST /api/v1/partner/orgs/{org_id}/users/{user_id}/activate
POST /api/v1/partner/orgs/{org_id}/users/{user_id}/resend-invitation
POST /api/v1/partner/orgs/{org_id}/users/{user_id}/reset-passwordScopes: partner:users:manage
Teams
List / create teams
GET /api/v1/partner/orgs/{org_id}/teams
POST /api/v1/partner/orgs/{org_id}/teamsScopes: partner:teams:read / partner:teams:write
POST supports Idempotency-Key.
Get / update / delete a team
GET /api/v1/partner/orgs/{org_id}/teams/{team_id}
PATCH /api/v1/partner/orgs/{org_id}/teams/{team_id}
DELETE /api/v1/partner/orgs/{org_id}/teams/{team_id}Scopes: partner:teams:read / partner:teams:write / partner:teams:manage
Team members
GET /api/v1/partner/orgs/{org_id}/teams/{team_id}/members
POST /api/v1/partner/orgs/{org_id}/teams/{team_id}/members # { user_id }
DELETE /api/v1/partner/orgs/{org_id}/teams/{team_id}/members/{user_id}Scopes: partner:teams:read / partner:teams:write / partner:teams:manage
Plans & subscriptions
List available plans
GET /api/v1/partner/plansScopes: partner:plans:read
Returns plans that are published to this reseller.
Get a subscription
GET /api/v1/partner/orgs/{org_id}/subscriptionScopes: partner:billing:read
Change plan
POST /api/v1/partner/orgs/{org_id}/subscription/change-plan
{ "plan_slug": "business-plus" }Scopes: partner:plans:write
Change seat count
POST /api/v1/partner/orgs/{org_id}/subscription/change-seats
{ "seats": 25 }Scopes: partner:plans:write
The server enforces a seat floor: you cannot drop below the number of active users + pending invitations.
End-to-end example — provision a new customer
import uuid, httpx
API = "https://api.eunifyer.com/api/v1"
def provision_customer(token: str, plan_slug: str) -> dict:
client = httpx.Client(
base_url=API,
headers={"Authorization": f"Bearer {token}"},
)
# 1. Create the org
org = client.post(
"/partner/orgs",
json={
"name": "Acme Corp",
"slug": "acme-corp",
"billing_email": "billing@acme.example.com",
"plan_slug": plan_slug,
"admin_email": "admin@acme.example.com",
"admin_first_name": "Jane",
"admin_last_name": "Smith",
},
headers={"Idempotency-Key": str(uuid.uuid4())},
).raise_for_status().json()
org_id = org["id"]
# 2. Invite users
users = [
("alice@acme.example.com", "Alice", "Chen"),
("bob@acme.example.com", "Bob", "Müller"),
]
for email, first, last in users:
client.post(
f"/partner/orgs/{org_id}/users",
json={"email": email, "first_name": first, "last_name": last, "role_slug": "member"},
headers={"Idempotency-Key": str(uuid.uuid4())},
).raise_for_status()
# 3. Create a team
team = client.post(
f"/partner/orgs/{org_id}/teams",
json={"name": "Engineering"},
headers={"Idempotency-Key": str(uuid.uuid4())},
).raise_for_status().json()
return {"org": org, "team": team}Isolation guarantees
- A reseller can only manage orgs where their org is the
parent_organization_id. Requests targeting another reseller’s customers return 404 Not Found — not 403. - Scope checks are enforced per operation. A token with
partner:orgs:readcannot callPOST /partner/orgs— it receives 403 Forbidden. - All writes emit audit events (
module="partner") that are visible in the org audit log.
What’s not in v1
- Bulk user import via CSV (use repeated
POST /userswithIdempotency-Key). - Reading customer Drive or Sites data via the Partner API (use Drive API with a user-bound PAT).
- Cross-reseller delegation.