API reference

NerdPortal HTTP API

The endpoints behind the guest portal and the dashboard, with the exact request and response shapes they use.

Overview

All endpoints are served from your admin host and return JSON unless noted otherwise:

https://nerdportal.unifinerds.com

They fall into three groups. Portal endpoints are public — they are what a guest device talks to. Dashboard endpoints back the admin UI and require a signed-in session. Webhooks are inbound only, called by Authorize.Net.

On public API keys

Pro and Enterprise list API access as a plan feature. Key-based authentication is not available yet — the endpoints below authenticate with a session cookie, which makes them practical for first-party use but not for server-to-server integrations. If you are building against NerdPortal, tell us what you need; it shapes what we ship.

Authentication

Dashboard endpoints use the session cookie issued when you sign in. There are two roles, and they are checked on every request:

  • client — scoped to a single account. The account is taken from the session, never from the request, so one client cannot read another's data by changing an ID
  • superadmin — platform administration

A request with no session returns 401. A request with a valid session but the wrong role returns 403.

Portal endpoints are unauthenticated by design — a guest has no network access yet, so they cannot present a credential. They are scoped by the subdomain in the request.

Portal endpoints

Public. These are what a captive portal page calls.

GET/api/portal/{subdomain}/settingsPublic

Returns the branding and form configuration for a portal. Responds with defaults if the account has not customized anything yet.

{
  "businessName": "Demo Coffee Shop",
  "logoUrl": null,
  "backgroundColor": "#0a0f1c",
  "primaryColor": "#00d4ff",
  "textColor": "#ffffff",
  "welcomeTitle": "Welcome to Our WiFi",
  "welcomeMessage": "Connect to our free WiFi...",
  "termsUrl": null,
  "privacyUrl": null,
  "collectPhone": false,
  "collectBirthdate": false,
  "requireTerms": true,
  "customCss": null
}

Returns 404 if the subdomain does not exist or the account is inactive.

POST/api/portal/submitPublic

Records a guest submission and authorises the device on the account's UniFi controller. subdomain, firstName, lastName and email are required; the rest are optional.

{
  "subdomain": "demo",
  "firstName": "Ada",
  "lastName": "Lovelace",
  "email": "ada@example.com",
  "phone": "+1 555 0100",
  "birthdate": "1815-12-10",
  "macAddress": "00:11:22:33:44:55",
  "apMac": "aa:bb:cc:dd:ee:ff",
  "redirectUrl": "https://example.com"
}

Returns 201:

{
  "success": true,
  "message": "Submission successful",
  "redirectUrl": "https://yourbusiness.com"
}

Controller authorisation is best-effort: if the controller is unreachable or the stored credentials are stale, the submission is still saved and the response is still 201. Check the authorized flag on the submission to tell the two cases apart.

Returns 429 when the account has used its plan's submission allowance for the current billing period, and 404 if the subdomain does not exist or is inactive.

Dashboard endpoints

All require a signed-in client session and operate on that session's account.

Submissions

GET/api/admin/submissionsclient session

Paginated submissions. Query parameters: page (default 1), limit (default 15, max 100), status (authorized or pending), dateRange (default 7days), search, sortBy (default createdAt) and sortOrder (default desc).

{
  "submissions": [ ... ],
  "pagination": { "page": 1, "limit": 15, "total": 0, "totalPages": 0 },
  "summary": { "total": 0, "authorized": 0, "pending": 0, "withPhone": 0 }
}
GET/api/admin/submissions/exportclient session

Returns a file download rather than a JSON envelope. Query parameters: format (csv or json, default csv), dateRange (default all) and status. Any other format returns 400.

GET/api/admin/statsclient session

Dashboard aggregates: totals, a 7-day series, hourly distribution and recent rows.

{
  "stats": { "totalSubmissions": 0, "todaySubmissions": 0,
             "averagePerDay": 0, "peakHour": "14:00" },
  "chartData": [ { "day": "Mon", "submissions": 0 } ],
  "hourlyData": [ { "hour": "00", "count": 0 } ],
  "recentSubmissions": [ ... ]
}

Portal configuration

GET/api/admin/brandingclient session

Current portal settings: colors, welcome copy, logoUrl, termsUrl, privacyUrl, redirectUrl, sessionDuration, the collectPhone, collectBirthdate and requireTerms toggles, and customCss.

PUT/api/admin/brandingclient session

Updates any subset of the above and echoes the saved settings back.

UniFi connection

GET/api/admin/unificlient session

Returns controllerUrl, username, siteId, isVerified and lastVerified. The password is always returned as "********" — the stored value is encrypted and is never sent back.

PUT/api/admin/unificlient session

Accepts controllerUrl (must be a valid URL), username, password and siteId. Send the literal "********" as the password to leave the stored one unchanged. Any change to the connection details clears isVerified; the response sets needsVerification accordingly.

POST/api/admin/unifi/verifyclient session

Signs in to the stored controller and requests site health. On success, marks the connection verified and stamps lastVerified. Returns 400 if no settings are stored or they are incomplete.

Domains

GET/api/admin/domainclient session

Returns subdomain, customDomain, customDomainVerified, the defaultUrl and the activeUrl currently serving the portal.

PUT/api/admin/domainclient session

Body is { customDomain, useCustomDomain }. Setting useCustomDomain to false clears the domain. Changing the domain resets its verified status. A domain already claimed by another account returns 400.

POST/api/admin/domain/verifyclient session

Body is { domain }. Resolves the domain's CNAME and checks it points at our portal host. On failure the response includes the record you need to create:

{
  "verified": false,
  "message": "No CNAME record found for this domain",
  "expectedRecord": {
    "type": "CNAME",
    "name": "wifi.yourbusiness.com",
    "value": "portal.unifinerds.xyz"
  }
}

Account

GET/api/admin/billingclient session

Current subscription, usage against the plan allowance, the last 10 payments and the list of available plans.

GET/api/admin/settingsclient session

Returns businessName, email, subdomain and createdAt. The matching PUT updates the profile or changes the password; a wrong current password returns 400.

GET/api/admin/setup/completeclient session

Onboarding status. POST to the same path marks the setup wizard complete.

Webhooks

POST/api/webhooks/authnetSignature

Receives Authorize.Net notifications. Every request must carry an X-ANET-Signature header, which is verified as an HMAC-SHA512 of the raw body against the configured signature key. A mismatch returns 401 and the payload is discarded.

Handled events:

  • payment.authcapture.created and payment.priorAuthCapture.created — mark the payment completed
  • payment.refund.created — mark it refunded
  • payment.void.created, payment.declined, payment.fraud.declined — mark it failed
  • customer.subscription.cancelled and .terminated — cancel the subscription
  • customer.subscription.suspended — move it to past due

Unrecognised events are acknowledged and ignored.

Errors

Errors return a JSON body with a message field describing what went wrong. Validation failures report the first field that failed.

  • 400 — validation failed, or the request was structurally wrong
  • 401 — no valid session
  • 403 — signed in, but the wrong role for this endpoint
  • 404 — the portal or record does not exist, or the account is inactive
  • 429 — the plan's submission allowance for the period is used up
  • 500 — something failed on our side