Skip to main content

Juniro API Mandate

Effective: January 2026 Status: Active Applies to: All Juniro services, teams, and integrations


The Mandate

Inspired by the famous "Bezos API Mandate" that transformed Amazon into a services company and led to AWS, Juniro adopts the following non-negotiable principles:

1. All Services Expose APIs

Every Juniro service, feature, and data source must expose its functionality through well-documented APIs. No exceptions.

2. Services Communicate Only Through APIs

Teams and services must communicate with each other exclusively through these APIs. No direct database access, no shared memory, no backdoors.

3. APIs Are Designed to Be Externalizable

Every API must be designed from the ground up as if it will be exposed to external partners. This means:

  • Clear, consistent naming
  • Comprehensive documentation
  • Proper authentication and authorization
  • Versioning strategy
  • Rate limiting and quotas

4. Documentation Is Not Optional

An undocumented API does not exist. All APIs must have:

  • OpenAPI 3.1 specification
  • Interactive documentation (Scalar)
  • Request/response examples
  • Error code documentation
  • Authentication instructions

5. Internal Quality = External Quality

There is no "internal-only" excuse for poor API design. Internal APIs must meet the same quality bar as partner-facing APIs.


Why This Matters

Without API Mandate:               With API Mandate:
├── Spaghetti integrations ├── Clear service boundaries
├── Tribal knowledge ├── Self-documenting systems
├── "Ask John how X works" ├── Read the API docs
├── Fragile dependencies ├── Contract-based integration
├── Can't scale team ├── Teams work independently
└── Can't expose to partners └── Partner-ready from day one

Business Benefits

BenefitDescription
Partner EcosystemAny API can become a partner API with minimal effort
Team ScalabilityNew teams integrate via APIs, not meetings
Quality EnforcementDocumentation requirement catches bad design early
Future-ProofingMobile apps, partner widgets, acquisitions - all use the same APIs

API Tiers

Juniro APIs are organized into three tiers based on audience:

Tier 1: Internal APIs

Audience:    Juniro services and frontends only
Auth: Supabase JWT (user context) or service tokens
Docs: /docs (requires internal auth)
Examples: User management, admin operations, internal analytics

Tier 2: Partner APIs

Audience:    Approved partners and integrators
Auth: Partner API keys + optional user OAuth
Docs: /partner/docs (API key required)
Examples: Activity search, booking creation, webhook subscriptions
Restrictions: Rate limited, curated subset of Tier 1

Tier 3: Public APIs

Audience:    Anyone (open data)
Auth: None or optional API key for higher limits
Docs: /public/docs (no auth required)
Examples: Public activity listings, provider profiles
Restrictions: Read-only, heavily cached, strict rate limits

Tier Matrix

CapabilityInternalPartnerPublic
Read activities
Read providers
Create bookings
Read own bookings
Read all bookings
User management
Admin operations
Webhooks
Rate limit1000/min100/min20/min

API Standards

Naming Conventions

✅ DO:
/v1/activities
/v1/activities/{id}
/v1/activities/{id}/sessions
/v1/providers/{id}/activities

❌ DON'T:
/v1/getActivities
/v1/activity_list
/v1/Activities
/v1/activities/{id}/get-sessions

HTTP Methods

MethodUsageIdempotent
GETRead resource(s)Yes
POSTCreate resourceNo
PATCHPartial updateYes
PUTFull replaceYes
DELETERemove resourceYes

Response Format

All responses follow this structure:

// Success response
{
"success": true,
"data": { ... },
"meta": {
"page": 1,
"limit": 20,
"total": 100
}
}

// Error response
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Human readable message",
"details": [
{ "field": "email", "message": "Invalid email format" }
]
}
}

HTTP Status Codes

CodeMeaningWhen to Use
200OKSuccessful GET, PATCH, DELETE
201CreatedSuccessful POST
400Bad RequestValidation error, malformed request
401UnauthorizedMissing or invalid auth
403ForbiddenValid auth but insufficient permissions
404Not FoundResource doesn't exist
409ConflictDuplicate, state conflict
422UnprocessableBusiness logic error
429Too Many RequestsRate limit exceeded
500Server ErrorUnexpected error (log and alert)

Versioning

URL versioning (required):
/v1/activities
/v2/activities

Header versioning (not used):
Accept: application/vnd.juniro.v1+json ❌

Version policy:

  • v1 is the current stable version
  • Breaking changes require new version (v2)
  • Old versions supported for 12 months after deprecation
  • Non-breaking changes added to current version

Pagination

// Request
GET /v1/activities?page=2&limit=20

// Response
{
"success": true,
"data": [...],
"meta": {
"page": 2,
"limit": 20,
"total": 156,
"totalPages": 8,
"hasNext": true,
"hasPrev": true
}
}

Filtering & Sorting

// Filtering
GET /v1/activities?city=Austin&category=art&ageMin=5

// Sorting
GET /v1/activities?sort=price_asc
GET /v1/activities?sort=created_at_desc

// Combined
GET /v1/activities?city=Austin&sort=rating_desc&limit=10

Error Codes

All error codes are documented and consistent:

// Standard error codes
enum ErrorCode {
// Validation (400)
VALIDATION_ERROR = 'VALIDATION_ERROR',
INVALID_INPUT = 'INVALID_INPUT',

// Auth (401, 403)
UNAUTHORIZED = 'UNAUTHORIZED',
FORBIDDEN = 'FORBIDDEN',
TOKEN_EXPIRED = 'TOKEN_EXPIRED',

// Not Found (404)
NOT_FOUND = 'NOT_FOUND',
RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND',

// Conflict (409)
DUPLICATE = 'DUPLICATE',
ALREADY_EXISTS = 'ALREADY_EXISTS',

// Business Logic (422)
BOOKING_FULL = 'BOOKING_FULL',
REGION_MISMATCH = 'REGION_MISMATCH',
PAYMENT_REQUIRED = 'PAYMENT_REQUIRED',

// Rate Limit (429)
RATE_LIMITED = 'RATE_LIMITED',

// Server (500)
INTERNAL_ERROR = 'INTERNAL_ERROR',
}

Documentation Structure

Endpoint Documentation

Every API endpoint in Scalar/OpenAPI includes:

/v1/activities/{id}:
get:
summary: Get activity by ID
description: |
Retrieves detailed information about a specific activity,
including provider info, sessions, and reviews summary.
tags:
- Activities
- partner # Exposed to partners
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: Activity UUID
responses:
200:
description: Activity found
content:
application/json:
schema:
$ref: '#/components/schemas/ActivityResponse'
examples:
art-class:
summary: Art class example
value:
success: true
data:
id: "123e4567-e89b-12d3-a456-426614174000"
title: "Creative Art Explorers"
...
404:
description: Activity not found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'

Documentation URLs

EnvironmentInternal DocsPartner DocsOpenAPI Spec
Locallocalhost:3000/docslocalhost:3000/partner/docslocalhost:3000/openapi.json
Stagingapi.staging.juniro.com/docsapi.staging.juniro.com/partner/docsapi.staging.juniro.com/openapi.json
Productionapi.juniro.com/docsdevelopers.juniro.comapi.juniro.com/openapi.json

Scalar Integration

Why Scalar

FeatureSwagger UIScalar
Modern UI
Dark mode
Try it now
Code samplesBasicMultiple languages
SearchBasicFull-text
Mobile friendly
CustomizableLimitedFully themeable
CostFreeFree (self-hosted)

Implementation

// src/docs/index.ts
import { apiReference } from '@scalar/hono-api-reference'

export const scalarConfig = {
theme: 'purple',
layout: 'modern',
darkMode: true,
hiddenClients: ['unirest'],
defaultHttpClient: {
targetKey: 'javascript',
clientKey: 'fetch',
},
metaData: {
title: 'Juniro API',
description: 'API documentation for Juniro marketplace',
ogImage: 'https://juniro.com/og-api-docs.png',
},
}

// Internal docs (all endpoints)
export const internalDocs = apiReference({
spec: { url: '/openapi.json' },
...scalarConfig,
metaData: {
...scalarConfig.metaData,
title: 'Juniro API (Internal)',
},
})

// Partner docs (filtered)
export const partnerDocs = apiReference({
spec: { url: '/partner/openapi.json' },
...scalarConfig,
metaData: {
...scalarConfig.metaData,
title: 'Juniro Partner API',
description: 'Integrate Juniro activities into your platform',
},
})

Route Tagging for Filtering

// Tag routes for different audiences
const activityRoutes = createRoute({
method: 'get',
path: '/v1/activities',
tags: ['Activities', 'partner', 'public'], // Exposed to all tiers
// ...
})

const adminRoutes = createRoute({
method: 'get',
path: '/v1/admin/users',
tags: ['Admin'], // Internal only (no 'partner' or 'public' tag)
// ...
})

Consuming APIs

Option 1: Scalar UI (Browser)

1. Navigate to /docs (internal) or /partner/docs (partner)
2. Browse endpoints in sidebar
3. Click "Try it" to test
4. Authentication: Click "Auth" → Enter JWT or API key
5. Send request and see response

Option 2: Export to Postman

# Download OpenAPI spec
curl https://api.juniro.com/openapi.json -o juniro-api.json

# Import into Postman
1. Open Postman
2. File → Import
3. Select juniro-api.json
4. Collection created with all endpoints

Option 3: Postman Dynamic Import

1. Open Postman
2. APIs → Create API → Import
3. Enter URL: https://api.juniro.com/openapi.json
4. Enable "Auto-sync" to keep updated

Option 4: Scalar Cloud (Partner Portal)

For public partner documentation:

1. Create account at scalar.com
2. Create new API reference
3. Connect to: https://api.juniro.com/partner/openapi.json
4. Customize branding
5. Publish to: https://developers.juniro.com
6. Features:
- Custom domain
- Analytics (who's reading what)
- API key management
- Changelog notifications

Option 5: Generate Client SDKs

# Generate TypeScript client
npx openapi-typescript-codegen \
--input https://api.juniro.com/openapi.json \
--output ./src/api-client \
--client fetch

# Generate Python client
openapi-generator generate \
-i https://api.juniro.com/openapi.json \
-g python \
-o ./juniro-client-python

Enforcement

Code Review Checklist

Every API PR must verify:

  • OpenAPI spec updated
  • All parameters documented
  • Request/response examples included
  • Error responses documented
  • Appropriate tags assigned (internal/partner/public)
  • Follows naming conventions
  • Proper HTTP methods and status codes
  • Rate limiting configured

Automated Checks

# CI/CD pipeline checks
- name: Validate OpenAPI Spec
run: npx @redocly/cli lint openapi.json

- name: Check Breaking Changes
run: npx oasdiff breaking base.json new.json

- name: Ensure All Routes Documented
run: npm run check:undocumented-routes

Quality Metrics

MetricTargetMeasurement
Documentation coverage100%All routes have OpenAPI spec
Example coverage100%All endpoints have examples
Error documentation100%All error codes documented
Breaking changes0 per releaseAutomated diff check

Summary

The Juniro API Mandate in one sentence:

"Every feature is an API, every API is documented,
every document is a potential partner integration."

Build for external from day one. Your future self (and partners) will thank you.