Skip to Content
Developer APIPartner API

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/orgs

Scopes: 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/orgs

Scopes: 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}/activate

Scopes: 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}/users

Scopes: partner:users:read
Returns: CursorPage[UserSummary]
Includes both active users and pending invitations.

Invite a user

POST /api/v1/partner/orgs/{org_id}/users

Scopes: 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-password

Scopes: partner:users:manage


Teams

List / create teams

GET /api/v1/partner/orgs/{org_id}/teams POST /api/v1/partner/orgs/{org_id}/teams

Scopes: 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/plans

Scopes: partner:plans:read
Returns plans that are published to this reseller.

Get a subscription

GET /api/v1/partner/orgs/{org_id}/subscription

Scopes: 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:read cannot call POST /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 /users with Idempotency-Key).
  • Reading customer Drive or Sites data via the Partner API (use Drive API with a user-bound PAT).
  • Cross-reseller delegation.