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

# Caching & ETags

> Reduce redundant requests with conditional fetching and cache strategies

Trademark data changes infrequently for most records. By leveraging ETags and conditional requests, you can avoid re-downloading unchanged data, reduce your rate limit consumption, and speed up your application.

## How ETags Work

When you fetch a resource, the response includes an `ETag` header containing a content fingerprint:

```
HTTP/1.1 200 OK
ETag: "a3f2b7c8d1e4"
Cache-Control: private, max-age=300
Content-Type: application/json

{
  "id": "tm_7d4e1f2a",
  "object": "trademark",
  "mark_text": "SIGNA",
  ...
}
```

On subsequent requests, send the ETag back via `If-None-Match`. If the resource has not changed, you get a `304 Not Modified` with no body, saving bandwidth and processing time:

```
GET /v1/trademarks/tm_7d4e1f2a
If-None-Match: "a3f2b7c8d1e4"

HTTP/1.1 304 Not Modified
ETag: "a3f2b7c8d1e4"
```

<Note>
  A `304 Not Modified` response does **not** count against your rate limit, so using ETags effectively gives you free requests.
</Note>

***

## Endpoints That Support ETags

| Endpoint                           | ETag Support | Typical `max-age` |
| ---------------------------------- | ------------ | ----------------- |
| `GET /v1/trademarks/:id`           | Yes          | 300 s (5 min)     |
| `GET /v1/owners/:id`               | Yes          | 300 s             |
| `GET /v1/attorneys/:id`            | Yes          | 300 s             |
| `GET /v1/firms/:id`                | Yes          | 300 s             |
| `GET /v1/offices`                  | Yes          | 3600 s (1 hour)   |
| `GET /v1/jurisdictions`            | Yes          | 3600 s            |
| `GET /v1/trademarks` (list/search) | No           | --                |
| `POST /v1/trademarks` (search)     | No           | --                |
| `POST /v1/trademarks/batch`        | No           | --                |

<Tip>
  Reference data endpoints (offices, jurisdictions, classifications) change very rarely. Cache these aggressively with a long TTL.
</Tip>

***

## Cache-Control Headers

Every cacheable response includes a `Cache-Control` header:

```
Cache-Control: private, max-age=300
```

| Directive   | Meaning                                                                                       |
| ----------- | --------------------------------------------------------------------------------------------- |
| `private`   | Response is specific to this API key and must not be stored by shared caches (CDNs, proxies). |
| `max-age=N` | The response is considered fresh for N seconds. After that, revalidate with `If-None-Match`.  |
| `no-store`  | Present on mutation responses (POST, PATCH, DELETE). Do not cache.                            |

<Warning>
  All Signa responses include `private` because they are scoped to your organization. Never cache API responses in a shared/public cache.
</Warning>

***

## Conditional Request Flow

<CodeGroup>
  ```typescript TypeScript theme={null}
  const cache = new Map<string, { etag: string; data: any }>();

  async function getTrademarkCached(id: string, apiKey: string) {
    const url = `https://api.signa.so/v1/trademarks/${id}`;
    const headers: Record<string, string> = {
      Authorization: `Bearer ${apiKey}`,
    };

    // Send ETag if we have a cached version
    const cached = cache.get(id);
    if (cached) {
      headers['If-None-Match'] = cached.etag;
    }

    const response = await fetch(url, { headers });

    if (response.status === 304 && cached) {
      // Not modified -- return cached data
      return cached.data;
    }

    if (!response.ok) {
      throw new Error(`API error: ${response.status}`);
    }

    const data = await response.json();
    const etag = response.headers.get('ETag');

    // Store in cache
    if (etag) {
      cache.set(id, { etag, data });
    }

    return data;
  }
  ```

  ```python Python theme={null}
  import requests

  cache: dict[str, dict] = {}

  def get_trademark_cached(trademark_id: str, api_key: str) -> dict:
      url = f"https://api.signa.so/v1/trademarks/{trademark_id}"
      headers = {"Authorization": f"Bearer {api_key}"}

      # Send ETag if we have a cached version
      cached = cache.get(trademark_id)
      if cached:
          headers["If-None-Match"] = cached["etag"]

      response = requests.get(url, headers=headers)

      if response.status_code == 304 and cached:
          # Not modified -- return cached data
          return cached["data"]

      response.raise_for_status()

      data = response.json()
      etag = response.headers.get("ETag")

      # Store in cache
      if etag:
          cache[trademark_id] = {"etag": etag, "data": data}

      return data
  ```
</CodeGroup>

***

## Caching Strategies for Trademark Data

Different types of trademark data have different change frequencies. Tailor your caching strategy accordingly.

### Reference Data (offices, jurisdictions, classifications)

These change only when Signa adds a new office or jurisdiction.

* **Strategy**: Cache locally with a 24-hour TTL. Revalidate with ETags daily.
* **Storage**: In-memory or local file.

### Individual Trademarks

Most trademark records change infrequently (a few times per year), but some change during active prosecution.

* **Strategy**: Cache with the `max-age` value from the response (typically 5 min). Revalidate with ETags after expiry.
* **Storage**: In-memory cache (Redis, local Map) keyed by trademark ID.

### Search Results

Search results are dynamic and depend on query parameters, so they are not ETag-cacheable.

* **Strategy**: Client-side TTL cache keyed by the full query hash. A 30--60 second TTL works well for typeahead and repeated searches.
* **Storage**: In-memory only. Do not persist search result caches.

### List Endpoints

Paginated lists may change as new records are added.

* **Strategy**: Short TTL (60 s from `max-age`). Revalidate with ETags. Note that the cursor itself provides consistency within a pagination session.
* **Storage**: In-memory, keyed by the full URL including query parameters.

***

## Cache Invalidation

ETags handle revalidation automatically: a conditional `GET` against the same resource returns `304 Not Modified` when the record is unchanged, so your cache entry stays valid and the request counts against your rate budget at the light-weight "not modified" rate.

For records you display in hot UI surfaces, complement ETag revalidation with a background refresh on the [Trademark Changes](/api-reference/trademarks/trademark-changes) endpoint -- it returns the versioned change log for a specific mark, letting you invalidate a cache entry the moment a new version is detected without waiting for the next ETag check.

<Tip>
  Pair ETags for normal traffic with a low-frequency poll of `trademark-changes` for your hottest marks. You get efficient revalidation on every read plus proactive busting on the small set of records that actually churn.
</Tip>

***

## Multi-Tier Caching

For applications that display trademark data to end users, consider a two-tier approach:

| Tier                      | TTL       | Purpose                                                                          |
| ------------------------- | --------- | -------------------------------------------------------------------------------- |
| **L1: In-process**        | 30--60 s  | Eliminates redundant API calls within a single request cycle.                    |
| **L2: Redis / Memcached** | 5--15 min | Shares cached data across application instances. Uses ETag revalidation on miss. |

```typescript theme={null}
async function getTrademark(id: string): Promise<Trademark> {
  // L1: Check in-process cache
  const l1 = memoryCache.get(id);
  if (l1) return l1;

  // L2: Check Redis
  const l2 = await redis.get(`tm:${id}`);
  if (l2) {
    const parsed = JSON.parse(l2);
    memoryCache.set(id, parsed, { ttl: 30_000 });
    return parsed;
  }

  // L3: Fetch from API with ETag revalidation
  const data = await getTrademarkCached(id, apiKey);
  memoryCache.set(id, data, { ttl: 30_000 });
  await redis.set(`tm:${id}`, JSON.stringify(data), 'EX', 900);

  return data;
}
```
