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.
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.
| Endpoint | ETag Support | Typical max-age |
|---|
GET /v1/trademarks/:id | Yes | 300 s (5 min) |
GET /v1/trademarks (list) | Yes | 60 s (1 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/statuses | Yes | 3600 s |
POST /v1/trademarks/search | No | — |
POST /v1/trademarks/batch | No | — |
Reference data endpoints (offices, jurisdictions, statuses) change very rarely. Cache these aggressively with a long TTL.
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. |
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:
| 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. |
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;
}