Docs
SDKs and tools

TypeScript SDK

The official TypeScript and JavaScript library for the Agoo API, for Node.js, Bun, Deno and Cloudflare Workers.

Planned· P9For developers

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

RuntimeSupported
Node.js20 or later
Bun1.1 or later
Deno2 or later, through npm:@ardent-africa/agoo
Cloudflare WorkersYes, and other edge runtimes with fetch and Web Crypto
BrowsersNo. 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/agoo

Create a client

agoo.ts
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.

invite.ts
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… expected

Resources

Every v1 endpoint has a method. Methods that take an ID take it as the first argument.

NamespaceMethodsEndpoints
agoo.organisationretrieve()GET /organisation
agoo.siteslist(params?), retrieve(siteId)/sites
agoo.peoplelist(params?), create(body), retrieve(personId), update(personId, body), deactivate(personId)/people (deactivate is DELETE)
agoo.visitslist(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.visitorslist(params?), retrieve(visitorId), erase(visitorId)/visitors (erase is DELETE)
agoo.deliverieslist(params?), create(body), collect(deliveryId, body?)/deliveries
agoo.bookingTypeslist(params?), availability(btypeId, params)/booking-types
agoo.bookingslist(params?), create(body), retrieve(bookingId), confirm(bookingId), decline(bookingId, body?), reschedule(bookingId, body), cancel(bookingId, body?)/bookings
agoo.attendance.eventslist(params?), create(body)/attendance/events
agoo.attendancesummary(params)GET /attendance/summary
agoo.shiftslist(params?)GET /shifts
agoo.watchlistlist(params?), create(body), delete(watchId)/watchlist
agoo.rollCallslist(params?), create(body), retrieve(rollcallId), close(rollcallId)/roll-calls
agoo.webhookEndpointslist(params?), create(body), retrieve(whepId), update(whepId, body), delete(whepId), rotateSecret(whepId, body?), test(whepId, body?)/webhook-endpoints
agoo.eventslist(params?), retrieve(evtId), redeliver(evtId)/events
agoo.auditEventslist(params?)GET /audit-events
agoo.visitTypeslist(params?), retrieve(key)/visit-types
agoo.formslist(params?), retrieve(formId)/forms
agoo.webhooksverify(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().

One page
const page = await agoo.visits.list({ status: "checked_in", limit: 50 })

page.data // Visit[]
page.has_more // boolean
page.next_cursor // string | null
Every page
for 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:

Manual cursor
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:

Your own key
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.

Handling errors
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 in Retry-After
  • the API returns 500 internal_error or 503 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:

Per-request options
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():

Headers
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.

app/webhooks/agoo/route.ts
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:

worker.ts
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:

Types
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:

Typed custom fields
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.

On this page