Pagination and rate limits#

Doctor searches use forward-only cursor pagination. A response contains:

{
  "page": {
    "count": 25,
    "hasMore": true,
    "nextCursor": "eyJhZnRlciI6…"
  }
}

Pass nextCursor unchanged in the next request. A cursor is bound to its country, city, text, WhatsApp, and verification filters. Changing a filter while reusing the cursor returns 400.

const endpoint = new URL('https://www.doctosan.com/api/v1/doctors')
endpoint.searchParams.set('country', 'es')
endpoint.searchParams.set('city', 'madrid')
endpoint.searchParams.set('whatsapp', 'true')
endpoint.searchParams.set('limit', '100')

let cursor
do {
  if (cursor) endpoint.searchParams.set('cursor', cursor)
  const response = await fetch(endpoint, {
    headers: { 'X-API-Key': process.env.DOCTOSAN_API_KEY },
  })
  if (response.status === 429) {
    const retryAfter = Number(response.headers.get('Retry-After') || 1)
    await new Promise(resolve => setTimeout(resolve, retryAfter * 1000))
    continue
  }
  if (!response.ok) throw new Error(`Doctosan API ${response.status}`)

  const page = await response.json()
  await persistCandidates(page.doctors)
  cursor = page.page.nextCursor || undefined
} while (cursor)

Cursor rules#

  • Treat cursors as opaque; their encoding can change.
  • Do not persist them as durable record identifiers.
  • Stop when nextCursor is null, regardless of count.
  • A deleted or newly published record can shift a later traversal; deduplicate durable imports by doctor id.

Rate limits#

The default authenticated limit is 300 requests per 60 seconds per API key. Read the response headers instead of hard-coding that number: Doctosan may assign a different limit to a key. The limiter is shared across concurrent API instances.