Delivery URLs
GET https://cdn.picsoar.com/v1/:projectKey/:preset/*sourcePath?v=token
HEAD https://cdn.picsoar.com/v1/:projectKey/:preset/*sourcePath?v=tokenSource path rules
The path is validated before any network call. It must:
- end in
.jpg,.jpeg,.png,.gif,.webp,.avif, or.svg; - contain no traversal segments, in any encoding;
- contain no double-encoded sequences (
%25); - contain no backslashes, control characters, or empty segments;
- be at most 1024 characters, 32 segments, 255 characters per segment.
Anything else is a 400 with the reason in x-picsoar-reason. The extension allowlist is a security control, not a convenience: without it the endpoint would stream any file at your origin through our CDN.
Query parameters
Exactly one is accepted: v, matching [A-Za-z0-9._~-]{1,64}. Any other parameter is rejected, not ignored.
Ignoring unknown parameters sounds harmless and is not: ?a=1 through?a=1000000 would be a million cache entries and a million transformations, mintable by anyone with a for-loop.
Cache key
A derivative is stored under:
k1 / projectKey / e{cacheEpoch} / preset@version / format / v{token} / encodedPathEach component is there for a reason:
- cacheEpoch — bumped by a purge or an origin change, so invalidating everything is one increment rather than an enumeration.
- preset@version — editing a preset creates a new version, so old derivatives are never silently reinterpreted and rollback is a pointer move.
- format — the negotiated output. Keeping it in the key means correctness does not depend on
Varybeing honoured, which historically it often is not. - encodedPath — the canonical encoding, so two spellings of one path cannot become two cache entries.
Changing this format is a breaking change: every existing derivative becomes unreachable and re-transforms on first request. It is gated behind explicit approval in CI.
Response headers
| Header | Meaning |
|---|---|
x-picsoar-preset | Preset and version applied, e.g. card@1. |
x-picsoar-format | webp, avif, or source. |
x-picsoar-decision | Why that format: accept-webp, resize-only, transform-failed, quota-degraded, source-too-large… |
x-picsoar-cache | hit or miss. |
x-picsoar-reason | Present only on rejections and errors. |
x-request-id | Quote this in support requests. |
Cache-Control we set
- With
?v=:public, max-age=31536000, immutable. You promised the bytes will not change under that token. - Without:
public, max-age=300, s-maxage=86400, stale-while-revalidate=86400. We cannot make that promise for you. - Errors and rejections:
no-store, so a transient origin blip never becomes a persistent one.