API reference
Sigil’s API is not a public integration surface. It exists for the add-in and the admin portal, and it is documented here because a security review usually wants to see it.
All endpoints live on portal.usesigil.app.
Authentication
Section titled “Authentication”| Auth | Meaning |
|---|---|
| Add-in token | An Entra access token carrying the access_as_user scope |
| Admin token | An Entra access token carrying the portal_admin scope |
| Tenant API key | A credential an Admin created for a script, presented as Authorization: Bearer sigil_… |
| None | Public, unauthenticated |
Every token is verified on the server: cryptographic signature against the calling tenant’s published keys, then issuer, audience, home tenant and scope. See the security model.
API keys
Section titled “API keys”An API key is presented on the same Authorization header a
portal token uses, and is told apart by its sigil_ prefix. An Entra token
begins eyJ and carries three dot-separated segments, so there is no ambiguity
and no second header for a client to be taught about.
A key carries a set of capabilities and no role. What it may reach is decided first by an allow-list, and only then by those capabilities.
One list names every operation a key may call. A method and path that does not
match an entry is refused with 404 and code: "not_available_to_api_keys" before
the route runs, whatever the key holds. 404 rather than 403, because a 403 on a
route that exists for people confirms which routes are real, and because the
endpoint genuinely does not exist for keys.
The allow-list narrows and never grants. A request that matches an entry still meets the route’s own capability guard, so a mismatch here could at worst admit a request the route then refuses.
| Refused to a key | Response |
|---|---|
| Any method and path outside the allow-list | 404 with not_available_to_api_keys |
| A write of any kind, where the key is read-only | 403 |
| A capability the key was not granted | 403 |
| Anything guarded by the Admin role rather than a capability | 403 |
The Admin-role guards are mostly moot, because those routes are outside the
allow-list and answer 404 first. The exception is a publish while
publish approval is switched on: those routes are on
the allow-list, so a key reaches them and is then refused with 403 and
code: "approval_required".
The signature endpoints under /api/signature are not reachable with a key.
They take a per-mailbox add-in token. Their administrative equivalents,
GET /api/admin/download and POST /api/admin/preview with an email, are
outside the allow-list for the same reason: both name a mailbox the caller chose
and return that mailbox’s rendered signature, and preview returns its directory
attributes alongside. Preview takes a second address as well, the sender
behind a shared mailbox send, resolved
against the directory the same way, so one call can read two people’s records. POST /api/admin/rules/simulate and
GET /api/admin/users/search are out on the same ground.
Rendering an archived version, at
GET /api/admin/templates/:id/versions/:version/preview, is on the allow-list
rather than outside it. It takes no address and reads no directory record: it
renders the stored body against sample data, which answers “what did this version
look like” without answering anything about a person.
The two render routes beside it, which answer the same question as a picture, are out. They return an image rather than data, and the HTML a key already gets from the preview route is the form a script can read, diff or store. See signature previews.
POST /api/admin/help/ask is out as well. It answers a person’s question in
prose, and a script wanting what the documentation says should read the
documentation. See ask the documentation.
So is GET /api/admin/events/socket, the connection the portal keeps open to
hear about changes as they happen. It tells an open page to re-read something,
which a script can do on its own schedule by calling the route it already uses.
GET /api/admin/profile-values and PUT /api/admin/profile-values/:email are
out for the same reason. Those return and write what a named colleague entered
about themselves, which is the same per-individual read, and arguably more
personal for having been typed by the person rather than held by the directory.
The field definitions are a different matter and are reachable: defining the
same six fields when a provider stands up a new client is exactly the
configuration work a key exists for.
Also outside it: POST /api/admin/test-email,
POST /api/admin/banners/:id/test-email and both digest routes, which send
or compose mail; POST /api/admin/banners/:id/preview, which renders the
caller’s own signature and so needs a caller who has one; every billing route that writes to Stripe, along with the
invoice and credit lists, whose rows carry links that pay an invoice and the
commercial reasoning behind concessions; PUT /api/admin/users
and DELETE /api/admin/users/:email; the custom role routes that write; PUT /api/admin/settings, GET /api/admin/approvals
and the draft submit and reject routes; POST /api/admin/dpa/accept; everything
under /api/admin/managed, /api/admin/partner and /api/admin/platform; the
onboarding routes; and the /api/admin/api-keys routes themselves. See
API keys.
The three branded link domain writes are out too, while the read of its state is in. Claiming a hostname starts a recurring charge, and releasing one takes every tracked link in every signature back to the shared hostname and breaks the ones already in sent mail, which is not undone by running the call again: getting it back means re-validating a certificate against the customer’s own DNS. Re-checking a certificate’s state is excluded only because it is useful next to those two and its answer is already in the read.
A test asserts in both directions against the registered routes: every allow-list entry names a path that exists and the capability that route’s guard actually demands, and any route in neither the allow-list nor the explicit exclusion list fails the build. That is what stops the published reference describing a permission the server does not enforce, and it is what makes adding a route a decision rather than a default.
| Route | Auth | Purpose |
|---|---|---|
GET /api/admin/api-surface |
Admin token or key | The allow-list as data, with the base URL. What the portal’s API reference tab renders |
GET /api/admin/api-reference.json |
Admin token or key | The same thing as an OpenAPI 3.0 document, served as a download |
Both are guarded by authentication alone. A key holds no role and so could never
pass an Admin-role guard, which would make an admin-gated reference unreachable
by the credential it documents, and neither route returns tenant data. Each
operation in the document carries x-required-capability. The read-only switch
cannot be expressed in OpenAPI at all, since security is static per operation, so
the document says so in its description instead.
X-Client-Tenant and X-Impersonate-Tenant are ignored under key
authentication. A key is bound to one tenant when it is issued, and letting a
header retarget it would make that binding a suggestion.
Requests are limited to 600 a minute per key, and a key over its limit is answered 429. That check runs before the allow-list, so probing for reachable endpoints is bounded by the same limit as real work. The tenant refusals below apply to keys exactly as they apply to people.
A valid token is not enough on its own. Before any admin route runs, the organisation behind the token has to be one Sigil will serve, and a refusal says which of three things is wrong:
| Code | Meaning |
|---|---|
not_onboarded |
This Entra tenant has never been connected, or was connected and then purged |
pending_deletion |
The organisation was removed and is inside its deletion grace window |
suspended |
Access is suspended for an account or billing reason |
All three return 403. They are separated because only the first can be resolved by the person holding the token, by granting admin consent. See connect your organisation.
Making a first call with a key
Section titled “Making a first call with a key”You need a key, which an Admin creates from API keys in the portal sidebar and whose secret is shown once, at creation. See API keys.
- Open API keys in the portal sidebar.
- Choose the API reference tab.
- Under Calling the API, choose Copy beside the example command.
- Replace
sigil_…in the copied command with the key’s secret. - Run it.
The command sends GET /api/admin/me with the key on the Authorization
header. The response names the key as email: "API key: <name>", lists the
capabilities it carries and returns an empty roles list, which proves the key
works before anything else does.
On the same tab, Show what this key can do lists your live keys. Picking one
greys out every operation that key would be refused, and hovering a greyed row
gives the reason and the status code it would get. OpenAPI 3.0 downloads the
same reference as sigil-openapi.json.
Signature endpoints
Section titled “Signature endpoints”| Route | Auth | Purpose |
|---|---|---|
POST /api/signature |
Add-in token | The rendered signature plus its inline images. email defaults to the caller; type is new or reply. 403 while delivery is paused |
GET /api/signature?email=&type= |
Add-in token | The same answer, for add-in versions that predate the POST form |
POST /api/signature/download |
Add-in token | The signature as a standalone HTML file with images inlined as data: URIs. Also refused while delivery is paused |
GET /api/signature/download?email=&type= |
Add-in token | The same answer, for add-in versions that predate the POST form |
POST /api/signature/report |
Add-in token | The add-in’s apply outcome for one attempt |
GET /api/signature/profile?email= |
Add-in token | Whether this mailbox has anything to fill in on the profile page, and where that page is |
The email parameter may name any mailbox in the calling tenant, which is what
allows a shared mailbox or an alias to render its own signature. The identity on
a report is taken from the verified token rather than from the payload.
The compose request takes its inputs in a JSON body for the same reason the rules simulation does: they name a person, and query strings end up in logs. It also stops the request looking like something to block. An address in a URL is the shape a data-loss-prevention proxy is configured to catch, and one customer’s egress filter was killing every signature request on that basis while leaving the add-in’s other calls to the same host alone. The URL now carries nothing but the host. The GET form is kept because add-ins update on their own schedule and an older one has to keep working; both go through the same handler, the same token check and the same gates.
The download follows the same pattern, and for the same reason. It used to put the mailbox address in the query string, which is the one shape that egress filter blocks on sight, so the add-in now posts it in a body. The GET form is kept for panes built before the change, and answers identically.
That request carries two identities rather than one, and the difference matters.
email is the mailbox the message leaves from, and the token names the person
holding it. A template using the
sender placeholders renders out of the gap
between them, so passing the signed-in user’s own address as email would turn
every shared-mailbox signature into a personal one.
GET /api/signature/profile is what decides whether the add-in offers its “Edit
my details” button. It answers editable: true only when the organisation has
switched profile editing on and has at least one field
available, and it applies the same organisation, subscription and exclusion
checks the compose path does, because a button leading to a refusal is worse than
no button. It is deliberately separate from GET /api/signature, which runs on
every compose and is usually answered from cache; the pane asks this one once
when it opens.
User profile endpoints
Section titled “User profile endpoints”These sit under /api/me rather than /api/admin, and it is not tidiness. The
admin router promotes the first person to sign in to full administrator, so
hanging a route every employee can reach off that middleware would make the first
colleague to open their own profile page an administrator.
| Route | Auth | Purpose |
|---|---|---|
GET /api/me/profile |
Portal token, no role | The available fields, this person’s values, and their directory attributes read-only |
PUT /api/me/profile |
Portal token, no role | Save this person’s own values |
GET /api/me/signature?type= |
Portal token, no role | This person’s own signature, as a standalone HTML document |
GET /api/me/status |
Portal token, no role | Whether the add-in is working for this person: when a signature last went in and in which Outlook, why the last attempt failed, or that none has been requested yet |
No role is required and none is consulted. Every route here acts on the mailbox
in the verified token, and there is no email parameter, because there is no
shared-mailbox case for editing your own details.
The middleware applies the same gates the compose path does. An organisation that
is not active, a lapsed subscription, an
excluded mailbox or profile editing switched off each
refuse, the last of them with code: "portal_off" so the page can explain rather
than show an error.
Saving validates every value against its field definition and answers 400 with the field-by-field reasons, rather than accepting something a signature cannot safely carry. Saves are rate limited per mailbox.
Template endpoints
Section titled “Template endpoints”| Route | Auth | Purpose |
|---|---|---|
GET /api/admin/templates |
Admin token | The library, the current role assignments, and signaturesPaused |
GET /api/admin/rollouts |
Admin token | In-flight rollouts, for the library’s badges |
POST /api/admin/templates |
Admin token | Create a template from the starter design, a blank canvas, the default HTML, or the starter filled in from a confirmed website look-up |
POST /api/admin/brand/probe |
Admin token | Read an organisation’s public website for a logo, brand colour, name, address, social profiles and legal page. Stores nothing |
GET /api/admin/templates/:id |
Admin token | One template’s body |
PUT /api/admin/templates/:id |
Admin token | Publish a new version |
PATCH /api/admin/templates/:id |
Admin token | Rename |
POST /api/admin/templates/:id/duplicate |
Admin token | Copy a template |
DELETE /api/admin/templates/:id |
Admin token | Delete, blocked while assigned. ?purge=true skips Recently deleted |
GET /api/admin/templates/deleted |
Admin token | Recently deleted templates and the retention window |
POST /api/admin/templates/:id/restore |
Admin token | Bring one back from Recently deleted |
GET /api/admin/templates/:id/versions |
Admin token | Rollback history |
GET /api/admin/templates/:id/versions/:version/preview |
Admin token | One archived version rendered against sample data, images inlined, with its length and the Outlook limit |
GET /api/admin/templates/:id/render |
Admin token | A PNG of the live version rendered for the sample person, for signature previews. 404 while no picture has been made. Sends an ETag, and answers a matching If-None-Match with 304 and no body. Not reachable with an API key |
GET /api/admin/templates/:id/versions/:version/render |
Admin token | The same picture for an archived version. Not reachable with an API key |
POST /api/admin/templates/:id/rollback |
Admin token | Restore a version |
PUT /api/admin/templates/:id/draft |
Admin token | Save a working copy |
DELETE /api/admin/templates/:id/draft |
Admin token | Discard a working copy |
POST /api/admin/templates/:id/publish-draft |
Admin token | Promote the working copy, which is also the approve action |
POST /api/admin/templates/:id/draft/submit |
Admin token | Put a working copy up for review |
POST /api/admin/templates/:id/draft/reject |
Admin token, Admin role | Send a submitted draft back, with a required note |
GET /api/admin/approvals |
Admin token | The review queue, whether the caller can approve, and whether approval is required |
GET /api/admin/templates/:id/export |
Admin token | Portable JSON, images included |
POST /api/admin/templates/import |
Admin token | Import a bundle as a new entry |
PUT /api/admin/roles |
Admin token, rules capability | Assign templates to the new and reply roles |
PUT /api/admin/signature-delivery |
Admin token, rules capability | Pause or resume delivery for the whole organisation. Takes { "paused": true } or false |
POST /api/admin/preview |
Admin token | Render with sample data, or against a named mailbox, in which case the response carries that mailbox’s directory attributes alongside the HTML. An optional sender renders the shared mailbox case. With chrome set, an optional at picks the day whose banner is shown: a date as YYYY-MM-DD, read as midday UTC, or a full ISO timestamp. Anything else answers 400 |
GET /api/admin/download?email=&type= |
Admin token | A mailbox’s live signature as a standalone file |
Shortcuts acting on the active new-message template exist at
GET/PUT /api/admin/template, GET /api/admin/versions and
POST /api/admin/rollback.
Every route above except PUT /api/admin/roles and
PUT /api/admin/signature-delivery needs the templates capability, which the
Editor role holds alongside an Admin. Pointing a role at a different template and
pausing delivery are guarded by the rules capability instead, which no built-in
role but Admin holds, though a custom role can be given it. See roles and capabilities.
Those two sit together on purpose. One decides which signature a mailbox gets,
the other whether it gets one at all, and they are the same decision from two
ends. Setting paused to the value already in force is accepted and changes
nothing, so a script can assert the state it wants without checking first and
without filling the change log with repeats. See
pausing delivery.
Rejecting a submitted draft is the one route here that needs the Admin role on top of the capability. Submitting is not, because anyone who can edit a draft can ask for it to be looked at.
The approvals queue reports requireApproval alongside the pending list so the
portal can offer the queue without reading settings, which most of the roles that
submit cannot see.
The library listing carries hasDraft per entry, computed as a boolean rather
than by reading the drafts themselves, so listing the library never loads a
template body. It is what the Draft badge is drawn from.
Avoiding overwriting a colleague’s change
Section titled “Avoiding overwriting a colleague’s change”Template writes accept two optional fields describing what the caller last read:
expectedVersion, the template’s version, and expectedDraftUpdatedAt, the
draft’s updatedAt, or null to say there was no draft. PUT /api/admin/templates/:id and POST …/publish-draft take both, and PUT …/draft and POST …/draft/submit take the draft one.
If the template no longer matches, the write is refused with 409, nothing is
changed, and the body carries a code and a current object saying what the
template holds now and who saved it:
code |
Meaning |
|---|---|
version_conflict |
A different version is live than the one you sent |
draft_conflict |
The draft was saved by somebody else after the one you sent |
draft_changed |
The draft you are approving or publishing has changed since you read it |
rollout_running |
You started a staged rollout while another is running |
schedule_exists |
You booked a publish while another is pending |
Leaving both fields out keeps the older behaviour, where the last write wins, so an existing script carries on working. A script that reads a template, changes it and writes it back should send them, for the same reason the portal does. The portal’s own handling is described under when a colleague saved first.
rollout_running and schedule_exists are cleared by sending replace: true
with the request, which replaces the running rollout or the pending booking on
purpose. It does not get past version_conflict: a rollout is always measured
against the version that is live when it starts.
When publish approval is on
Section titled “When publish approval is on”With publish approval switched on for the organisation,
every route that puts a body in front of users additionally requires the caller
to hold the Admin role. A caller who does not is refused with 403 and
code: "approval_required".
That covers PUT /api/admin/template, PUT /api/admin/templates/:id,
POST /api/admin/rollback, POST /api/admin/templates/:id/rollback,
POST /api/admin/templates/:id/publish-draft,
POST /api/admin/templates/:id/rollout/promote and
PUT /api/admin/templates/:id/schedule.
Abandoning a rollout is deliberately not in that list. Neither is saving, discarding or submitting a draft, none of which reach a user.
Staged rollout endpoints
Section titled “Staged rollout endpoints”| Route | Auth | Purpose |
|---|---|---|
PUT /api/admin/templates/:id with a rollout block |
Admin token | Publish as a staged rollout rather than to everyone |
GET /api/admin/templates/:id/rollout |
Admin token | State, both versions’ apply results, and the next decision |
POST /api/admin/templates/:id/rollout/promote |
Admin token | Publish to everyone now |
POST /api/admin/templates/:id/rollout/rollback |
Admin token | Abandon the rollout |
GET /api/admin/rollouts |
Admin token | Every rollout currently in flight, as light summaries |
The listing route returns the template id, the percentage, who started it and when, and deliberately not the bodies. A rollout carries a whole canary signature and its design document, and the library only needs to know which templates are mid-rollout and how far along.
A staged publish writes the body to the rollout rather than to the template, so
the live signature is unchanged until it promotes. Every gate has a default,
which makes "rollout": true a complete request. These routes sit behind the
templates capability, the same as an ordinary publish. See
staged rollouts.
Scheduled publish endpoints
Section titled “Scheduled publish endpoints”| Route | Auth | Purpose |
|---|---|---|
PUT /api/admin/templates/:id/schedule |
Admin token | Book a publish for a future instant. A pending one is replaced only with replace: true, and otherwise answers 409 schedule_exists |
GET /api/admin/templates/:id/schedule |
Admin token | The schedule on one template, whatever its state |
GET /api/admin/schedules |
Admin token | Every pending schedule, soonest first |
DELETE /api/admin/templates/:id/schedule |
Admin token | Call off a booked publish |
The booking route takes the same body a publish takes, plus publishAt as an
ISO-8601 instant and an optional rollout block. It applies the same length,
validation and eject checks a publish does, so a schedule that would be rejected
when it fires is rejected when it is booked.
The eject check is the one guard a portal user never meets. Publishing plain
html to a template built in the designer discards its design document, so the
request is refused unless it also carries eject: true. The error says so. See
ejecting to HTML.
publishAt must be in the future and within 365 days. The body is stored on the
schedule rather than read from the draft when it fires. All four routes need the
templates capability. See
scheduled publishing.
Configuration endpoints
Section titled “Configuration endpoints”| Route | Auth | Purpose |
|---|---|---|
GET /api/admin/fields |
Admin token | The placeholder list the editors offer, including this organisation’s own profile fields under a “User profile” group |
GET /api/admin/profile-fields |
Admin token, settings capability | The custom fields this organisation’s staff fill in about themselves |
POST /api/admin/profile-fields |
Admin token, settings capability | Define a field. The key becomes {{custom.<key>}}, and {{sender.custom.<key>}} for whoever presses Send from a shared mailbox, and cannot be changed afterwards |
PUT /api/admin/profile-fields/:key |
Admin token, settings capability | Change a field’s label, help, type or availability. enabled: false hides it and keeps everyone’s values |
DELETE /api/admin/profile-fields/:key |
Admin token, settings capability | Delete a field and every value stored against it. Irreversible |
GET/PUT /api/admin/rules |
Admin token, rules capability | Assignment rules, replaced as one ordered list |
POST /api/admin/rules/simulate |
Admin token, rules capability | Dry-run the saved rules against one mailbox |
GET /api/admin/rules/groups/search |
Admin token, rules capability | The same group name search, reachable without cost management: how a group name becomes the object id a group rule matches on. Returns an empty list below two characters, and 502 if the directory permission for groups was never granted |
GET/POST /api/admin/banners, PUT/DELETE /api/admin/banners/:id |
Admin token, banners capability | Campaign banners. PUT is a partial update, and any change to startsAt or endsAt must carry timezone with it |
POST /api/admin/banners/:id/preview |
Admin token, banners capability | One banner on the caller’s own new-message signature, whatever its window or paused state, with images inlined and links untracked. renderedAs is null when the caller has no mailbox in the organisation and sample data was used. Not reachable with an API key |
POST /api/admin/banners/:id/test-email |
Admin token, banners capability | Mail the same thing to the caller. Takes no recipient and no mailbox. 400 when the caller has no mailbox in the organisation. Not reachable with an API key |
GET/POST /api/admin/footers, PUT/DELETE /api/admin/footers/:id |
Admin token, footers capability | Compliance footers |
GET /api/admin/footers/excluded-templates |
Admin token, footers capability | The templates that go out without the compliance footer, each with who turned it off, when, and the version of the statement they acknowledged |
GET /api/admin/assets |
Admin token, templates capability | The image list |
GET /api/admin/assets/usage |
Admin token, templates capability | How full the image library is, against its limits |
GET /api/admin/asset/:name |
Admin token, templates capability | One image, base64 encoded, for the portal’s preview |
PUT /api/admin/asset/:name, DELETE /api/admin/asset/:name |
Admin token, templates capability | Upload or remove an image. An upload must be a PNG or JPEG of up to 1 MB: another type answers 400, a larger file 413, and a full library 409 |
PUT /api/admin/templates/:id/tracking |
Admin token, templates capability | Toggle link tracking for a template |
PUT /api/admin/templates/:id/banners |
Admin token, templates capability | Say whether campaign banners may be added to a template. Takes { "enabled": true } or false; anything else answers 400 |
PUT /api/admin/templates/:id/footer |
Admin token, footers capability | Say whether the compliance footer is added to a template. Takes { "enabled": true }, or { "enabled": false, "acknowledged": true }. Turning it off without acknowledged: true answers 400 with code acknowledgement_required. Needs the footers capability in both directions, not templates |
POST /api/admin/test-email |
Admin token, templates capability | Send a rendered signature to a named inbox. An optional banner is "live" (the default), "none", or a banner id, which attaches that banner whatever its window. An unknown id answers 400 |
The placeholder list is the one route here with no capability of its own. It describes what a template could reference rather than exposing any of your data, and both editors need it before anything else loads.
A profile field’s type is one of text, choice, url, email or phone.
The portal labels url as “Link”, so that is the one name that differs between
the screen and the request. options is required for choice, between 1 and 24
distinct entries, and ignored for every other type. maxLength defaults to 200
and must be between 1 and 500; it is enforced on every type, not just text,
even though the portal only offers the control on text fields. Changing the type
of a field that already holds values deletes the values the new type would
refuse, and the response says how many went. See
profile fields.
The simulation takes its email in the body rather than the query string,
because it names a person and query strings end up in logs. It replies with the
mailbox it resolved, the template each role lands on and what decided it, and a
per-rule trace saying whether each rule matched, what the mailbox’s value for the
tested attribute was, and which roles that rule actually settled. It reads the
directory live rather than from the hour-long resolution cache, writes nothing
back, and records no change log entry. An address that is not a mailbox in the
tenant answers 404. See assignment rules.
Monitoring endpoints
Section titled “Monitoring endpoints”| Route | Auth | Purpose |
|---|---|---|
GET /api/admin/activity |
Admin token, monitoring capability | Per-mailbox rollup, recent feed, adoption |
GET /api/admin/activity/events |
Admin token, monitoring capability | The event search, filtered by mailbox, source or outcome |
GET /api/admin/audit |
Admin token, monitoring capability | Directory attribute coverage, with the counts of accounts left out as external and as excluded reported separately |
GET /api/admin/log |
Admin token, monitoring capability | The change log. ?scope=template narrows it to the changes that alter what a signature looks like and ?scope=tenant to the rest, applied in the database rather than after the limit. ?limit= takes up to 500 and defaults to 100. ?before_at= and ?before_id=, set together to the created_at and id of the oldest entry you hold, return the page older than it, which is how the portal’s Audit log view reads 100 of each half at a time and offers Load more; either one alone is ignored. Each entry carries the actor’s current display name where one is known, beside the address the entry was written with. Actions Tophhie Cloud support took read as platform. followed by the lever, and name “Sigil operator” rather than the individual |
GET /api/admin/links |
Admin token, analytics capability | Click totals per tracked link, with a 30-day trend series on each |
GET /api/admin/links/overview |
Admin token, analytics capability | One window of tenant-wide analytics: daily series, campaign rollup, device, client and referrer splits, hour-of-day grid, and the clicks-per-1,000 rate |
GET /api/admin/links/detail/:slug |
Admin token, analytics capability | The same shape for one link |
Both analytics routes take a days query parameter. It is clamped to between 7
and 365 rather than rejected, and defaults to 30, so a stale bookmark returns a
chart instead of an error.
A slug belonging to another organisation returns 404 from the detail route. Slugs are globally unique, and the redirect being public does not make the numbers behind it public.
Click totals sit behind analytics rather than monitoring, which is what lets the Marketing role read its own campaign numbers without reaching the activity telemetry.
Organisation endpoints
Section titled “Organisation endpoints”| Route | Auth | Purpose |
|---|---|---|
GET /api/admin/me |
Admin token | Who the caller is, their role, and their organisation’s state |
GET /api/admin/flags |
Admin token | Which features being rolled out gradually are switched on for this organisation. Reachable with an API key of any scope |
GET/PUT /api/admin/users, DELETE /api/admin/users/:email |
Admin token, users capability | Manage users and roles |
GET /api/admin/users/search |
Admin token, templates or users capability | Directory lookup, for pickers such as download and test email |
GET /api/admin/custom-roles |
Admin token, users capability | The organisation’s custom roles: each one’s key, name, description, capabilities and how many people hold it. A holder’s role on GET /api/admin/users is that key, custom: followed by twelve characters, and roleName carries the role’s name |
POST /api/admin/custom-roles, PUT/DELETE /api/admin/custom-roles/:id |
Admin token, Admin role | Define, change or delete a custom role. Answers 409 for a duplicate name, at the limit of 20, or when deleting a role somebody still holds |
GET/PUT /api/admin/settings |
Admin token, settings capability | The organisation-wide switches: publish approval, profile editing, product update emails and digest frequency |
GET /api/admin/profile-values |
Admin token, staff profile details capability | Every mailbox with stored profile values, each with its completion count, and the enabled field definitions to label them with |
PUT /api/admin/profile-values/:email |
Admin token, staff profile details capability | Edit a colleague’s values on their behalf, or enter them for a mailbox that has none yet. The address must exist in the directory, and a secondary alias is stored against the mailbox’s primary address. Validated exactly as the colleague’s own save is, and recorded in the change log |
GET /api/admin/settings/digest/preview |
Admin token, settings capability | The health digest as it would be sent now. Sends nothing |
POST /api/admin/settings/digest/send |
Admin token, settings capability | Mail the digest to the calling admin alone |
GET /api/admin/link-domain |
Admin token, settings capability | The organisation’s branded link domain: the hostname, how far its certificate has got, the CNAME target a new one points at, and whether the add-on is held. domain is null while links are still on the shared hostname, which the response names |
POST /api/admin/link-domain |
Admin token, settings capability | Claim a hostname and order a certificate for it. Answers 402 when the add-on is not held, and 400 with the reason when the hostname will not do |
DELETE /api/admin/link-domain |
Admin token, settings capability | Release the hostname. New links return to the shared domain, and links already sent on the branded one stop resolving |
POST /api/admin/link-domain/check |
Admin token, settings capability | Re-read the certificate’s state now rather than waiting for the nightly check |
POST /api/admin/help/ask |
Admin token, any role | A question about the documentation, answered from docs.usesigil.app with the pages it drew on. Answers 404 if the feature is unavailable to the organisation, before the body is read. Not available to API keys. See ask the documentation |
GET /api/admin/events/socket |
Admin token, templates, settings or billing capability | The WebSocket the portal holds open for live updates. Each message names what changed by id and nothing more, and only reaches people who hold the capability for it. Answers 403 for a role with none of the three. Not available to API keys |
GET /api/admin/onboarding |
Admin token, Admin role | Getting started checklist state |
POST /api/admin/onboarding/dismiss |
Admin token, Admin role | Dismiss the checklist |
POST /api/admin/dpa/accept |
Admin token, Admin role | Record acceptance of the data processing agreement |
GET /api/admin/billing |
Admin token, billing capability | Subscription status, seats, card, invoice |
POST /api/admin/billing/checkout |
Admin token, billing capability | A hosted Stripe card-capture URL. Answers 409, naming the missing fields, until the billing profile is complete |
POST /api/admin/billing/setup-intent |
Admin token, billing capability | Start the card form on the Billing page: a one-use secret the form is drawn against. Answers 409, naming the missing fields, until the billing profile is complete. Not available to API keys |
POST /api/admin/billing/setup-intent/:id/complete |
Admin token, billing capability | Report that the card form succeeded, so the card becomes the invoice default at once. Answers 409 while the card is still being confirmed, and 502 when it was saved but could not be made the default. Not available to API keys |
POST /api/admin/billing/invoices/:id/payment-intent |
Admin token, billing capability | Start paying one open invoice by card in the portal. Answers 404 for an invoice that is not this organisation’s, and 409 for one that is no longer open. Not available to API keys |
POST /api/admin/billing/portal |
Admin token, billing capability | A hosted Stripe management URL |
POST /api/admin/billing/cancel, …/reactivate |
Admin token, billing capability | Schedule the subscription to end at the close of the current period, or resume it: inside that window reactivating lifts the schedule, after it a new subscription is started. Reactivating answers 409 until the billing profile is complete, and also answers 409, with the reason in error, when Stripe refuses the new subscription, most often because a card account has no payment method |
PUT /api/admin/billing/profile |
Admin token, billing capability | Save the billing profile |
GET /api/admin/billing/invoices |
Admin token, billing capability | Invoice history, newest first, each row carrying the hosted page and the PDF. Answers with an empty list and a flag rather than an error when the invoices cannot be read |
GET /api/admin/billing/adjustments |
Admin token, billing capability | Credits and corrections applied to the account, newest first, each with its category and the reason |
POST /api/admin/billing/addons/link-domain |
Admin token, billing capability | Buy or drop the branded link domain add-on. Refused for a partner-managed client, for an organisation with no live subscription, and until the billing profile and a means of payment are in place. Dropping it removes the domain |
GET /api/admin/exclusions |
Admin token, cost management capability | The cost management list and the mode that says how it is read: the individually listed mailboxes, each annotated with whether it still resolves in the directory and whether it was billable, the listed groups, the totals across both, and the seats the list bills |
PUT /api/admin/exclusions/mode |
Admin token, cost management capability | Switch between exclusion mode and inclusion mode. Refused while anything is on the list |
POST /api/admin/exclusions |
Admin token, cost management capability | Put one mailbox or many on the list, with an optional note. In exclusion mode that excludes them, in inclusion mode it includes them |
DELETE /api/admin/exclusions/:email |
Admin token, cost management capability | Take one off the list |
GET /api/admin/exclusions/suggested |
Admin token, cost management capability | Billable mailboxes that have never applied a signature, and what excluding them would save. Refused in inclusion mode, where the question has no counterpart |
POST /api/admin/exclusions/groups |
Admin token, cost management capability | Put an Entra group’s members on the list. The group is confirmed against the directory, and its membership resolved, before the call returns |
DELETE /api/admin/exclusions/groups/:id |
Admin token, cost management capability | Take a group off the list, and report which addresses that released |
POST /api/admin/exclusions/groups/:id/sync |
Admin token, cost management capability | Refresh one group’s membership now, rather than waiting for the nightly refresh |
GET /api/admin/groups/search |
Admin token, cost management capability | Group name search for the cost management picker, from two characters |
GET /api/admin/api-keys |
Admin token, Admin role | Every API key for the organisation, revoked ones included |
POST /api/admin/api-keys |
Admin token, Admin role | Mint one. The secret is in this response and in nothing else, ever |
DELETE /api/admin/api-keys/:id |
Admin token, Admin role | Revoke one. The row survives so the change log stays resolvable |
The three key routes are the ones an API key can never call, whatever it holds, so a leaked key cannot mint a replacement or revoke the key an administrator is about to use to stop it. A key may not be granted capabilities its creator does not hold, and an expiry in the past is refused rather than quietly accepted. See API keys.
Neither digest route stamps the digest schedule, so previewing or test-sending cannot delay the real one. The send route answers 503 where mail sending is unavailable, and 502 with the underlying reason if the send itself fails. That failure is surfaced rather than swallowed, since finding out whether the send works is the entire point of the route.
Most of these guards name a capability rather than a role, so the Billing role reaches the subscription and the user list alongside an Admin. The routes that name the Admin role are Admin-only, whatever a custom role reaches. The settings capability belongs to no built-in role but Admin, though a custom role can be given it. See roles and capabilities.
A partner’s own administrator cannot accept a managed client’s DPA on their behalf. That request is refused, because the client is the data controller. See compliance.
Partner endpoints
Section titled “Partner endpoints”Present only for partner staff, and scoped to the partner rather than to a
tenant. They live under /api/admin/partner.
| Route | Purpose |
|---|---|
GET /clients |
The managed client list |
POST /clients/:tenantId/release |
Release a client back to direct billing |
POST /clients/:tenantId/addons/link-domain |
Enable or disable the branded link domain add-on for one client. Disabling removes their domain. Needs the manage permission and an accepted agreement |
GET /invites |
Outstanding client invitations |
POST /invites |
Issue one, returning the consent link and optionally mailing it |
DELETE /invites/:token |
Revoke a pending invitation |
GET /transfers |
Outstanding requests to take over an existing tenant |
POST /transfers |
Ask to take one over, by domain or Entra tenant id |
GET /billing, POST /billing/checkout, POST /billing/portal |
The consolidated subscription. GET /billing also carries periodToDate (days measured, mailbox-days and the average so far), seatMethod (last or average) and seatMethodNote, the sentence the page prints |
POST /billing/sync |
Refresh seats. Reports the period’s average once the account is on it, returned as averageSeatsMilli (thousandths of a mailbox) beside seats. Returns seats: null with a skipped reason, usually already-reported-today, when the day has already been reported. Once every ten minutes, 429 sooner |
POST /billing/setup-intent, POST /billing/setup-intent/:id/complete |
The card form on Partner billing, as for an organisation |
POST /billing/invoices/:id/payment-intent |
Pay one of the partner account’s open invoices by card in the portal |
PUT /billing/profile |
Save the partner’s own invoice details |
GET /billing/invoices, GET /billing/adjustments |
The partner account’s invoice history, and the credits applied to it |
GET /usage, GET /usage/export |
Per-client seat counts, and the CSV for rebilling |
GET /usage/periods |
The periods already invoiced, as reconciliation windows for the report. Empty rather than an error when the invoices cannot be read |
GET/PUT /staff, DELETE /staff/:email |
Partner staff and their roles |
GET/PUT /preferences |
Whether the partner’s own Owners and Admins receive product update emails. Owners only, like the staff list |
GET /events |
The partner-level audit trail |
POST /agreement/accept |
Record acceptance of the partner agreement |
Inviting a client, requesting a transfer and releasing a client are all refused until the partner agreement has been accepted, so a partner cannot take a client on before agreeing the terms they are taking them on under.
Working inside a client uses the ordinary tenant endpoints above, with the client’s context established by the partner relationship. See the partner programme.
The client’s own half of that relationship lives under /api/admin/managed. The
two writes answer only an Admin of the managed tenant, never their partner.
GET /status answers anyone signed in to the tenant.
| Route | Purpose |
|---|---|
GET /status |
Who manages this organisation, and any pending request to |
POST /transfer/:id/approve, …/decline |
Answer a request to take the organisation over |
POST /partner/revoke |
End the partner relationship |
Public endpoints
Section titled “Public endpoints”| Route | Purpose |
|---|---|
GET /admin |
The portal single-page app |
GET /admin/config.js |
The portal’s public MSAL configuration |
GET /admin/consent |
Redirect to Microsoft admin consent |
GET /admin/consent/callback |
Provisioning callback |
GET /pricing |
Public pricing page |
GET /privacy, /terms, /support, /dpa, /partner-agreement |
Published legal and support pages |
GET /health |
Configuration and storage check |
GET /r/:slug |
Tracked link redirect, served on e-clk.usesigil.app |
GET /vcf/:token.vcf |
The sender’s contact card, as a downloadable vCard |
GET/POST /email-preferences/:token |
Where one address stands on product update emails. The GET only shows it; the change is the POST |
The contact card route carries no mailbox in its URL. The token is signed, and one is only ever minted while rendering that mailbox’s own signature, so the route cannot be walked to enumerate a directory. A malformed token and a forged one both return 404, so probing it reveals nothing about which of the two it hit. It stops answering for a suspended or removed organisation.
POST /api/billing/stripe-webhook is authenticated by a Stripe signature rather
than by a token, and mirrors subscription state locally.
Status codes worth knowing
Section titled “Status codes worth knowing”| Code | Meaning on the signature path |
|---|---|
| 200 | A signature was rendered and returned |
| 401 | The token failed verification |
| 402 | Billing is not active. The add-in applies nothing |
| 403 | The organisation is not connected, or this mailbox has been excluded. The add-in applies nothing |
| 404 | The mailbox did not resolve in the directory |
| 413 | The request body was over 16 KB |
| 429 | This mailbox has made more than 120 signature requests in the last minute. The answer carries Retry-After: 60, and the add-in applies nothing |
A 402 across an entire organisation is almost always a lapsed trial or a past-due subscription. See troubleshooting.
The two causes of a 403 are distinguishable from the server’s own records but not by the add-in, which treats both as a reason to apply nothing. An excluded mailbox therefore reports the same way in the pane as an unconnected organisation.
Outside the signature path, one more code is worth handling in a script. While
Tophhie Cloud support is
moving your organisation’s data between databases,
a request that would change your templates, rules, users or settings is
answered 503 with
Retry-After: 120 and the error “Your organisation’s data is being moved. Try
again in a couple of minutes.” Reads keep working. The move takes a few
minutes, so waiting for the Retry-After and trying once more is enough.
