Skip to main content
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"
A 304 Not Modified response does not count against your rate limit, so using ETags effectively gives you free requests.

Endpoints That Support ETags

EndpointETag SupportTypical max-age
GET /v1/trademarks/:idYes300 s (5 min)
GET /v1/trademarks (list)Yes60 s (1 min)
GET /v1/owners/:idYes300 s
GET /v1/attorneys/:idYes300 s
GET /v1/firms/:idYes300 s
GET /v1/officesYes3600 s (1 hour)
GET /v1/jurisdictionsYes3600 s
GET /v1/statusesYes3600 s
POST /v1/trademarks/searchNo
POST /v1/trademarks/batchNo
Reference data endpoints (offices, jurisdictions, statuses) change very rarely. Cache these aggressively with a long TTL.

Cache-Control Headers

Every cacheable response includes a Cache-Control header:
Cache-Control: private, max-age=300
DirectiveMeaning
privateResponse is specific to this API key and must not be stored by shared caches (CDNs, proxies).
max-age=NThe response is considered fresh for N seconds. After that, revalidate with If-None-Match.
no-storePresent on mutation responses (POST, PATCH, DELETE). Do not cache.
All Signa responses include private because they are scoped to your organization. Never cache API responses in a shared/public cache.

Conditional Request Flow

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;
}

Caching Strategies for Trademark Data

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

Reference Data (offices, jurisdictions, statuses)

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, but you can also proactively invalidate your cache using webhooks:
// When you receive a trademark.updated webhook event,
// invalidate the cached entry for that trademark
app.post('/webhooks/signa', (req, res) => {
  const { event_type, data } = req.body;

  if (event_type === 'trademark.updated' || event_type === 'trademark.status_changed') {
    cache.delete(data.id);
  }

  res.status(200).json({ received: true });
});
Combining ETags with webhook-driven invalidation gives you the best of both worlds: efficient polling for reads, with instant cache busting when data actually changes.

Multi-Tier Caching

For applications that display trademark data to end users, consider a two-tier approach:
TierTTLPurpose
L1: In-process30—60 sEliminates redundant API calls within a single request cycle.
L2: Redis / Memcached5—15 minShares cached data across application instances. Uses ETag revalidation on miss.
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;
}