# FairClose API Structure & Partner Integration Guide

Version: v1 stable  
Production host: `https://fairclose.co`  
Authenticated API base URL: `https://fairclose.co/api/v1`  
Public API base URL: `https://fairclose.co/api/v1/public`  
Partner API base URL: `https://fairclose.co/api/v1/public/partner`  
Canonical developer page: `https://fairclose.co/developers/api`  
Public OpenAPI: `https://fairclose.co/api/v1/public/openapi.json`

This document is for partner marketplaces, provenance systems, auction houses, editorial sites, inventory tools, storefront widgets, and automation services that need to connect correctly with FairClose.

## 1. Integration surfaces

FairClose exposes three integration surfaces:

| Surface | Base path | Auth | Best for |
| --- | --- | --- | --- |
| Public marketplace API | `/api/v1/public` | None | Public category/listing/search feeds, read-only discovery widgets |
| Partner API | `/api/v1/public/partner` | `X-API-Key` | Server-to-server partner feeds, usage tracking, signed listing embeds |
| User/session API | `/api/v1/...` | `Authorization: Bearer <JWT>` | FairClose web app, authenticated buyer/seller/admin workflows |

For third-party sites, prefer the public API for anonymous read-only discovery and the partner API for production integrations that need auditability, rate limits, or signed embeds.

## 2. Environments and canonical URLs

Use only production URLs for production apps:

```txt
Frontend: https://fairclose.co
FAIRCLOSE_API_BASE: https://fairclose.co/api/v1
Public API: https://fairclose.co/api/v1/public
Partner API: https://fairclose.co/api/v1/public/partner
Health: https://fairclose.co/api/health
Release marker: https://fairclose.co/api/release
```

Do not call preview, staging, localhost, or `*.preview.emergentagent.com` URLs from production partner sites.

For Arquivis, set:

```txt
FAIRCLOSE_API_BASE=https://fairclose.co/api/v1
FAIRCLOSE_COA_EVENTS_URL=https://fairclose.co/api/v1/partner/coa-events
```

## 3. Authentication

### 3.1 Public read-only endpoints

No authentication is required.

```bash
curl 'https://fairclose.co/api/v1/public/listings?limit=12&sort=newest'
```

### 3.2 Partner endpoints

Partner API keys are issued by FairClose admins. Send the key in the `X-API-Key` header.

```bash
curl -H 'X-API-Key: fc_live_your_partner_key' \
  'https://fairclose.co/api/v1/public/partner/listings?limit=12'
```

Partner keys can have:

- `read` scope: partner metadata, listing feeds, search, listing detail.
- `write` scope: signed embed URL generation.
- Rate limits per minute and per day.
- Optional allowed-origin restrictions.
- Optional expiration dates.

Never expose a raw partner API key in browser JavaScript. Use your own backend as a proxy, or generate signed embed URLs server-side.

### 3.3 User/session endpoints

Authenticated buyer, seller, and admin workflows use JWT bearer tokens:

```http
Authorization: Bearer <jwt_token>
```

These endpoints are not the recommended server-to-server partner surface unless the integration explicitly acts on behalf of a FairClose user.

## 4. Rate limits

Partner responses include rate-limit headers:

```http
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 42
X-RateLimit-Policy: minute;w=60;limit=60, day;w=86400;limit=10000
X-RateLimit-Day-Limit: 10000
X-RateLimit-Day-Remaining: 9998
X-RateLimit-Day-Reset: 73420
```

When a limit is exceeded, FairClose returns `429` with `Retry-After`.

## 5. Public endpoints

### `GET /meta`

Returns API name, version, docs URL, OpenAPI URL, SDK links, and supported capabilities.

```bash
curl 'https://fairclose.co/api/v1/public/meta'
```

### `GET /openapi.json`

Machine-readable public API contract.

```bash
curl 'https://fairclose.co/api/v1/public/openapi.json'
```

### `GET /categories`

Returns marketplace categories.

```bash
curl 'https://fairclose.co/api/v1/public/categories'
```

Response shape:

```json
{
  "items": [
    {
      "category_id": "cat_...",
      "name": "Watches",
      "slug": "watches",
      "description": "",
      "listing_count": 0
    }
  ],
  "total": 1
}
```

### `GET /listings`

Returns public active listings with pagination.

Query params:

| Param | Type | Default | Notes |
| --- | --- | --- | --- |
| `q` | string | optional | Searches title, description, brand |
| `category_id` | string | optional | Filter by category id |
| `page` | integer | `1` | 1-based page |
| `limit` | integer | `12` | Max `50` |
| `sort` | string | `newest` | `newest`, `ending_soon`, `price_low`, `price_high`, `most_viewed` |

```bash
curl 'https://fairclose.co/api/v1/public/listings?limit=12&sort=ending_soon'
```

Response shape:

```json
{
  "items": [
    {
      "listing_id": "lst_...",
      "title": "Vintage Watch",
      "status": "active",
      "auction_format": "buy_now_only",
      "current_price": 5500,
      "buy_now_price": 5500,
      "currency": "USD",
      "primary_image": "/uploads/example.png",
      "category_id": "cat_...",
      "category_name": "Watches",
      "brand": "Rolex",
      "condition": "used",
      "bid_count": 0,
      "watch_count": 18,
      "view_count": 4,
      "created_at": "2026-05-01T00:00:00+00:00",
      "end_time": null,
      "listing_url": "https://fairclose.co/listing/lst_...",
      "seller": {
        "seller_id": "user_...",
        "name": "Luxury Watches Co",
        "verified": false,
        "rating": null,
        "sales_count": 0
      },
      "shipping": {
        "free_shipping": false,
        "shipping_cost": null,
        "handling_days": null
      }
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 12,
  "pages": 1,
  "sort": "newest",
  "query": null,
  "category_id": null
}
```

### `GET /search`

Keyword search. Same response shape as `/listings`.

Required query param: `q`.

```bash
curl 'https://fairclose.co/api/v1/public/search?q=Rolex&limit=6'
```

### `GET /listings/{listing_id}`

Single public listing detail. Private listings return `404`.

```bash
curl 'https://fairclose.co/api/v1/public/listings/lst_123'
```

Additional detail fields include `description`, `images`, `return_policy`, and `shipping_policy`.

## 6. Partner endpoints

All partner endpoints require `X-API-Key`.

### `GET /partner/meta`

Returns public metadata plus partner key id, scopes, and enabled partner features.

```bash
curl -H 'X-API-Key: fc_live_your_partner_key' \
  'https://fairclose.co/api/v1/public/partner/meta'
```

### `GET /partner/listings`

Authenticated listing feed with usage logging. Same params and response shape as public `/listings`.

```bash
curl -H 'X-API-Key: fc_live_your_partner_key' \
  'https://fairclose.co/api/v1/public/partner/listings?category_id=cat_123&limit=24'
```

### `GET /partner/search`

Authenticated search with usage logging.

```bash
curl -H 'X-API-Key: fc_live_your_partner_key' \
  'https://fairclose.co/api/v1/public/partner/search?q=handbag&sort=price_high'
```

### `GET /partner/listings/{listing_id}`

Authenticated single listing detail.

```bash
curl -H 'X-API-Key: fc_live_your_partner_key' \
  'https://fairclose.co/api/v1/public/partner/listings/lst_123'
```

### `POST /partner/embed-signatures`

Creates a signed URL for a browser-safe listing embed. Requires `write` scope.

```bash
curl -X POST 'https://fairclose.co/api/v1/public/partner/embed-signatures' \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: fc_live_your_partner_key' \
  -d '{
    "listing_id": "lst_123",
    "theme": "sand",
    "expires_in_minutes": 60
  }'
```

Request body:

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `listing_id` | string | yes | Active public listing id |
| `theme` | string | no | `sand`, `light`, `dark`; default `sand` |
| `accent` | string | no | Optional color token, max 24 chars |
| `expires_in_minutes` | integer | no | 5–1440; default 60 |

Response:

```json
{
  "key_id": "key_...",
  "listing_id": "lst_123",
  "expires_at": "2026-05-06T20:00:00+00:00",
  "signature": "...",
  "embed_url": "https://fairclose.co/embed/listing/lst_123?key_id=...&expires=...&sig=...&theme=sand",
  "iframe_snippet": "<iframe src=\"https://fairclose.co/embed/listing/lst_123?...\"></iframe>"
}
```

### Signed embed runtime

The signed browser URL points to:

```txt
https://fairclose.co/embed/listing/{listing_id}?key_id=...&expires=...&sig=...&theme=sand
```

Under the hood, the JSON endpoint is:

```txt
GET /api/v1/public/partner/embed/listings/{listing_id}?key_id=...&expires=...&sig=...&theme=sand
```

Embed signatures are HMAC SHA-256 over:

```txt
key_id:listing_id:expires_at:theme:accent
```

## 7. Webhooks

FairClose supports outbound webhook subscriptions managed by admins.

Supported event names:

- `listing.created`
- `listing.updated`
- `listing.authenticated`
- `listing.coa_updated`
- `listing.ended`
- `listing.sold`
- `listing.revoked`
- `bid.placed`
- `bid.outbid`
- `order.created`
- `order.paid`
- `order.shipped`
- `order.delivered`
- `user.registered`
- `user.verified`
- `review.created`
- `dispute.opened`
- `dispute.resolved`

Webhook payloads should be verified with HMAC signatures when configured. Treat webhook handlers as idempotent: store event ids and ignore duplicates.

Recommended webhook receiver behavior:

1. Validate the signature header before processing.
2. Return `2xx` only after durable storage or queueing.
3. Retry-safe handlers: do not double-create records for repeated events.
4. Process asynchronously for slow downstream tasks.

## 8. Arquivis ↔ FairClose COA webhook contract

### 8.1 Arquivis → FairClose inbound COA events

FairClose exposes an HMAC-signed inbound endpoint for Arquivis certificate/provenance updates:

```txt
POST https://fairclose.co/api/v1/partner/coa-events
Content-Type: application/json
X-Webhook-Signature: sha256=<hmac_sha256_hex_digest>
```

Signing secret in FairClose production env:

```txt
ARQUIVIS_WEBHOOK_SECRET=<shared secret generated for Arquivis>
```

Arquivis-side env values:

```txt
FAIRCLOSE_API_BASE=https://fairclose.co/api/v1
FAIRCLOSE_COA_EVENTS_URL=https://fairclose.co/api/v1/partner/coa-events
```

Supported Arquivis event types:

- `certificate_generated`
- `record_published`
- `record_revoked`

FairClose matches listings by `external_reference`, `public_record_id`, `record_id`, or an existing stored Arquivis/COA reference. If no listing is matched, the event is stored for later reconciliation and returns a successful accepted response.

Sample payload:

```json
{
  "event_type": "certificate_generated",
  "record_id": "rec_arquivis_123",
  "public_record_id": "pub_arquivis_abc",
  "external_reference": "lst_fairclose_123",
  "serial": "AQV-COA-2026-0001",
  "brand": "Rolex",
  "model": "Submariner",
  "verification_url": "https://arquivis.com/records/pub_arquivis_abc",
  "certificate_version": "1.0",
  "issued_at": "2026-05-07T12:00:00Z",
  "prior_coa": {
    "provider": "Brand Archive",
    "number": "BA-123",
    "issue_date": "2024-01-10"
  },
  "images": [
    {"url": "https://cdn.example.com/cert.jpg", "type": "certificate"}
  ]
}
```

Sample signing code:

```js
import crypto from 'crypto';

const body = JSON.stringify(payload);
const signature = 'sha256=' + crypto
  .createHmac('sha256', process.env.ARQUIVIS_WEBHOOK_SECRET)
  .update(body)
  .digest('hex');
```

FairClose stores mapped fields on the listing:

- `authenticity_verified`
- `authentication_status`
- `authenticity_provider = "Arquivis"`
- `authenticity_certificate`
- `arquivis_public_record_id`
- `arquivis_record_id`
- `arquivis_verification_url`
- `source_coa_provider`
- `source_coa_number`
- `source_coa_issue_date`
- `coa` object with record, certificate, image, and status metadata

### 8.2 FairClose → Arquivis outbound webhooks

Configure this in FairClose Admin → Ops Hub → API → Webhooks using the Arquivis endpoint configured as `ARQUIVIS_OUTBOUND_WEBHOOK_URL` in the backend environment. The current receiver URL is shown in Admin → API → Webhooks → Arquivis monitoring.

Subscribe to:

- `authentication.requested`
- `listing.authenticated`
- `listing.coa_updated`
- `listing.sold`
- `listing.revoked`

FairClose signs Arquivis outbound webhooks using `ARQUIVIS_OUTBOUND_WEBHOOK_SECRET` when set, otherwise `ARQUIVIS_WEBHOOK_SECRET`. If neither env secret is configured, FairClose falls back to the per-webhook secret shown once at webhook creation. Arquivis should store the matching value as:

```txt
FAIRCLOSE_WEBHOOK_SECRET=<secret copied from FairClose webhook creation>
```

FairClose sends:

```http
X-Webhook-Signature: sha256=<hmac_sha256_hex_digest>
X-Webhook-Event: listing.coa_updated
```

Sample outbound payload:

```json
{
  "event": "listing.coa_updated",
  "timestamp": "2026-05-07T12:05:00Z",
  "data": {
    "listing_id": "lst_fairclose_123",
    "listing_url": "https://fairclose.co/listing/lst_fairclose_123",
    "external_reference": "pub_arquivis_abc",
    "public_record_id": "pub_arquivis_abc",
    "verification_status": "verified",
    "authentication_provider": "Arquivis",
    "external_certificate_number": "AQV-COA-2026-0001",
    "verification_url": "https://arquivis.com/records/pub_arquivis_abc",
    "verification_date": "2026-05-07T12:00:00Z",
    "source_coa_provider": "Brand Archive",
    "source_coa_number": "BA-123",
    "source_coa_issue_date": "2024-01-10"
  }
}
```

Use `external_reference` / `public_record_id` as the primary match key on the Arquivis side.

### 8.3 Staging/live test publish checklist

After both sides deploy their webhook handlers:

1. Create or choose one staging Arquivis record with a known `public_record_id`.
2. Create or choose one FairClose listing and store that same value as `external_reference` or pass the FairClose `listing_id` in `external_reference`.
3. Arquivis sends one signed `certificate_generated` event to `https://fairclose.co/api/v1/partner/coa-events`.
4. Capture the FairClose response. Expected shape:

```json
{
  "status": "matched",
  "event_id": "arquivis_...",
  "listing_id": "lst_...",
  "message": "COA event applied"
}
```

If no listing is matched, FairClose returns:

```json
{
  "status": "unmatched",
  "event_id": "arquivis_...",
  "listing_id": null,
  "message": "COA event stored; no matching listing found"
}
```

Use the returned `listing_id`, plus the known production listing URL pattern `https://fairclose.co/listing/{listing_id}`, to confirm listing URL extraction.

## 9. Arquivis provenance integration pattern

Arquivis exposes a public read-only developer API protected by `x-api-key`. Its documented endpoints include:

- `GET /records`
- `GET /records/:public_record_id`
- `GET /records/:public_record_id/integrity`
- `GET /stats`
- `GET /events/digest`
- `GET /events/breakdown`

Arquivis webhooks include events such as `record_published`, `record_revoked`, `issue_reported`, and `certificate_generated`, signed with `X-Arquivis-Signature` using HMAC SHA-256.

Recommended FairClose ↔ Arquivis flow:

1. Exchange an Arquivis API key and a FairClose partner API key.
2. Store a shared webhook secret on both sides.
3. Arquivis calls FairClose partner endpoints to find or verify a listing URL.
4. FairClose calls Arquivis `GET /records/:public_record_id` to hydrate provenance metadata during lot creation or review.
5. Partner listing metadata should use stable IDs:
   - `platform = "FairClose"`
   - `listing_id = "lst_..."`
   - `listing_url = "https://fairclose.co/listing/lst_..."`
   - `currency = "USD"`
   - `price = current_price or buy_now_price`
6. Arquivis verification pages can show a “View on FairClose” CTA using `listing_url`.
7. FairClose listing detail pages can show provenance status using the Arquivis public record id and integrity endpoint.

## 10. JavaScript example

```js
const baseUrl = 'https://fairclose.co/api/v1/public';

async function searchFairClose(q) {
  const res = await fetch(`${baseUrl}/search?q=${encodeURIComponent(q)}&limit=6`);
  if (!res.ok) throw new Error(`FairClose API error ${res.status}`);
  return res.json();
}

const data = await searchFairClose('Rolex');
console.log(data.items);
```

Partner server-side example:

```js
const res = await fetch('https://fairclose.co/api/v1/public/partner/listings?limit=12', {
  headers: { 'X-API-Key': process.env.FAIRCLOSE_PARTNER_API_KEY }
});
```

## 11. Python example

```py
import os
import requests

BASE_URL = 'https://fairclose.co/api/v1/public'

resp = requests.get(f'{BASE_URL}/listings', params={'limit': 12, 'sort': 'newest'}, timeout=20)
resp.raise_for_status()
print(resp.json()['items'])

partner_resp = requests.get(
    f'{BASE_URL}/partner/meta',
    headers={'X-API-Key': os.environ['FAIRCLOSE_PARTNER_API_KEY']},
    timeout=20,
)
partner_resp.raise_for_status()
print(partner_resp.json())
```

## 12. Error handling

Common status codes:

| Status | Meaning |
| --- | --- |
| `400` | Invalid query/body |
| `401` | Missing, invalid, expired API key or embed signature |
| `403` | Missing scope or origin not allowed |
| `404` | Listing/category/resource not found or private |
| `429` | Rate limit exceeded; respect `Retry-After` |
| `500` | Server error |

Error response shape is typically:

```json
{ "detail": "Human-readable error message" }
```

## 13. Caching expectations

- `/meta` and `/openapi.json`: short public cache.
- `/categories`: public cache up to 1 hour.
- `/listings`, `/search`, `/listings/{id}`: short public cache, currently around 60 seconds.
- Partner endpoints: private short cache or no shared-cache assumptions.

Partner apps should not cache signed embed URLs beyond their `expires_at` value.

## 14. Security checklist

- Use HTTPS only.
- Never expose partner API keys client-side.
- Use signed embeds for browser-safe listing widgets.
- Store webhook event ids to prevent duplicate processing.
- Validate HMAC signatures for webhooks.
- Pin production integrations to `https://fairclose.co`.
- Parse JSON additively; ignore unknown future fields.

## 15. Go-live checklist

Before sending production traffic:

- Confirm production base URL: `https://fairclose.co/api/v1/public`.
- Confirm API key scopes: `read` and, if needed, `write`.
- Confirm allowed origins if your key is origin-restricted.
- Confirm rate limits with FairClose admins.
- Confirm webhook secret and event names.
- Confirm `/api/release` and `/api/v1/public/meta` return expected production values.
- Test with a small `limit` first.
- Confirm embeds expire and rotate as expected.
