Authentication
Authenticate with secret or publishable API keys for your own integrations, or with OAuth 2.1 and PKCE for apps that other organisations connect.
This is a published preview. Names and fields may still change before general availability; changes will be listed in the changelog.
Every request to the Agoo API carries a bearer token in the Authorization header:
GET /v1/sites HTTP/1.1
Host: api.agoo.ardent.africa
Authorization: Bearer agoo_sk_test_…The token is one of two things:
| Credential | Who it's for | What it can do |
|---|---|---|
| API key | Your own integrations with your own organisation, such as your HR system or CRM | Whatever its scopes allow, across your organisation, in one mode (live or test) |
| OAuth access token | Apps that other organisations connect, and MCP clients such as AI assistants | Whatever its scopes and the signed-in person's role allow |
Agoo only accepts credentials in the Authorization header. There's no query-string or cookie alternative, so keys never end up in URLs, server logs or browser history.
API keys
Secret and publishable keys
| Prefix | Kind | Use it in | Can call |
|---|---|---|---|
agoo_sk_live_ | Secret, live | Your servers only | Any endpoint its scopes allow |
agoo_sk_test_ | Secret, test | Your servers and CI | The same, in test mode |
agoo_pk_live_ | Publishable, live | Browsers and mobile apps | Create pre-registrations and bookings; read public booking types and their free slots, and the visit types open for pre-registration |
agoo_pk_test_ | Publishable, test | Browsers and mobile apps | The same, in test mode |
A secret key can read and change your organisation's data, including personal data about visitors and staff. Treat it like a password: it belongs on a server, never in a web page, a mobile app or a kiosk.
A publishable key is safe to ship in client code because it can only do what a member of the public can already do on your booking and pre-registration pages:
- GET/visit-types and
GET/visit-types/{visit_type}, only for visit types that are turned on and open for
pre-registration, each with its
public_form: the questions asked of the visitor, never the fields your staff fill in - POST/visits, which creates a pre-registration (
source: "pre_registration") for a visit type open for pre-registration, with only the fields in itspublic_form - GET/booking-types, public booking types only, each with its
public_formof intake questions - GET/booking-types/{btype_id}/availability, for public booking types
- POST/bookings, for public booking types, with only the fields in their
public_form
Any other call with a publishable key returns forbidden, including GET /forms (full forms contain the fields your staff fill in) and GET /people (your staff directory is never readable from a web page). A visit type that isn't open for pre-registration returns not_found from GET /visit-types/{visit_type} and validation_failed from POST /visits. Admins open a type for pre-registration in Console → Settings → Forms (Forms and fields).
Live and test keys
The key decides the mode. Live and test mode share one base URL, https://api.agoo.ardent.africa/v1. A _test_ key works on a separate sandbox copy of your organisation where no SMS, WhatsApp or email is sent and nothing is billed. See Environments and test mode.
Create a key
Owners and Admins create keys.
Open Console → Developers → API keys
Choose Create key.
Name it after what uses it
For example "HR sync (production)" or "Front desk integration (CI)". The name appears in the audit trail next to everything the key does.
Choose the kind, the mode and the scopes
Choose secret or publishable, live or test, and tick only the scopes the integration needs. Publishable keys have a fixed set of permissions.
Copy the key now
Agoo shows the full key once. Store it straight away in your secret manager or deployment environment, for example as AGOO_API_KEY. After this, the console shows only the key's prefix, its last four characters and when it was last used.
Plans
Test keys work on every plan. Live secret keys need a plan that includes the API: Growth can manage webhook endpoints and
read events; Pro and Enterprise get the full API. A live call above your plan returns plan_required.
Live publishable keys work in the embed script and the WordPress plugin's blocks on every plan that has booking pages, because those show your hosted pages in a frame. Calling the API directly with a publishable key, as the React components do, needs Pro or Enterprise in live mode.
You can't change a key's scopes after you create it. To give an integration more or fewer permissions, create a new key and rotate to it.
Store keys safely
- Keep keys in environment variables or a secret manager, never in source code. Agoo's prefixes (
agoo_sk_live_and friends) make leaked keys easy for secret scanners to spot. - Give each integration its own key, so you can see what each one does in the audit trail and revoke one without breaking the others.
- Give each key the fewest scopes it needs. A payroll export needs
attendance:read, notpeople:write. - Never log the
Authorizationheader. If you log requests, log theAgoo-Request-Idresponse header instead. - Never send a secret key to a browser, a mobile app or a kiosk. If a page needs to create bookings, use a publishable key; if it needs more, call your own server and let your server call Agoo.
Rotate a key
Rotate keys when someone who had access leaves, on your regular schedule, or straight away if you think a key has leaked.
Create the replacement
Create a new key with the same mode and scopes.
Deploy it
Update the secret wherever the old key is used and redeploy.
Check the old key has gone quiet
In Console → Developers → API keys, the old key's last used time stops moving once nothing uses it.
Revoke the old key
Choose Revoke on the old key.
Revoke a key
Revoking takes effect at once: every request with that key returns unauthorized, and revoking can't be undone. If a key leaks, revoke it first and create the replacement second; a few minutes of failed calls are better than an open door. Revocations are recorded in the audit trail.
IP allow-lists (Enterprise)
Enterprise organisations can limit API access to the IP ranges they list. A request from any other address returns forbidden, even with a valid key.
OAuth 2.1 for apps
Use OAuth when you build an app that other organisations connect to their Agoo, or when a person connects an MCP client such as an AI assistant. The app never sees anyone's password or API key: each organisation grants it a token, limited to the scopes it asked for, that the organisation can revoke at any time.
Agoo follows OAuth 2.1: the authorization code flow with PKCE (S256 only), exact redirect URI matching, short-lived access tokens and refresh tokens that rotate on every use. The implicit and password grants aren't supported.
| Endpoint | URL |
|---|---|
| Authorization | https://app.agoo.ardent.africa/oauth/authorize |
| Token | https://api.agoo.ardent.africa/oauth/token |
| Server metadata (RFC 8414) | https://api.agoo.ardent.africa/.well-known/oauth-authorization-server |
| Dynamic client registration | https://api.agoo.ardent.africa/oauth/register (for MCP clients that can't use a metadata document) |
The metadata document lists everything else, including the revocation endpoint, the client authentication methods the token endpoint accepts and client_id_metadata_document_supported: true, which tells MCP clients they can register with a Client ID Metadata Document. Read those values from it rather than hard-coding them.
Register your app
Register your app in Console → Developers on your own Agoo organisation (Pro or Enterprise). You get a client ID and, for apps that run on a server, a client secret, shown once.
- Redirect URIs must match exactly, including the path and any trailing slash. Use HTTPS;
http://localhostis allowed while you develop. - Confidential clients run on a server and keep a client secret. Public clients, such as desktop and mobile apps, have no secret and rely on PKCE alone.
- MCP clients register themselves, so you don't need to do anything. See How MCP clients register.
How MCP clients register
AI assistants and other MCP clients connect to many servers they have no prior relationship with, so they identify themselves when they first connect. Agoo supports the two ways the MCP authorization specification (revision 2026-07-28) describes:
| Method | How it works | Status in the MCP spec |
|---|---|---|
| Client ID Metadata Document (preferred) | The client's client_id is an HTTPS URL with a path, such as https://assistant.example/oauth/client.json. Agoo fetches the JSON document at that URL, checks that its client_id equals the URL, and accepts only the redirect_uris it lists. No registration call is needed. | Recommended |
| Dynamic client registration (RFC 7591) | The client posts its details to https://api.agoo.ardent.africa/oauth/register and gets a client_id back. | Deprecated; kept for clients without metadata documents |
A metadata document needs at least client_id, client_name and redirect_uris. Agoo caches it as its HTTP cache headers allow, shows the client's name and the redirect address's host name on the consent screen, and warns when the only redirect addresses are on localhost. Not every MCP client supports metadata documents yet; those that don't use dynamic client registration, which works the same for you. Either way, an admin approves the client for your organisation before it gets a token. See Connect an MCP client.
Walk through the authorization code flow
The example below is Coastline Consult's CRM connecting to Volta Bank's Agoo, so it can invite Coastline's consultants as visitors.
Create a PKCE verifier, a challenge and a state
Make a fresh set for every sign-in, and keep the verifier and state in the person's session.
import { createHash, randomBytes } from "node:crypto"
export function createPkce() {
const codeVerifier = randomBytes(32).toString("base64url") // 43 characters
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url")
const state = randomBytes(16).toString("base64url")
return { codeVerifier, codeChallenge, state }
}Send the person to Agoo
Redirect their browser to the authorization endpoint. Ask only for the scopes you need, separated by spaces.
GET https://app.agoo.ardent.africa/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fcrm.coastline.example%2Fagoo%2Fcallback
&scope=visits%3Aread%20visits%3Awrite%20people%3Aread
&state=kN3vQ8tWzR1yLp6s
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256The person signs in and approves
They sign in to Agoo, choose the organisation and see your app's name and the scopes it asks for.
The first time an app is connected to an organisation, an Owner or Admin must approve it for that organisation. If the person isn't an admin, Agoo tells them to ask one. Once approved, the app appears in the organisation's console, where admins can revoke it at any time.
Handle the callback
Agoo redirects back to your redirect URI:
GET https://crm.coastline.example/agoo/callback?code=AUTHORIZATION_CODE&state=kN3vQ8tWzR1yLp6sCheck that state matches the one you stored, and stop if it doesn't. If the person declined, you get error=access_denied instead of a code. Codes are single-use and expire after a short time, so exchange them straight away.
Exchange the code for tokens
Post the code and the verifier to the token endpoint as a form. Confidential clients also authenticate, here with HTTP Basic (client_secret_basic); public clients send client_id in the form instead.
curl https://api.agoo.ardent.africa/oauth/token \
-u "$AGOO_CLIENT_ID:$AGOO_CLIENT_SECRET" \
-d grant_type=authorization_code \
-d code="$AUTHORIZATION_CODE" \
-d redirect_uri=https://crm.coastline.example/agoo/callback \
-d code_verifier="$CODE_VERIFIER"{
"access_token": "<access token>",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "<refresh token>",
"scope": "visits:read visits:write people:read"
}The granted scope can be narrower than you asked for, so check it. Store the refresh token encrypted at rest. Treat both tokens as opaque strings; their format may change.
Call the API
Use the access token exactly like an API key:
curl https://api.agoo.ardent.africa/v1/visits?status=expected \
-H "Authorization: Bearer $ACCESS_TOKEN"An access token acts for the person who connected the app. It can do only what both its scopes and that person's role allow: a host's token can't approve visits for other hosts, whatever its scopes. A call the role doesn't allow returns forbidden; a missing scope returns scope_missing.
Refresh tokens
Access tokens last 1 hour. Before one expires, or when a call returns unauthorized, get a new one:
curl https://api.agoo.ardent.africa/oauth/token \
-u "$AGOO_CLIENT_ID:$AGOO_CLIENT_SECRET" \
-d grant_type=refresh_token \
-d refresh_token="$REFRESH_TOKEN"Refresh tokens rotate: every refresh returns a new refresh token, and the one you sent stops working. Save the new one before you use the new access token. If an old refresh token is ever presented again, Agoo treats it as stolen and revokes the whole grant, so the person has to connect again. Two servers refreshing the same grant at once can trigger this, so refresh in one place, under a lock.
Errors from the token endpoint follow OAuth (RFC 6749), not problem details: for example { "error": "invalid_grant" } when a code or refresh token is expired, used or revoked. When you get invalid_grant on a refresh, send the person through the authorization flow again.
Revocation
- Your app can revoke a token it no longer needs, for example when a customer disconnects Agoo in your product. Post it to the
revocation_endpointfrom the server metadata (RFC 7009). Revoking a refresh token also revokes the access tokens issued from it. - An organisation's admins can revoke your app at any time from the console. Its tokens stop working at once and further calls return
unauthorized; your app should show the customer that the connection has ended.
Choosing between keys and OAuth
| You're building | Use |
|---|---|
| A script, sync job or integration for your own organisation | A secret API key |
| A booking widget or pre-registration form on your website | A publishable key |
| A product that many organisations connect to their Agoo | OAuth |
| Something that acts as a specific person and respects their role | OAuth |
| An AI assistant or other MCP client | OAuth (handled for you by the MCP server) |
| Tests in CI | A secret test key (agoo_sk_test_…) |
Scopes
Scopes apply to API keys and OAuth tokens alike. Enterprise organisations can agree custom scopes with Ardent.
| Scope | Allows |
|---|---|
organisation:read | Read the organisation's name, plan and wallet balance |
sites:read | Read sites, gates and paired kiosks |
people:read | Read hosts and employees |
people:write | Create, update and deactivate hosts and employees |
visits:read | Read visits |
visits:write | Create and update visits; check in, check out, approve, deny, cancel; resend passes |
visitors:read | Read visitor records |
visitors:delete | Permanently erase a visitor's personal data |
deliveries:read | Read deliveries |
deliveries:write | Record deliveries and mark them collected |
bookings:read | Read booking types, free slots and bookings |
bookings:write | Create, reschedule, cancel, confirm and decline bookings |
attendance:read | Read clock-ins and clock-outs, attendance summaries and shifts |
attendance:write | Record clock-ins and clock-outs from your own terminals |
attendance:manage | Change attendance settings, record acknowledgements and consents, review punches |
watchlist:read | Read the watchlist |
watchlist:write | Add and remove watchlist entries |
rollcalls:read | Read roll calls and who is accounted for |
rollcalls:write | Start and close roll calls |
webhooks:manage | Manage webhook endpoints, rotate secrets, send test events and redeliver events |
events:read | Read events |
audit:read | Read the audit trail |
forms:read | Read visit types, forms and their custom field schemas |
files:read | Read files and get short-lived download links |
files:write | Register file uploads (photos, selfies, signatures, documents) |
Each endpoint in the API reference lists the scope it needs.
A write scope lets you read the objects it creates or changes in the response. For example, a key with only people:write gets the person back from POST /people and PATCH /people/{person_id}, and a key with only visits:write gets the visit back from POST /visits and the visit actions. It doesn't let you list or fetch those objects: that still needs the read scope.
Related
Make your first API call
Create a test key, list your sites, invite a visitor with an idempotent request, read the visit back, and receive a signed test webhook.
Environments and test mode
Build against a sandbox copy of your organisation with a test key, where no messages are sent and nothing is billed, then switch to a live key.