SolutionDeskDocs

Host SaaS API

API keys, companies, end-users, tickets on behalf, and MagicLink portal entry.

Your product (the host SaaS) talks to SolutionDesk server-to-server with an API key. End-users never hit these routes from the browser with a staff password.

1. Create an API key

  1. Settings → API keys
  2. Create a key, name it (e.g. production-host).
  3. Copy the raw secret once — it is not shown again.

Send it as:

Authorization: Bearer <api-key>

Invalid or missing keys → 401.

2. Upsert companies and end-users

Key by your own ids (externalId) so MagicLinks and tickets stay stable:

GoalIntegration surface (under /v1/integration/…)
End-customer companyCompanies upsert
Person at that companyEnd-users upsert (attributes feed restricted visibility)

Staff can also create companies/users in the dashboard; the host path is for automation.

When a company or person is removed on your side, mirror it with a DELETE (keyed by the same externalId). Both are idempotent — deleting an unknown record still returns 200 { "ok": true }, so removals are safe to retry.

GoalIntegration surfaceScope
Remove a personDELETE /v1/integration/end-users/{externalId}Revokes their portal identity and sessions. Tickets and comments they authored are kept as history.
Remove a companyDELETE /v1/integration/companies/{externalId}Tears the org out of the workspace (see below).

Removing a company deletes the org completely so no end-user can see it, while keeping what your support team already handled:

  • Its guest portals are deleted.
  • Tickets that reached your support team — anything escalated or provider-tier — are kept for traceability (their company link stays as historical data).
  • The org's internal, never-escalated tickets are permanently deleted with their comments, attachments, and history.
  • The company is pruned from every member's memberships, but the end-user accounts are not deleted — remove those yourself with the end-user DELETE above (for anyone who belonged only to that org).

To only remove someone from one company (not delete them), use DELETE /v1/integration/end-users/{externalId}/companies/{companyExternalId}.

3. Create tickets on behalf of customers

Create tickets with company linkage, visibility, type/priority, and custom fields. Defaults come from workspace ticket config when omitted.

POST /v1/integration/portal/links
Authorization: Bearer <api-key>
Content-Type: application/json

{ "externalId": "jane-acme" }

Response:

{ "url": "https://<api-host>/v1/auth/portal/enter?token=…" }

Your host redirects the signed-in end-user to that URL (usually once). The link is single-use; reuse shows an expired/invalid portal message.

Typical product UX:

  1. User clicks Support in your app.
  2. Your backend mints a link for their externalId.
  3. Browser opens the portal (often on a custom domain).

See the Customer portal guide for what they see next.

5. White-label domains

Settings → Domains — portal (+ optional API) hostnames so cookies stay same-site and branding stays yours.

6. Events

With the events module on, everything visitors see of an event is public: read it without a key from your website, your app or your server, and build your own event overview or event page. {workspace} is your workspace's address (as in the app's URL), {event} an event's web address. Answers are JSON; times are ISO 8601 in UTC, with the event's own timeZone (e.g. Europe/Berlin) to show them on the event's clock.

Only published events appear — never drafts, registrations, invitations or anything staff-only. An unknown workspace, a workspace without events and an unpublished event all answer 404.

Languages. For events offered in several languages, add ?lang=de (any of the event's languages) to every read below; without it the request's Accept-Language header decides, then the event's main language. The texts come translated, and every event carries locale (the language of its texts) and languages (all it is offered in, main first, e.g. [{ "code": "en", "label": "English" }, { "code": "de", "label": "Deutsch" }]).

List your events

GET /v1/event-pages/{workspace} — the published events that haven't ended yet, soonest first (at most 100). Add ?when=past for those that are over, latest first.

curl https://api.solutiondesk.io/v1/event-pages/acme
{
  "workspace": { "name": "Acme", "slug": "acme", "logoUrl": null },
  "events": [
    {
      "locale": "en",
      "languages": [{ "code": "en", "label": "English" }],
      "slug": "kundentag-2026",
      "title": "Kundentag 2026",
      "description": "<p>Our yearly day with <strong>customers</strong>.</p>",
      "descriptionText": "Our yearly day with customers.",
      "status": "published",
      "startsAt": "2026-11-12T08:00:00.000Z",
      "endsAt": "2026-11-12T16:00:00.000Z",
      "timeZone": "Europe/Berlin",
      "location": { "kind": "onsite", "name": "Acme HQ", "address": "Hauptstraße 1, Berlin" },
      "access": "open",
      "coverImage": {
        "id": "img_3f1c…",
        "url": "https://api.solutiondesk.io/v1/event-pages/images/…/img_3f1c….jpg",
        "width": 1600,
        "height": 900,
        "alt": "The stage at Acme HQ"
      },
      "registration": { "open": true, "waitlist": false },
      "url": "https://events.solutiondesk.io/acme/kundentag-2026"
    }
  ]
}
  • description: the team's formatted text as safe HTML — paragraphs, headings (h1–h3), lists, links, bold, italic and quotes, without styles, scripts or images — so you can put it into your page as it is and style the tags yourself; null without one. descriptionText is the same as plain text, e.g. for previews and meta tags.
  • status: published, or registration_closed once you closed registration (with when=past also ended). Cancelled events are left out.
  • access: open, or open_with_approval when the team approves each registration. Invite-only events are not listed — invited people get their link by mail.
  • location: onsite with name and address, or online — the meeting link is never public; registered people get it by mail.
  • coverImage: the event's cover image, or null. url works in any <img> on any website and is cached for a year — a new image gets a new address, so always use the one from the latest answer. width and height (in pixels, null when unknown) let you reserve space before it loads; alt (or null) describes it.
  • registration: { "open": true, "waitlist": true|false } (waitlist — new registrations go onto the waiting list), or { "open": false, "reason": … } with closed, full, ended, cancelled or invite_only.
  • url: the event page, on your own address for event pages when you set one.

One event

GET /v1/event-pages/{workspace}/{event} — everything on the event page: the fields above (without url), plus

  • images — more pictures of the event, in the team's order, each like coverImage.
  • sessions — the published programme, in order: id, title, description, startsAt, endsAt, track (room or track), speakers (name, role, bio), choosable (can be picked in the form) and full.
  • programmeVisible — false for an invite-only event whose programme is hidden from people without an invitation (then sessions is empty). Add ?invite= with the token from an invitation link to see it.
  • form — the registration form: askCompany, askJobTitle, companyRequired, jobTitleRequired (only count while the field is asked) and the fields of your own questions (id, label, type, required, options for choices).
  • workspace (name, address, logo) and invitation (the invited person's email and name with a valid ?invite=, else null).

Register

From a browser on one of the websites under Event settings → Your websites (other websites can read, but not register):

POST /v1/event-pages/{workspace}/{event}/registrations
Content-Type: application/json

{
  "firstName": "Ada",
  "lastName": "Lovelace",
  "email": "ada@example.com",
  "company": "Analytical Engines",
  "answers": { "diet": "vegetarian", "talks": ["s_keynote"], "privacy": true },
  "locale": "en",
  "elapsedMs": 5400
}
  • answers are keyed by the field id: text for text fields, the option id for a choice, a list of option ids for several choices, a list of session ids for the talk choice, true for a ticked box.
  • company and jobTitle when the form asks for them — required when companyRequired / jobTitleRequired say so. With an invitation link, also send its token as invite.
  • locale — the language the person used (one of the event's languages); their emails come in it. Without it, the event's main language.
  • elapsedMs — how long the person took to fill in the form. Sent too fast (under 1.5 seconds) counts as a bot and is silently ignored.
  • The answer is {"result": "check_email"}: the person confirms by email (the same answer whether or not the address is already registered). A wrong answer is a 400 with error and fields (field id → message).

From your server, with your API key, register someone who is signed in to your product — no confirmation email, because you vouch for the address:

curl -X POST https://api.solutiondesk.io/v1/integration/events/{event}/registrations \
  -H "Authorization: Bearer $SOLUTIONDESK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"firstName":"Ada","lastName":"Lovelace","email":"ada@example.com","answers":{}}'

The answer is {"result": "confirmed" | "pending_approval" | "waitlisted"}. The person gets a mail with a link to change or cancel their registration.

Limits

The public endpoints are limited per IP address: reading to 120 requests a minute (images to 600), registering to 10 in 10 minutes (and 3 an hour per email address). Over the limit the answer is 429.

See also On your website for the ready-made snippet and the announcement on your website, and MCP for agents to let an agent manage events.

MCP vs Host API

Host API (API key)MCP (staff OAuth)
WhoYour backendAI agent as a staff member
AuthBearer API keyBrowser login / OAuth for MCP client
UseSync companies, mint portal linksWork tickets day to day

Configure MCP under Account → MCP.

On this page