# GiveRefer public API — endpoint reference

Derived from [`referralgpt-openapi.json`](https://giverefer.com/referralgpt-openapi.json)
(OpenAPI 3.1, `info.version` 1.2.0). **That spec is the source of truth. If this document and
the spec disagree, the spec wins.**

- Base URL: `https://giverefer.com/api`
- All endpoints: `GET`, no authentication, no API key
- Content type: `application/json`
- Send `Accept: application/json`; optionally `X-GiveRefer-Client: agent-skill`

Responses may contain fields not listed here. Ignore what you do not recognise rather than
treating it as an error.

---

## Endpoint index

| Endpoint | Purpose |
|---|---|
| [`/referral/search?brand=` or `?domain=`](#referralsearch) | Brand or website lookup — the entry point |
| [`/referral/{store_slug}/top-clicked-codes`](#referralstore_slugtop-clicked-codes) | 5 most-clicked codes — the default code call |
| [`/referral/{store_slug}/latest-codes`](#referralstore_sluglatest-codes) | 5 newest codes |
| [`/referral/{store_slug}`](#referralstore_slug) | Full store detail: how-it-works, FAQs, features, stats, countries |
| [`/referral/suggest?intent=`](#referralsuggestintent) | Intent-driven brand suggestions |
| [`/popular-referrals`](#popular-referrals) | Curated popular brands |
| [`/stores/search?q=`](#storessearchq) | Fuzzy store search — the 404 fallback |
| [`/stores/popular`](#storespopular) | Most-viewed stores |
| [`/stores/trending`](#storestrending) | Recently updated stores |
| [`/categories`](#categories) | Category taxonomy |
| [`/countries`](#countries) | Country taxonomy |

---

## `/referral/search`

`GET /api/referral/search?brand={name}` or `GET /api/referral/search?domain={hostname}`

One of the two is required; sending neither is a `422`.

| Parameter | Max | Matching |
|---|---|---|
| `brand` | 120 chars | **Exact**, case-insensitive, against the store's slug, name and title. Not a fuzzy search: `?brand=Uber` matches, `?brand=uber ride` does not. A value that looks like a host or URL is also tried as a domain first |
| `domain` | 253 chars | The brand's own website. Scheme, `www.`, path and query are stripped, so `?domain=https://www.uber.com/ride` and `?domain=uber.com` are the same lookup. Treated **only** as a domain — it never falls back to a name match |

Prefer `domain` whenever you have the website: it survives brands whose display name differs
from what the user typed. Use `brand` when all you have is a name.

**200**

```json
{
  "brand": "Uber",
  "store_slug": "uber",
  "domain": "uber.com",
  "bonus": "₹200 ride credit",
  "tagline": "Free rides for new users",
  "description": "Get ₹200 ride credit using referral",
  "category": "ride-hailing",
  "url": "https://giverefer.com/store/uber"
}
```

| Field | Type | Notes |
|---|---|---|
| `brand` | string | Display name |
| `store_slug` | string | Use this with every other `/referral/*` endpoint |
| `domain` | string \| null | The brand's own website. Check it to confirm you resolved the company the user meant. `null` means GiveRefer has not recorded one, not that the match is wrong |
| `bonus` | string | Derived from the store's headline offer. Falls back to a generic "Check offer details on GiveRefer" when the store has no offer text — do not quote a number that is not there |
| `tagline` | string \| null | |
| `description` | string | |
| `category` | string \| null | Category slug |
| `url` | string | The GiveRefer store page. Always surface this |

**404** — nothing listed under that exact brand string, or under that domain. Also returned
when a `?domain=` value cannot be parsed as a hostname at all.

```json
{ "message": "Referral program not found for the requested brand." }
```

After a `brand` 404, retry with the slug form (lowercase, hyphenated), then fall back to
`/stores/search`. After a `domain` 404, fall back to the registrable name as `brand`
(`wise.com` -> `wise`).

**422** — neither parameter given, or one is over its length limit. The error names `brand`.

```json
{ "message": "The given data was invalid.", "errors": { "brand": ["..."] } }
```

---

## `/referral/{store_slug}/top-clicked-codes`

`GET /api/referral/{store_slug}/top-clicked-codes`

The 5 codes with the highest click count, ties broken by most recently updated. Click count is
the best available signal that a code still works, so this is the default call.

**200**

```json
{
  "brand": "Zepto",
  "referral_codes": [
    {
      "code": "https://zepto-prod.onelink.me/tC90/o9uykfz5",
      "click_count": 446,
      "updated_at": "2025-12-30T17:10:49.000000Z"
    },
    {
      "code": "YIUNNT",
      "click_count": 204,
      "updated_at": "2026-03-03T07:40:07.000000Z"
    }
  ]
}
```

| Field | Type | Notes |
|---|---|---|
| `brand` | string | |
| `referral_codes[].code` | string | **Either a literal code or a full URL.** Check for an `http://` / `https://` prefix before describing it to the user |
| `referral_codes[].click_count` | integer | Lifetime clicks through GiveRefer. `0` is normal for a new code |
| `referral_codes[].updated_at` | string \| null | ISO 8601 |

**`referral_codes` can be `[]` on a 200.** The store exists and has no publishable codes right
now — either nobody has submitted one, or the brand has asked GiveRefer to suppress code
listings. This is a normal outcome, not a failure. Report it as "listed, no active codes right
now" and link the store page.

**404** — no store with that slug.

```json
{ "message": "Referral details not found for the requested store slug." }
```

---

## `/referral/{store_slug}/latest-codes`

`GET /api/referral/{store_slug}/latest-codes`

Identical shape to `top-clicked-codes`; the 5 most recently updated codes instead, ties broken
by click count. Use it when the user asks for the newest code, or as a second attempt when
`top-clicked-codes` returns an empty array.

---

## `/referral/{store_slug}`

`GET /api/referral/{store_slug}` — the full store record. Large. Fetch it only when the user
asks something search cannot answer.

**200** (abridged)

```json
{
  "brand": "Uber",
  "store_slug": "uber",
  "bonus": "₹200 ride credit",
  "tagline": "Free rides for new users",
  "description": "Get ₹200 ride credit using referral",
  "domain": "uber.com",
  "about": "Uber is a ride-hailing platform ...",
  "category": "ride-hailing",
  "countries": ["India", "United Kingdom"],
  "top_referral_codes": ["ABCD1234", "https://uber.com/invite/xyz"],
  "steps_to_redeem": "1. Sign up — use the code at registration. 2. Take your first ride ...",
  "how_it_works": [{ "step": 1, "title": "Sign up", "body": "Use the code at registration." }],
  "key_features": [{ "title": "No minimum spend", "body": "Credit applies to any ride." }],
  "faqs": [{ "question": "When does the credit arrive?", "answer": "After your first ride." }],
  "stats": [{ "value": "₹200", "label": "Signup bonus" }],
  "referral_link": "https://uber.com/invite/{code}",
  "giverefer_page": "https://giverefer.com/store/uber"
}
```

| Field | Type | Notes |
|---|---|---|
| `domain` | string \| null | The brand's own website, or null if not recorded |
| `top_referral_codes` | array\<string\> | Bare code strings, no click counts. **Empty array when the store has no publishable codes** — same non-error meaning as above |
| `countries` | array\<string\> | Country names. Empty when unrestricted or unrecorded — do not read an empty array as "not available anywhere" |
| `how_it_works` | array\<`{step, title, body}`\> | Empty for stores without structured content |
| `key_features` | array\<`{title, body}`\> | May be empty |
| `faqs` | array\<`{question, answer}`\> | May be empty |
| `stats` | array\<`{value, label}`\> | May be empty |
| `steps_to_redeem` | string | Plain-text flattening of `how_it_works` |
| `referral_link` | string | The brand's own referral URL **pattern**. It may contain a `{code}` placeholder and is not necessarily a working link on its own — prefer a real `code` from the code endpoints |
| `giverefer_page` | string | The GiveRefer store page. Always surface this |

**404** — no store with that slug.

---

## `/referral/suggest?intent=`

`GET /api/referral/suggest?intent={text}&limit={1..20}` — **required**: `intent`.
`limit` optional, default 5.

Keyword and category matching over store text for open-ended questions ("food delivery",
"travel", "money transfer"). Ranked by store popularity.

**200** — a bare array (not an object):

```json
[{ "brand": "Swiggy", "bonus": "₹100 off first order", "url": "https://giverefer.com/store/swiggy" }]
```

Note this returns `url`, not `store_slug`. To get codes for a suggestion, take the last path
segment of `url` as the slug, or re-resolve through `/referral/search`.

An empty array means nothing matched. **422** on a missing `intent`.

---

## `/popular-referrals`

`GET /api/popular-referrals` — no parameters. A hand-curated list, not a live ranking.

```json
{
  "popular_referrals": [
    {
      "brand": "Uber",
      "bonus": "₹200 ride credit",
      "store_slug": "uber",
      "giverefer_page": "https://giverefer.com/store/uber"
    }
  ]
}
```

---

## `/stores/search?q=`

`GET /api/stores/search?q={text}` — **required**: `q` (string, max 255 chars).

Fuzzy, relevance-ranked search. This is the fallback when `/referral/search` returns 404.

```json
[{ "brand": "Wise", "slug": "wise", "category": "money-transfer" }]
```

| Field | Type |
|---|---|
| `brand` | string |
| `slug` | string — usable as `store_slug` |
| `category` | string — category slug |

An empty array is the only reliable evidence GiveRefer does not list the brand. **422** on a
missing `q`.

> Matching runs over the store's title, descriptions, and category name — **not** the store's
> `name` column. A brand whose name never appears in its own description can be missed. Treat
> an empty result as "probably not listed", not as proof.

---

## `/stores/popular`

`GET /api/stores/popular` — up to 10 stores ordered by page views.

```json
[{ "brand": "Uber", "slug": "uber", "category": "ride-hailing", "views": 120340 }]
```

## `/stores/trending`

`GET /api/stores/trending` — up to 10 stores ordered by most recently updated. Same item shape
as `/stores/popular`. "Trending" here means recently changed, not fastest-growing.

## `/categories`

`GET /api/categories`

```json
[{ "id": 3, "name": "Ride Hailing", "slug": "ride-hailing" }]
```

## `/countries`

`GET /api/countries`

```json
{ "success": true, "countries": [{ "id": 1, "name": "India", "short_name": "IN" }] }
```

Note the wrapper object — unlike `/categories`, this is not a bare array.

---

## Caching, freshness, and limits

- Responses are cached server-side for up to **one hour**. A code can be up to an hour stale,
  and a freshly submitted code may not appear immediately. Never promise real-time accuracy.
- Rate limited per caller. A `429` carries a `Retry-After` header and a JSON body. Back off
  and tell the user — do not answer a rate-limited lookup from memory.
- Codes are **user-submitted**. GiveRefer does not verify that each one still works.

## Error shapes

Every error is JSON.

| Status | Body | Cause |
|---|---|---|
| `404` | `{"message": "..."}` | Brand or slug not listed |
| `422` | `{"message": "The given data was invalid.", "errors": {"field": ["..."]}}` | Missing or invalid query parameter |
| `429` | `{"message": "Too Many Attempts.", "retry_after": 42}` | Rate limited |
