> ## Documentation Index
> Fetch the complete documentation index at: https://docs.winnerr.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Reputation API

> Authenticated reputation endpoints for testimonials, surveys, referrals, profile, and aggregate, plus the public CORS-enabled endpoints that back the embeddable widget.

The Reputation API is the programmatic surface behind [Reputation Studio](/reputation/overview). It has two layers:

* **Authenticated routes** under `/api/reputation/*` — bearer-token, org-scoped, used by the in-app Reputation Studio UI and your own integrations.
* **Public routes** under `/api/public/{survey,reviews,referral}/*` — unauthenticated, CORS-enabled where appropriate, used by tokenized survey pages, the embeddable reviews widget, and the public referral capture form.

All authenticated endpoints follow the same conventions as the rest of the Winnerr API. See [API Introduction](/api-reference/introduction) for base URLs, bearer-token forwarding, and the standard `401` behavior when `userId` or `orgId` are missing.

## Authentication

Authenticated routes expect the standard bearer token forwarded by `apps/app`:

```ts theme={null}
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/reputation/testimonials`, {
  headers: {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
});
```

Every authenticated route runs inside the caller's organization scope — you can only read and write reputation data for your own org.

Public routes (`/api/public/...`) require no auth. The capability is the survey `accessToken` or the published profile slug, and the server resolves the owning org server-side.

## Testimonials

### `GET /api/reputation/testimonials`

List testimonials in the caller's org. Optional query params:

| Param         | Values                                         |
| ------------- | ---------------------------------------------- |
| `status`      | `PENDING`, `APPROVED`, `PUBLISHED`, `REJECTED` |
| `visibility`  | `PRIVATE`, `INTERNAL`, `PUBLIC`                |
| `agentUserId` | Filter to a specific agent                     |
| `category`    | `BUYER`, `SELLER`, `INVESTOR`, ...             |

Returns `{ testimonials: Testimonial[] }`, ordered by `sortOrder` asc then `createdAt` desc.

### `POST /api/reputation/testimonials`

Create a testimonial manually. Body fields include `agentUserId`, `authorPersonId`, `dealId`, `source`, `category`, `rating`, `headline`, `body`, display fields, and the three `permission*` flags. New entries always start `PENDING` + `PRIVATE` — they must still flow through approve → publish before they are public.

### `GET /api/reputation/testimonials/:id`

Fetch one testimonial including its `syndications`.

### `PATCH /api/reputation/testimonials/:id`

Partial update. Useful for editing copy, toggling `isFeatured`, reordering via `sortOrder`, or demoting visibility back to `PRIVATE` after publish.

### `POST /api/reputation/testimonials/:id/approve`

Transition `PENDING` → `APPROVED`. Records `approvedByUserId` and `approvedAt` and emits `REVIEW_APPROVED`. Still not public.

### `POST /api/reputation/testimonials/:id/publish`

Transition `APPROVED` → `PUBLISHED`, set `visibility = PUBLIC`, stamp `publishedAt`, emit `REVIEW_PUBLISHED`, and recompute the agent's `ReputationAggregate` synchronously. Requires the testimonial to already be `APPROVED` — the call fails closed if the testimonial is still `PENDING`.

See [Testimonials](/reputation/testimonials) for the lifecycle in context.

## Surveys

### `GET /api/reputation/surveys`

List surveys in the caller's org. Optional query params: `status`, `agentUserId`.

### `POST /api/reputation/surveys`

Issue a new survey manually. Body:

```json theme={null}
{
  "templateId": "tmpl_123",
  "recipientPersonId": "person_456",
  "agentUserId": "user_789",
  "dealId": "deal_abc",
  "channel": "EMAIL"
}
```

The server generates a unique `accessToken`, schedules the send, and emits `SURVEY_SCHEDULED`. The default expiry window is 30 days from creation.

Auto-scheduled surveys (from `DEAL_WON`) use the same code path — they do not require a client call.

### Survey templates

| Method | Path                                | Purpose                                                         |
| ------ | ----------------------------------- | --------------------------------------------------------------- |
| `GET`  | `/api/reputation/surveys/templates` | List org-scoped `SurveyTemplate` rows.                          |
| `POST` | `/api/reputation/surveys/templates` | Create a template. `sendDelayHours` defaults to `168` (7 days). |

See [Surveys](/reputation/surveys) for template fields and lifecycle.

## Referrals

### `GET /api/reputation/referrals`

List referrals in the caller's org. Optional query params: `status`, `agentUserId`.

### `POST /api/reputation/referrals`

Log a referral manually. Body fields include `referrerPersonId`, `agentUserId`, `channel`, `referredName`, `referredContact`, `notes`, and optional reward tracking (`rewardType`, `rewardStatus`, `rewardValue`).

When public capture creates a referral instead, the server emits `REFERRAL_RECEIVED` and the downstream workflow turns the referral into a `Person` with `LeadSource.REFERRAL`. See [Referrals](/reputation/referrals).

## Profile

### `GET /api/reputation/profile`

Fetch an `AgentPublicProfile`. Optional `?agentUserId=<id>` returns that agent's profile; otherwise returns the profile owned by the calling user.

### `PUT /api/reputation/profile`

Upsert the calling user's profile (or `agentUserId` if provided). Edits write into `draftConfig` — the live public page does not change until publish.

### `POST /api/reputation/profile/publish`

Validate `draftConfig` against the approved-block schema, copy it into `publishedConfig`, flip `status` to `PUBLISHED`, and stamp `publishedAt`. Body is optional `{ "agentUserId": "..." }`; defaults to the caller's own profile.

## Aggregate

### `GET /api/reputation/aggregate`

Read the denormalized `ReputationAggregate` rollup.

* `?agentUserId=<id>` returns that agent's aggregate (scope `AGENT`).
* With no `agentUserId`, returns the org-wide aggregate row (scope `ORG`, where `agentUserId` is null).

The response includes total reviews, average rating, NPS, response rate, review velocity, and rating distribution. Aggregates recompute automatically when a testimonial is published or unpublished.

## Public endpoints

The public routes are the surfaces that don't require auth. The capability is the survey `accessToken` or the published profile slug, and the org is resolved server-side.

### `GET /api/public/survey/:token`

Resolve a tokenized survey, return its question schema and minimal branding (brokerage name only — never agent PII, never other surveys), mark the survey `OPENED`, and emit `SURVEY_OPENED`.

Returns a uniform `404` (`{ "state": "gone" }`) for unknown, expired, completed, or skipped tokens so that guessing learns nothing.

### `POST /api/public/survey/:token`

Submit a `SurveyResponse`. The body is validated against the template's Zod schema, size-capped at 64 KB, and rate-limited. On success the server persists the response, flips the survey to `COMPLETED`, and emits `SURVEY_COMPLETED`. The reputation response consumer then runs sentiment analysis and (if the client opted in and rated highly) promotes the response into a `PENDING` `Testimonial`.

### `GET /api/public/reviews/:agentSlug`

The JSON feed the embeddable Reviews Widget and the public profile read. Returns:

* Published, public testimonials for the agent (consent-allowed public fields only — display name, location, photo).
* The agent's `ReputationAggregate`.
* Minimal profile branding.

CORS-enabled (`Access-Control-Allow-Origin: *`) and cached at the edge for one minute. Anything other than a `PUBLISHED`, non-deleted profile returns a uniform `404`.

Embed:

```html theme={null}
<script>
  fetch("https://api.winnerr.ai/api/public/reviews/jane-doe-realty")
    .then((r) => r.json())
    .then((data) => renderWidget(data));
</script>
```

### `POST /api/public/referral/:agentSlug`

Capture a referral from the public profile or widget CTA. Body example:

```json theme={null}
{
  "referrerName": "Pat Jones",
  "referrerEmail": "pat@example.com",
  "referredName": "Sam Lee",
  "referredEmail": "sam@example.com",
  "notes": "Sam is selling their condo this spring."
}
```

The server records the referrer as a `Person`, creates a `Referral` (`status: RECEIVED`, `channel: PUBLIC_PROFILE`), and emits `REFERRAL_RECEIVED` so the lead re-enters the pipeline. Rate-limited; body is capped at 32 KB.

## Events emitted

The reputation routes emit the following system events on the standard event bus. Workflows in `apps/api` consume them.

| Event               | Fired when                                                                            |
| ------------------- | ------------------------------------------------------------------------------------- |
| `DEAL_WON`          | (Upstream) The reputation survey workflow listens for this to auto-schedule a survey. |
| `SURVEY_SCHEDULED`  | A new `Survey` is created.                                                            |
| `SURVEY_OPENED`     | The tokenized public page is opened.                                                  |
| `SURVEY_COMPLETED`  | A `SurveyResponse` is submitted.                                                      |
| `REVIEW_APPROVED`   | A testimonial is moved to `APPROVED`.                                                 |
| `REVIEW_PUBLISHED`  | A testimonial is published to the public surface.                                     |
| `REFERRAL_RECEIVED` | A referral is captured (public or manual).                                            |

## Related

* [Reputation Studio overview](/reputation/overview)
* [Testimonials](/reputation/testimonials)
* [Surveys](/reputation/surveys)
* [Public profile](/reputation/public-profile)
* [Reviews widget](/reputation/widgets)
* [Referral rewards](/reputation/rewards)
* [Review syndication](/reputation/syndication)
* [Referrals](/reputation/referrals)
* [API Introduction](/api-reference/introduction)
