TypeScript SDK
The official TypeScript and JavaScript library for the Agoo API, for Node.js, Bun, Deno and Cloudflare Workers.
This is designed and scheduled but not built yet. We document it now so you can plan your integration.
@ardent-africa/agoo is a typed client for the Agoo REST API. It's generated from the same OpenAPI contract as the API reference, so every endpoint, parameter and response has a TypeScript type, and it adds the things you would otherwise write yourself: pagination, idempotency keys, retries, timeouts, typed errors and webhook verification.
The install commands on this page will work once the package is published to npm.
Requirements
| Runtime | Supported |
|---|---|
| Node.js | 20 or later |
| Bun | 1.1 or later |
| Deno | 2 or later, through npm:@ardent-africa/agoo |
| Cloudflare Workers | Yes, and other edge runtimes with fetch and Web Crypto |
| Browsers | No. The SDK uses secret keys, which must never reach a browser. Use the React components or the embed on web pages. |
The SDK has no runtime dependencies. It uses the platform's fetch and Web Crypto. TypeScript 5.0 or later is recommended for the types.
Install
npm i @ardent-africa/agooCreate a client
import { Agoo } from "@ardent-africa/agoo"
export const agoo = new Agoo({ apiKey: process.env.AGOO_API_KEY })Keep your key in an environment variable and only use the client in server code. A key containing _test_ works on your test-mode sandbox, where no SMS, WhatsApp or email is sent; a _live_ key works on your real data. The code is the same for both. See environments.
Prop
Type
Publishable keys (agoo_pk_…) are rejected when you create the client: they are for browser pages, not server code.
Make a request
Methods mirror the API's resources and actions. Paths and query parameters become method arguments; request and response bodies keep the API's snake_case field names, so what you read in the API reference is what you write in code.
import { agoo } from "./agoo"
const visit = await agoo.visits.create({
site_id: "site_01kjpt3yw0fz0v414608h9x65s", // Ridge HQ
host_id: "person_01kjsesxm0e4zbyvkr7bcfxbfg", // Kwame Mensah
type: "meeting",
expected_at: "2026-10-14T09:30:00Z",
visitor: {
name: "Ama Owusu",
phone: "+233241234567",
email: "ama@coastline.example",
company: "Coastline Consult",
},
purpose: "Quarterly review",
})
console.log(visit.id, visit.status) // visit_01j… expectedResources
Every v1 endpoint has a method. Methods that take an ID take it as the first argument.
| Namespace | Methods | Endpoints |
|---|---|---|
agoo.organisation | retrieve() | GET /organisation |
agoo.sites | list(params?), retrieve(siteId) | /sites |
agoo.people | list(params?), create(body), retrieve(personId), update(personId, body), deactivate(personId) | /people (deactivate is DELETE) |
agoo.visits | list(params?), create(body), retrieve(visitId), update(visitId, body), checkIn(visitId, body?), checkOut(visitId, body?), approve(visitId, body?), deny(visitId, body?), cancel(visitId, body?), resendPass(visitId, body?) | /visits and its actions |
agoo.visitors | list(params?), retrieve(visitorId), erase(visitorId) | /visitors (erase is DELETE) |
agoo.deliveries | list(params?), create(body), collect(deliveryId, body?) | /deliveries |
agoo.bookingTypes | list(params?), availability(btypeId, params) | /booking-types |
agoo.bookings | list(params?), create(body), retrieve(bookingId), confirm(bookingId), decline(bookingId, body?), reschedule(bookingId, body), cancel(bookingId, body?) | /bookings |
agoo.attendance.events | list(params?), create(body) | /attendance/events |
agoo.attendance | summary(params) | GET /attendance/summary |
agoo.shifts | list(params?) | GET /shifts |
agoo.watchlist | list(params?), create(body), delete(watchId) | /watchlist |
agoo.rollCalls | list(params?), create(body), retrieve(rollcallId), close(rollcallId) | /roll-calls |
agoo.webhookEndpoints | list(params?), create(body), retrieve(whepId), update(whepId, body), delete(whepId), rotateSecret(whepId, body?), test(whepId, body?) | /webhook-endpoints |
agoo.events | list(params?), retrieve(evtId), redeliver(evtId) | /events |
agoo.auditEvents | list(params?) | GET /audit-events |
agoo.visitTypes | list(params?), retrieve(key) | /visit-types |
agoo.forms | list(params?), retrieve(formId) | /forms |
agoo.webhooks | verify(rawBody, headers, secret) | Verifies a webhook you received; no API call |
Each call needs the scope its endpoint needs. A missing scope throws an AgooError with code scope_missing. See authentication.
Pagination
List methods return one page that you can await, or iterate through every page with autoPaginate().
const page = await agoo.visits.list({ status: "checked_in", limit: 50 })
page.data // Visit[]
page.has_more // boolean
page.next_cursor // string | nullfor await (const visit of agoo.visits.list({ status: "checked_in" }).autoPaginate()) {
console.log(visit.visitor.name, visit.checked_in_at)
}autoPaginate() fetches the next page only when you reach the end of the current one, so you can break at any point without fetching more. Each page is one request and counts towards your rate limit; use the largest limit (100) when you know you'll read everything.
To control the cursor yourself, for example in a job that resumes where it stopped:
let cursor: string | undefined
do {
const page = await agoo.people.list({ limit: 100, cursor })
await saveToHr(page.data)
cursor = page.next_cursor ?? undefined
} while (cursor)See pagination.
Idempotency
Every POST the SDK sends carries an Idempotency-Key. If you don't pass one, the SDK generates a random UUID for the call and reuses it on its own retries, so a retried request never creates a second visit or booking.
Pass your own key when the same operation might be attempted again from a different process, for example by a job queue after a crash. Derive it from something stable in your own system:
await agoo.visits.create(
{
site_id: "site_01kjpt3yw0fz0v414608h9x65s",
host_id: "person_01kjsesxm0e4zbyvkr7bcfxbfg",
expected_at: "2026-10-14T09:30:00Z",
visitor: { name: "Ama Owusu", phone: "+233241234567" },
},
{ idempotencyKey: `crm-meeting-${meeting.id}` },
)Keys last 24 hours. Reusing a key with a different body throws idempotency_key_reused. See idempotency.
Errors
Every failure throws an AgooError. API errors carry the fields of the problem details response.
import { AgooError } from "@ardent-africa/agoo"
try {
await agoo.visits.checkIn(visitId)
} catch (err) {
if (!(err instanceof AgooError)) throw err
switch (err.code) {
case "invalid_state":
// Already checked in, cancelled or denied
break
case "validation_failed":
for (const e of err.errors) console.warn(e.location, e.message)
break
case "rate_limited":
// Only thrown after the SDK's own retries are used up
break
default:
console.error(`Agoo ${err.status} ${err.code}: ${err.detail} (request ${err.requestId})`)
throw err
}
}Prop
Type
Match on code, not on title or detail, which can be reworded.
Retries and timeouts
The SDK retries a request up to maxRetries times (2 by default) when:
- the connection fails or the attempt times out
- the API returns
429 rate_limited, waiting for the number of seconds inRetry-After - the API returns
500 internal_erroror503 unavailable
Other errors are never retried, because sending the same request again would fail the same way. Between attempts the SDK waits with exponential backoff and jitter, starting at half a second. POST requests are safe to retry because of their idempotency keys.
Override the defaults for one call:
const summary = await agoo.attendance.summary(
{ from: "2026-09-01", to: "2026-09-30", site_id: "site_01jx45hy43kwjrp1xpa7z3dj8f" },
{ timeout: 60_000, maxRetries: 4 },
)Every method accepts these request options as its last argument:
Prop
Type
Reading response headers
To read headers such as Agoo-Request-Id or RateLimit-Remaining on a successful call, use withResponse():
const { data: page, response } = await agoo.sites.list().withResponse()
console.log(response.headers.get("Agoo-Request-Id"))
console.log(response.headers.get("RateLimit-Remaining"))Verify webhooks
agoo.webhooks.verify checks a webhook's Standard Webhooks signature and timestamp and returns the typed event. It throws if the signature is wrong or the timestamp is more than 5 minutes old. It's the same function as verifyWebhook in @ardent-africa/agoo-webhooks, which you can install on its own if you only receive webhooks.
import { agoo } from "@/lib/agoo"
export async function POST(req: Request) {
const body = await req.text() // the raw body, exactly as sent
let event
try {
event = await agoo.webhooks.verify(body, req.headers, process.env.AGOO_WEBHOOK_SECRET!)
} catch {
return new Response("Invalid signature", { status: 400 })
}
if (event.type === "visit.checked_in") {
const visit = event.data.object // typed as Visit
console.log(`${visit.visitor.name} checked in`)
}
return new Response(null, { status: 204 })
}Verify the raw body before parsing it. Parsing and re-serialising JSON changes the bytes and breaks the signature.
Edge runtimes
The SDK only uses fetch and Web Crypto, so it runs unchanged on Cloudflare Workers, Vercel's edge runtime, Deno and Bun. Edge runtimes don't always have process.env, so pass the key from your platform's environment:
import { Agoo } from "@ardent-africa/agoo"
interface Env {
AGOO_API_KEY: string
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const agoo = new Agoo({ apiKey: env.AGOO_API_KEY })
const onSite = await agoo.visits.list({ status: "checked_in", limit: 100 })
return Response.json({ on_site: onSite.data.length, more: onSite.has_more })
},
}Store the key as a secret (wrangler secret put AGOO_API_KEY), not in wrangler.toml.
Types
All request and response types are exported:
import type { AgooEvent, AttendanceEvent, Booking, Person, Visit, VisitCreateParams } from "@ardent-africa/agoo"
function isLate(visit: Visit, now = new Date()): boolean {
return visit.status === "expected" && visit.expected_at !== null && Date.parse(visit.expected_at) < now.getTime()
}Type names follow the schemas in the API reference, such as Visit, Person and AttendanceEvent; request bodies end in Params. Webhook events are a union discriminated by type, so checking event.type narrows event.data.object to the right object type.
Custom fields
Each organisation defines its own custom fields per visitor type, so the SDK types custom_fields as Record<string, unknown> by default. If you know your organisation's fields, pass a type to the method to get them typed:
type CoastlineVisitFields = {
nda_signed: boolean
vehicle_plate?: string
laptop_serial?: string
}
const visit = await agoo.visits.retrieve<CoastlineVisitFields>("visit_01m4k5z4j0fxbte6sn6e8tpgza")
visit.custom_fields.nda_signed // boolean
for await (const v of agoo.visits.list<CoastlineVisitFields>({ status: "checked_in" }).autoPaginate()) {
if (v.custom_fields.vehicle_plate) console.log(v.custom_fields.vehicle_plate)
}The generic only changes types; the SDK doesn't validate the data. Field definitions are JSON Schema documents you can read with agoo.forms.list(), so you can generate these types with a JSON Schema to TypeScript tool and keep them in step with your form builder. See custom fields.
Versioning
The SDK follows semantic versioning. Version 1 of the SDK targets /v1 of the API. New endpoints and fields arrive in minor versions; anything that would break your code waits for a new major version. While the API is in preview, the SDK is published with a -preview tag and may change with it. See versioning and the changelog.