Resource file downloads
Retrieve an existing file using the email/hash pair supplied by its download link. The pair grants access to the matched record; resource public keys and member tokens are not substitutes.
| Task | Operation |
|---|---|
| Download a supplied resource file | Download |
Example clients and synthetic values follow response conventions.
Retrieve a resource file
GET /api/v1/resource/file/download
Stream stored bytes from the configured private storage bucket. After sending the bytes, this GET attempts to update the download count.
Before you call
Guest ACL permits the action. There is no member/resource requirement or owner/resource/active/private/expiry/download-limit check in its file selection. It matches only file_hash and normalized email. Use the existing supplied link pair, with access controlled by your trusted application; do not generate a hash or substitute a resource key. The email is visible in query transport.
The action uses configured private_bucket and the matched record key, ignoring the record’s own bucket/storage/private fields. File selection and metadata do not establish provider storage availability. After echoing bytes, count_downloads is incremented from the previously read value and saved unchecked; no exact concurrent count guarantee. Storage/output/local-save failures can yield partial bytes or different state.
Request
Bodyless; no token/resource headers required.
| Name | Location | Type / requirement | Meaning |
|---|---|---|---|
| query | required valid email, max 60 characters before normalization | Supplied link recipient; sanitized email/trim/lower for matching. | |
| hash | query | required string, max 32 characters before normalization | Supplied file_hash; sanitized string/trim/striptags. No client hash derivation or exact 32-character validation. |
Result
Success streams bytes directly and terminates, with no JSON wrapper or redirect. The handler does not explicitly set a success HTTP status.
| Header / output | Type / presence | Meaning |
|---|---|---|
| Content-Type | storage-supplied string | Forwarded from storage ContentType; no universal MIME type or file extension inference. |
| Content-Disposition | attachment; filename=stored original_name | Original filename appended without a universal quoted filename/encoding guarantee. |
| Pragma | public | Explicit header value; no expiry guarantee. |
| Body | binary/file bytes | Matched storage object body; do not unconditionally parse JSON. |
Example: Handle the supplied link as file bytes
Use the email/hash from an existing link; RESOURCE_FILE_HASH is an obvious placeholder supplied separately, not a generated credential. The expected result is stored bytes and its media type, with the filename from the matched record. No synthetic JSON success body is fabricated.
curl --get "${WALLKIT_API_BASE}/api/v1/resource/file/download" \
--data-urlencode "email=reader@example.com" \
--data-urlencode "hash=${RESOURCE_FILE_HASH}" \
--dump-header download-headers.txt --output download-body.bin
# Inspect HTTP status and Content-Type before interpreting download-body.bin.
const url = new URL(`${process.env.WALLKIT_API_BASE}/api/v1/resource/file/download`);
url.searchParams.set("email", "reader@example.com");
url.searchParams.set("hash", process.env.RESOURCE_FILE_HASH);
const response = await fetch(url);
const media = response.headers.get("content-type") || "";
if (media.includes("application/json")) {
console.log(response.status, await response.json());
} else if (!response.ok) {
console.log(response.status, await response.text());
} else {
const bytes = await response.arrayBuffer();
console.log(response.status, media, bytes.byteLength);
// Inspect expected media/content; a non-JSON body is not proof of a complete file.
}
import os, json
from urllib.parse import urlencode
from urllib.request import Request, urlopen
from urllib.error import HTTPError
query = urlencode({"email": "reader@example.com", "hash": os.environ["RESOURCE_FILE_HASH"]})
request = Request(os.environ["WALLKIT_API_BASE"] + "/api/v1/resource/file/download?" + query, method="GET")
try:
with urlopen(request) as response:
media = response.headers.get("Content-Type", "")
body = response.read()
if "application/json" in media:
print(response.status, json.loads(body))
else:
print(response.status, media, len(body))
# Inspect expected file content; length/status alone do not prove completeness.
except HTTPError as error:
body = error.read()
if "application/json" in error.headers.get("Content-Type", ""):
print(error.code, json.loads(body))
else:
print(error.code, body.decode("utf-8", errors="replace"))
Illustrative header/body shape only, with media dependent on the stored object:
Content-Type: <stored ContentType>
Content-Disposition: attachment; filename=<stored original_name>
Pragma: public
<stored file bytes>
Consequential alternate
A mismatched supplied pair returns HTTP 409 JSON excerpt (other response metadata omitted):
{"error":"invalid_data","error_description":"invalid_data"}
A caught storage exception instead echoes exception text and terminates, without a stable JSON/status contract; non-JSON bytes or nominal success status alone do not prove a complete valid file.
Recovery
| Status | Code / shape | Cause | Next action |
|---|---|---|---|
| 409 | invalid_event_email / invalid_event_hash | Email/hash presence, length or email validation fails. | Use exact supplied link values and correct encoding. |
| 409 | invalid_data | No record matches normalized email/hash pair. | Ask the link issuer for the intended existing link; a member token does not repair the pair. |
| Unspecified | Storage exception text / partial output; no universal JSON envelope | Storage access fails or later handling fails. | Inspect status/media/content and ask integration owner to check stored object/configuration; do not assume a completed download or blindly retry count-writing requests. |
Next task
Validate expected media/content before presenting the file. For a transaction document instead, use its distinct User subscriptions and Pricing selection only when that is the supplied document type.