API reference menu
Getting started
OverviewAuthentication and rate limitExamplesSending
Send a notificationPriority and repeatLinks, images and buttonsSourcesScheduled sendsWebhook templatesReceipts and replies
Receipts and callbacksRepliesTeams
Teams, roles and endpointsChannels and escalationWebhooks and emailAlert email and timelineTeam formsPlans, audit log and transferCompany sign‑in and SCIMThe web consoleAccount
Identity and moving phonesDevices, keys and historyThe Upgrade PassCompatibility
ntfy's publish APIProwl's APIAppendix
Endpoint indexErrors and status codesLimitsPriorities and fieldsGlossaryCompany sign‑in and SCIM
A team's owner connects their identity provider; employees then sign in to the console with the company account, join the team, and get a role from their provider's groups.
Company sign‑in (Okta and other OpenID Connect providers)#
PUT /v1/teams/{id}/sso(owner):slug(the name people type),issuer,client_id,client_secret(omit to keep),email_domains,groups_claim,admin_groups,sender_groups,trust_email(omit to keep; defaultfalse: accept an ID token with noemail_verifiedclaim at all, as Okta sends none, for a provider that is the company's own directory; a claim that is present must be true, and the email must still be on one of the team's domains). The server reads the provider's discovery document and keeps its endpoints.GET(admin) returns the settings and the redirect URI to register at the provider; the secret is never returned.DELETE(owner) removes it.issuermay be a bare Okta domain (yourcompany.okta.com), which means that org's own authorization server,https://yourcompany.okta.com.POST /v1/teams/{id}/sso/test(owner), the same body: the console's setup uses this instead ofPUT. The settings, client secret included, are checked and stored as a draft only this test can use, and the answer is{"url"}: open it in this browser and sign in as the owner. If that sign‑in succeeds, the draft becomes the team's settings, already confirmed, so they can be required straight away. If it fails or is not finished within 10 minutes, the draft is never used and the team's settings stay as they were.PUT /v1/teams/{id}/sso/required {"required": true}(owner): the console then needs this team's company sign‑in, and invite codes stop adding people. Refused until one company sign‑in has worked, so a wrong setting can not lock a team out. Owners are exempt (the way back in), and the app, holding an account key, is not a console session and is not gated.- Finding the company:
POST /v1/sso/discover {"email": "pat@yourcompany.com"}(no credential; 30 lookups an hour from one IP address) returns{"org": "<sign-in name>", "name": "<team name>"}when the email's domain is one a team has verified (Email domains, below) and that team has company sign‑in set up, and{"org": null}for any other well-formed address, so the answer does not tell anyone which domains have a team (a malformed one is400, too many lookups429). The console login page asks for a work email and uses it; the sign‑in name still works there too. A verified domain also lets its people sign in, as if it were listed inemail_domains. - From the app: it makes a pairing code with its own key and opens
/org/sso#org=<name>&code=<pairing code>&app=1in the system sign‑in sheet. The provider's callback then ends atnowlark://sso-done?handoff=<code>(no console session), and the app trades it:POST /v1/sso/handoff {"handoff": "<code>"}with its account key returns{"linked": true, "team": {"id", "name"}}when the company account is now this phone's, or, when that company account already belongs to the person's account from another phone,{"linked": false, "account_id", "account_key", "team"}: a new key for that account, which the app moves to. A handoff is single use, expires after two minutes, and is refused (404 invalid_handoff) for any account but the one that began the sign‑in. - Without
app=1, the same address signs a browser in to the console and links the company account to the phone that made the code, so the phone is the member and gets the alerts; a company account already linked to another Nowlark account is refused (409). A sign‑in with no code makes a browser-only account that can use the console but receives no pushes.
How it is held: authorization code with PKCE (S256), one-time state bound to the browser that started the
sign‑in (an HttpOnly nl_sso cookie; the callback in any other browser is refused, so a provider link cannot
be finished by someone else), nonce in the ID token; RS256 only,
signature from the provider's JWKS, exact issuer and audience, expiry; every request to the provider goes
through the same public-address-only, pinned, no-redirect sender as webhooks. A person is the provider's
sub under that issuer, never an email address. The email must be verified and on one of the team's
domains, and is checked once and not stored. Roles come from groups only when the team maps groups: member,
sender or admin, never owner, and the provider is then authoritative for them (removing someone from the group
demotes them at their next sign‑in). Sign‑in is OpenID Connect; SAML is not supported. Removing someone at the provider is handled by provisioning, below.
Email domains#
A team proves it owns an email domain by adding a TXT record to that domain's DNS. A verified domain is how the app finds a company's sign‑in from a work email address.
POST /v1/teams/{id}/domains {"domain": "yourcompany.com"}(owner): returns{"domain", "verified", "verified_at", "record": {"type": "TXT", "name": "yourcompany.com", "value": "nowlark-verification=…"}}. Add that TXT record at your DNS host. Adding the same domain again returns the same record. Free email domains (gmail.com, icloud.com, outlook.com and the like) are refused, and a team lists 20 at most.POST /v1/teams/{id}/domains/{domain}/verify(owner): reads the domain's TXT records and marks it verified when the record is there;409 record_not_founduntil it is (DNS changes can take a few minutes).- A domain is verified for one team only: once one team has verified it, another gets
409 domain_taken. - An admin lists them with
GET /v1/teams/{id}/domains, and the owner removes one withDELETE /v1/teams/{id}/domains/{domain}.
Automatic provisioning (SCIM 2.0)#
Company sign‑in alone could not remove people who left the company. With provisioning on, the identity provider lists who belongs and the team follows.
POST /v1/teams/{id}/scim/token(owner) turns it on and returns the bearer token once (replaces any old one; stored only as a hash). It needs the company sign‑in first.GET .../scim(admin) shows the base URL, counts and last use.DELETE .../scim(owner) turns it off and forgets the provider's list.- The provider is pointed at
<public base URL>/scim/v2with the token. Supported:ServiceProviderConfig,ResourceTypes,Schemas, andUsers(GETwithuserName,externalIdorideqfilters and paging,POST,GET,PUT,PATCH,DELETE).Groupsanswers an empty list and refuses writes (501): roles still come from the groups in the sign‑in. Errors are SCIM errors; a bad token is one 401 for every reason; a duplicateuserNameis a 409uniqueness. - Effects: while it is on, a person can sign in to the team only if they are on the provider's list and active (owners
excepted), matched on the provider's own id first and then, for a person no sign‑in has bound yet, on the work address it
gave both here and in the sign‑in, and invite codes add no one. Setting
activefalse (or deleting the user) removes the member at once by the same function a manual removal uses (their channel places, the keys they made, their copies of team alerts), ends the console sessions their company sign‑in opened, and stops them coming back. The owner is never removed this way (recorded asscim.owner_skipped). Reactivating adds no one until they sign in. Provisioning changes are in the activity log under "the identity provider". - Stored per person: work address, the provider's id, a display name, active, and (after their first sign‑in) the account they became. Members who joined by invite before provisioning was turned on are not on the list, so the provider can not remove them; a team that turns on both provisioning and "require it" locks out such members from the console until they are provisioned (the app still works).