Skip to main content

Developer Page Specifications

API Contracts

GET listing/summary/details endpoints + POST subscription. Query params, response shapes, caching.

Complete

Three endpoints power the public Opportunity Radar surface: listing search, summary counters, and details fetch. One internal endpoint powers subscriptions. All responses are locale-aware via the Accept-Language header AND a fallback ?lang= query param (lang wins when both present). Responses are JSON, gzipped, cache-control honoured at the CDN.

1 · GET /api/opportunities — listing

Query params
q (string ≥2) · status (open|closing-soon|rolling|closed|all) · type (csv of slugs) · country (csv of ISO codes) · stage (csv) · sector (csv) · sort (most-relevant|deadline-soonest|newest|recently-verified|closing-soon) · page (int ≥1, default 1) · pageSize (int ≤100, default 24) · lang (en|ar).
Response shape
{ items: OpportunityListItem[], totalCount: int, pageSize: int, page: int, filters: { applied, available }, counts: { open, closingSoon, rolling, closed, all } }.
OpportunityListItem fields
id, slug, title, shortDescription, status, type {slug, label}, country {code, name}, stage[], sectors[], organizer {id, name, logoUrl}, deadlineDate (ISO 8601 or null), isRolling, isSponsored, isVerified, officialUrl, publishedAt, lastVerifiedAt.
Caching
Cache-Control: public, max-age=120, s-maxage=300, stale-while-revalidate=600. Invalidate on Opportunity publish / status flip.

2 · GET /api/opportunities/summary — board counters

  • No params (locale via header). Returns { open: int, closingSoon: int, rolling: int, tracked: int, latest: OpportunityListItem[] } where latest is the 5 most recent active opportunities.
  • Cache-Control: public, max-age=60, s-maxage=120, stale-while-revalidate=300.

3 · GET /api/opportunities/{slug} — details

Path params
slug (string, required, kebab-case).
Query params
lang (en|ar, optional). Defaults to Accept-Language.
Response shape
{ id, slug, title, shortDescription, fullDescription, programStructure?, whyItMatters?, founderProfile?, minimumRequirements?, applicationRequirements?, bestForStages?, status, type, country, multiCountries[], stages[], sectors[], organizer {full}, deadlineDate, isRolling, isSponsored, isVerified, officialUrl, valueSignal, urgency, timeRequired, benefits[], eligibilityCriteria[], applicationRequirements {struct}, timeline[], relatedOpportunities[], connectedSignals[], seo {metaTitle, metaDescription, ogImage, jsonLd}, publishedAt, lastVerifiedAt, verifiedBy {displayName} }.
connectedSignals[] shape
[{ sourceEntityType, sourceEntityId, sourceTitle, sourceCoverUrl, reason, sourceRoute, relationType, displayOrder }]. Capped at 8 server-side; client can request /api/opportunities/{slug}/connected?offset=8 for the full list.
Caching
Cache-Control: public, max-age=300, s-maxage=600, stale-while-revalidate=1800. Invalidate on Opportunity update / verify.
Errors
404 when slug unknown. 410 Gone when IsActive=false (rare — soft-deletes preserve content but may return 410 if editorial decides to retire). Never 500 to client — log server-side, return 503 with retry hint.

4 · POST /api/subscriptions/opportunity-digest

  • Body: { email, country?, stage?, consentPlatform (bool, must be true), consentPartners (bool, default false), source: 'listing' | 'details', opportunityIdContext? (uuid, only when source='details') }.
  • Response: 201 { subscriptionId } on success. 422 with field-level errors on validation fail.
  • Server-side: de-dupes by Email; existing subscriber gets preference fields merged. consentPartners=true triggers a separate sponsor-CRM webhook.
  • No rate-limit headers exposed to client; honour 429 if it ever returns.