1. Partner API v2
  2. Guides
  3. Onboard a client
Guide

Onboard a client

Create a client, choose its settings, bring in its people and admins, all through the API.

This guide takes one client from nothing to running, with nobody opening the INFIMA dashboard: create it, choose its settings, bring in its people, set up training, and hand it its admins. Every step is the same action as a button in the dashboard and lands in the same place, so a client set up here looks exactly like one set up by hand, and your team can pick it up in the dashboard at any point.

Before you start

You need an API key with the write scope and access to all clients: creating a client needs it, and every later call needs the new client in the key's access. Create keys in the dashboard under Settings › Integrations › API Keys.

The examples use curl against the production base URL with the key in $INFIMA_KEY:

SHELL
export INFIMA_KEY="ak_prod_..."
export BASE="https://app.infimasecapis.com/v2"

Three habits make an onboarding script safe to run again:

  • Send an Idempotency-Key on every POST (your own id for the step, such as psa-10482-create). A retry within 24 hours gets the first response back instead of doing the work twice.
  • Set partner-wide defaults once in the dashboard's partner Settings, and send a client only what it does differently. Anything a client doesn't set follows your partner settings, and keeps following them when you change them later.
  • Read the details of a 400: it names every field that is wrong (training.courses_per_year), all at once, and nothing was saved.

1. Create the client

POST /clients with the client's name. external_id is your own id for it (a PSA company id) and comes back on every read; industry sets its benchmark peers.

SHELL
curl -X POST "$BASE/clients" \
  -H "X-API-Key: $INFIMA_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: psa-10482-create" \
  -d '{"name": "Acme Manufacturing", "industry": "manufacturing", "external_id": "PSA-10482"}'

The response (201) is the new client:

JSON
{
  "id": "3f2a9c1e-7b44-5d2e-9a1f-0c6d8e4b2a10",
  "name": "Acme Manufacturing",
  "industry": "manufacturing",
  "external_id": "PSA-10482",
  "created_at": "2026-11-10T18:24:03.200884Z",
  "updated_at": "2026-11-10T18:24:03.288568Z",
  "users": {
    "active": 0,
    "inactive": 0
  },
  "training": {
    "overdue_users": 0
  },
  "phishing": {},
  "frameworks": [
    "foundations"
  ],
  "features": {
    "dark_web_monitoring": true,
    "directory_sync": null
  }
}

Keep id: it is {client_id} in everything below (export CLIENT=3f2a9c1e-…). The client starts on the Security Awareness Foundations program (foundations), as one created in the dashboard does.

  • A name another active client of yours already has returns 409. Names are compared ignoring case.
  • A partner account still on a trial with an agreement waiting returns 409 until the agreement is accepted in the dashboard.

2. Choose its settings

Each of these is optional: skip any where the client follows your partner settings.

Phishing, training and Dark Web. PATCH /clients/{client_id}/settings changes only the fields you send. Send client_override: null to make the client follow your partner setting again.

SHELL
curl -X PATCH "$BASE/clients/$CLIENT/settings" \
  -H "X-API-Key: $INFIMA_KEY" -H "Content-Type: application/json" \
  -d '{
    "phishing": {"banner": "mini", "external_sender_tag": {"client_override": true}},
    "training": {"courses_per_year": 6, "grace_days": 21},
    "dark_web": {"enabled": true}
  }'

The response is the client's whole settings resource after the change; GET /clients/{client_id}/settings reads it at any time. One exception to "any time": phishing.interval_days is kept with the client's users, so a client with none yet returns 409. Send it after step 3.

Phishing categories and templates. GET /clients/{client_id}/phishing/categories lists every category with whether it is on for this client and why (source: client, partner or default). Turn one off, or back to your partner setting with null:

SHELL
curl -X PUT "$BASE/clients/$CLIENT/phishing/categories/social_media" \
  -H "X-API-Key: $INFIMA_KEY" -H "Content-Type: application/json" \
  -d '{"enabled": false}'
JSON
{
  "key": "social_media",
  "name": "Social Media",
  "description": "LinkedIn, Facebook, and other social platforms.",
  "enabled": false,
  "source": "client",
  "client_override": false
}

Some categories start off until you opt in (hr_benefits); send true to turn one on.

A single template works the same way with PUT /clients/{client_id}/phishing/templates/{template_id}, using the id from GET /clients/{client_id}/phishing/templates. Template ids contain /, so encode them as %2F in the path (behavior%2Fverify-oob%2Fceo-urgent-wire). The choice covers every translation of the template:

JSON
{
  "id": "behavior/verify-oob/ceo-urgent-wire",
  "enabled": false,
  "source": "client",
  "client_override": false
}

Monthly reports. PUT /clients/{client_id}/reports/config sets which reports the client's monthly email carries (reports: null follows your partner selection), and PUT /clients/{client_id}/recipients/monthly_report who gets it. Recipients are people from the client's directory (user_id, once step 3 has synced them) or typed addresses with delivery: "email":

SHELL
curl -X PUT "$BASE/clients/$CLIENT/recipients/monthly_report" \
  -H "X-API-Key: $INFIMA_KEY" -H "Content-Type: application/json" \
  -d '{"delivery": "email", "recipients": [{"address": "it-lead@acme-mfg.com"}]}'

Breach alert recipients are the same call with breach_notification.

Sender, Report Phishing Button and onboarding. PATCH /clients/{client_id}/branding sets the name and address the client's training emails come from. PATCH /clients/{client_id}/report-phishing-button sets where the client's reported emails go. PUT /clients/{client_id}/onboarding/selections picks the onboarding modules new users take, by id from the matching GET.

3. Add its people

Most clients sync from Microsoft 365 or Google Workspace. Use the manual path for a client without either, or for a few people outside the directory.

From the client's directory

Connecting a directory needs one click from the client's own IT admin, who consents on Microsoft's or Google's page. The API gets you everything around that click.

1. Create an invite link with POST /clients/{client_id}/sync/invites and send its url to the client's IT. The link works once, needs no INFIMA login and expires after 30 days.

SHELL
curl -X POST "$BASE/clients/$CLIENT/sync/invites" \
  -H "X-API-Key: $INFIMA_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: psa-10482-sync-invite" \
  -d '{"provider": "microsoft"}'
JSON
{
  "id": "201",
  "provider": "microsoft",
  "url": "https://dashboard.infimasec.com/sync-invite/…",
  "status": "pending",
  "created_at": "2026-11-10T18:24:50.976888Z",
  "expires_at": "2026-12-10T18:24:50.976888Z",
  "used_at": null,
  "revoked_at": null
}

2. Wait for the click. GET /clients/{client_id}/sync shows configured: true once IT has connected the directory, and GET /clients/{client_id}/sync/invites shows the invite as used. Checking once an hour is plenty. A link that went to the wrong person is revoked with DELETE /clients/{client_id}/sync/invites/{invite_id}.

3. Choose who syncs with PUT /clients/{client_id}/sync/groups. Microsoft 365 groups are named by object id; Google Workspace groups by email address, or organizational units by path in org_units. An empty list syncs everyone in the directory.

SHELL
curl -X PUT "$BASE/clients/$CLIENT/sync/groups" \
  -H "X-API-Key: $INFIMA_KEY" -H "Content-Type: application/json" \
  -d '{"groups": [{"id": "0b6e7c1a-3f8e-4d2a-9c11-5a7e2b4d9f03", "name": "All Staff"}]}'

4. Turn sync on with POST /clients/{client_id}/sync/enable. INFIMA first checks that it can read the directory; if it can't, sync stays off and the 409 says what to fix (for Google Workspace, usually domain-wide delegation). When it can, the first sync is queued and GET /clients/{client_id}/sync reports users_synced once it has run.

By hand

POST /clients/{client_id}/users adds one person. Send send_welcome_email: true to have them welcomed by the client's scheduled welcome email.

SHELL
curl -X POST "$BASE/clients/$CLIENT/users" \
  -H "X-API-Key: $INFIMA_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: psa-10482-user-maria" \
  -d '{"email": "maria.lopez@acme-mfg.com", "first_name": "Maria", "last_name": "Lopez", "job_title": "Accounts Payable Specialist", "send_welcome_email": true}'

Welcome emails

New users get a welcome email with their portal link, on the client's schedule. PATCH /clients/{client_id}/welcome-emails turns it on for this client; include_existing: true also welcomes the people already synced, rather than only those who arrive later:

SHELL
curl -X PATCH "$BASE/clients/$CLIENT/welcome-emails" \
  -H "X-API-Key: $INFIMA_KEY" -H "Content-Type: application/json" \
  -d '{"client_override": true, "send_hour": 14, "include_existing": true}'

send_hour is UTC. To send now instead of waiting for the schedule, POST /clients/{client_id}/welcome-emails/sends returns how many people it queued (at most once every 10 minutes per client).

With people in place, this is also when to send phishing.interval_days from step 2.

4. Training and compliance

Frameworks. GET /frameworks lists the frameworks a client can take. Assigning one schedules its required courses, so preview it first with dry_run, then send the same request without it:

SHELL
curl -X POST "$BASE/clients/$CLIENT/frameworks" \
  -H "X-API-Key: $INFIMA_KEY" -H "Content-Type: application/json" \
  -d '{"framework_id": "hipaa", "dry_run": true}'
JSON
{
  "dry_run": true,
  "assignment": null,
  "courses_to_schedule": [
    {
      "course_id": "phi-2026",
      "title": "Protecting Health Information",
      "user_count": 1
    },
    {
      "course_id": "phi-data-2026",
      "title": "Handling PHI Day-to-Day",
      "user_count": 1
    }
  ]
}

Courses. POST /clients/{client_id}/training/assignments assigns one course from GET /courses to the whole client, a collection or named users, with a due date. The automatic curriculum fills in the rest from training.courses_per_year.

Collections. POST /clients/{client_id}/collections groups people (a site, a department) so a framework or course can target them.

Policies. A policy is a PDF everyone in the client acknowledges or signs. Upload the file first, then publish it:

SHELL
# 1. Get an upload URL and key (the URL is valid for 15 minutes)
curl -X POST "$BASE/clients/$CLIENT/policies/uploads" -H "X-API-Key: $INFIMA_KEY"

# 2. PUT the PDF to upload_url from that response
curl -X PUT "$UPLOAD_URL" -H "Content-Type: application/pdf" --data-binary @acceptable-use.pdf

# 3. Publish it with upload_key from step 1
curl -X POST "$BASE/clients/$CLIENT/policies" \
  -H "X-API-Key: $INFIMA_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: psa-10482-aup" \
  -d '{"title": "Acceptable Use Policy", "upload_key": "1731254400123456789"}'

A new version later uses the same upload, then POST /clients/{client_id}/policies/{policy_id}/versions: everyone completes it again, and earlier completions stay on record.

5. Give the client its admins

POST /clients/{client_id}/admins/invites sends the standard INFIMA invitation email. The invitee sets a password and sees only this client, as an Administrator (admin) or read-only (viewer). They never become partner admins and never see billing.

SHELL
curl -X POST "$BASE/clients/$CLIENT/admins/invites" \
  -H "X-API-Key: $INFIMA_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: psa-10482-admin-it-lead" \
  -d '{"email": "it-lead@acme-mfg.com", "role": "admin"}'
JSON
{
  "email": "it-lead@acme-mfg.com",
  "status": "queued",
  "expires_at": "2026-12-10T18:25:34.153127Z"
}

The invitation expires after 30 days. Inviting the same address again while it is open returns 409.

Once they accept, they appear in GET /clients/{client_id}/admins. PATCH /clients/{client_id}/admins/{admin_id} changes their role and DELETE removes their access to this client.

6. Check the result

Webhooks are on the roadmap. Until they ship, poll: the sync status for a client being connected, and lists with updated_since for what changed since your last run. Every key may make 600 requests a minute; the X-RateLimit-* headers say how many are left.

What stays with people

A few steps need a person on purpose:

The directory consent
Only the client's own Microsoft 365 or Google Workspace admin can let INFIMA read their directory. The API hands them the link (step 3); it can't click it for them.
The Report Phishing Button
The add-in is deployed from the client's Microsoft 365 or Google Workspace admin center.
The client's logo, its Microsoft Defender connection, and policies for part of a client
These are in the dashboard for now.
Turning directory sync off
Partners only turn sync on; to stop a sync, contact INFIMA support.