Skip to main content

Skip unchanged reads with conditional requests

Which Platform API reads carry an ETag, how to send it back in If-None-Match, what a 304 Not Modified means, and when a read carries no tag.

The platform caches successful responses to cacheable GET reads for each caller and tags them. A cacheable read answers with an ETag, a tag that identifies that exact response body. Send the tag back in If-None-Match on your next request for the same resource. If nothing changed, the platform answers 304 Not Modified with no body. If something changed, it answers 200 with the new body and a new tag. A client that never sends If-None-Match receives the full response every time.

Polling with a tag replaces a full download with a status line. If you need to react to changes instead of polling for them, subscribe with webhooks.

Send a conditional request

Make the read once and keep the ETag header from the response. This request is illustrative: replace the host and API key with your deployment values. See Authentication for the key.

curl -i "https://your-instance.example.com/api/v2/tokens" \
  -H "X-Api-Key: sm_dalp_..."
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: private, no-cache
ETag: "84e2cdb7a166d1671bc5e2abc1e78b59777dd1f863d3c5b05d09c2dd088444c2"
Vary: Accept-Encoding, X-Participant, X-Executor, X-Custody-Account-Id, X-Chain-Id, X-Network-Id, Origin

The example leaves out the route's ordinary body, which the API reference describes. The route supplies Content-Type, cross-origin handling adds Origin to Vary, and the response cache sets the other headers. A real response carries more headers. Send the tag back on the next read:

curl -i "https://your-instance.example.com/api/v2/tokens" \
  -H "X-Api-Key: sm_dalp_..." \
  -H 'If-None-Match: "84e2cdb7a166d1671bc5e2abc1e78b59777dd1f863d3c5b05d09c2dd088444c2"'
HTTP/1.1 304 Not Modified
Cache-Control: private, no-cache
ETag: "84e2cdb7a166d1671bc5e2abc1e78b59777dd1f863d3c5b05d09c2dd088444c2"
Vary: Accept-Encoding, X-Participant, X-Executor, X-Custody-Account-Id, X-Chain-Id, X-Network-Id, Origin

The 304 has no body. Keep the body you already hold and reuse it. Request the same URL, including the same query string, because a different URL is a different response.

The If-None-Match header follows these rules:

  • It accepts one tag or a comma-separated list. The platform answers 304 when any tag in the list matches.
  • * matches when the read has any current response that carries a tag.
  • The platform compares tags strongly, so a weak tag, which starts with W/, never matches. A proxy that weakens tags therefore causes full responses.
  • On a read that carries no tag, If-None-Match has no effect and the platform answers as if you had not sent it.

Poll with conditional requests

For a client that watches a resource, send If-None-Match on every poll and replace your stored copy only on a 200:

  1. Make the first read and store the body with its ETag.
  2. On each poll, send the stored tag in If-None-Match.
  3. On 304, keep the stored body.
  4. On 200, replace the stored body and tag with the new ones.
  5. On any other status, handle it as an error as Errors describes and keep the stored pair.

On a 200 without an ETag, store the new body and drop the tag, so the next poll sends no If-None-Match.

Use the SDK

The TypeScript SDK does this for you. It stores the tag from each successful read, sends it on the next read of the same URL for the same organisation and participant, and turns a 304 back into the stored body, so your code receives a normal response either way. It keeps at most 64 responses of at most 1 MiB each. It clears them when the organisation or the X-Participant header changes, and you must call dalp.conditionalRequests.clear() yourself when you switch identity through a separate authentication client. Calling routes shows the call.

What a 304 means

A 304 on a tag the response cache set says the body the platform would send now is byte for byte the body that your tag identifies. It is not a statement about the resource's history. A tag the response cache sets is a SHA-256 hash of the response body before compression, so a resource that changed and then changed back gives the same tag again, and the response encoding you negotiated with Accept-Encoding does not change the tag. When an operation sets its own ETag, such as the version of a stored record, the cache keeps that tag instead of replacing it with the body hash and matches If-None-Match against it, so a 304 on that read means the tag still matches. Treat every tag as opaque.

Which reads carry a tag

The response cache tags a read when all of these hold:

  • The operation is a GET read and not an event stream. The cache does not apply to writes.
  • The operation has not opted out of caching. Some reads, such as custody reconciliation and deposits, opt out and answer without a tag.
  • The request does not set live=true. On a read whose reference lists a live query parameter, live=true skips the cache and returns the live result. Do not add it to other reads, because some reject unknown query parameters.
  • The response has a 2xx or 3xx status, fits in 1 MiB, and is not streamed, so an error carries no cache tag.
  • The operation does not itself set Cache-Control: no-store or no-cache, a cookie, or an Authorization header on its response.
  • The platform can resolve which participant the request acts as, which X-Participant selects, or the read gives the same answer for every participant. A read that depends on the participant and cannot be resolved is served without a tag. A read that does not depend on the participant is tagged even then.
  • The request URL and the caller's identity together stay within 16 KiB. A longer request is not cached.
  • Response caching is on, which is the default. A self-hosting operator can turn it off with the CACHE_VERSIONED_ENABLED environment variable. That switch turns off the versioned cache that response caching builds on, and then the cache tags no read.

A read that skips the cache, for example after a cache fault or when catch-up after your own write cannot complete, answers without a cache tag. A response without an ETag is not an error. Store its body without a tag. A few operations set their own ETag and Cache-Control, so they answer with their own tag whether or not the cache stores the response. The cache does not store a 304 that an operation answers itself. The operation's reference says whether it answers 304. The OpenAPI contract does not list the tag on every read that carries one, so check the response.

A browser app can read the tag and send If-None-Match: the API allows the request header and exposes the response header to cross-origin callers. A browser's HTTP cache also stores tagged responses and revalidates them by itself, so a browser app saves the transfer even if its code never sends the header. The code still receives the full response.

Who shares a cached response

The platform keeps cached responses apart per caller. A cached response is keyed to the organisation, the user, how the caller signed in, the caller's platform role, whether the caller owns the organisation, the chain the request resolved to, the participant the request acts as, and the values of the X-Executor, X-Custody-Account-Id, X-Chain-Id and X-Network-Id request headers. A participant is the person, organisation or account record a request acts for, which X-Participant selects. For a read whose answer depends on the participant, the key holds the participant the platform resolved, not the raw header value. The platform learns which reads do not depend on the participant, such as the public configuration and the enabled languages. It keys those on the raw X-Participant header value instead and serves them without resolving the participant. A read that writes an audit record always resolves it. The key holds no asset role or on-chain role. The response Vary header lists X-Participant and the other four headers. Keep one stored tag per caller and per combination of those headers.

How fresh a cached read is

The platform invalidates cached responses when the data behind them changes. By default, a cached copy also expires five minutes after it is stored.

After you write, the platform makes your next reads wait for the cache to catch up with that write. This applies for a configurable window after you write, 60 seconds by default. A read in that window does not return a copy from before the write. If the catch-up cannot complete, the read skips the cache and carries no tag. This applies to reads by the same user who wrote. Other callers see the change when the cache invalidates.

Cache-Control: private, no-cache on a response the cache tags tells a client it can store the response but must revalidate before it reuses it. Revalidating means sending If-None-Match.

Next steps

Was this page helpful?