Files and PDFs
How file values look, how to upload a file for an entry, and how to get an entry's PDF.
This page covers the files an entry holds and the PDFs Tepez produces from it. You will learn the shape of a file value, upload a file and attach it to an entry, download the PDF the form archived when the entry was submitted, and render a fresh PDF on demand.
To download every PDF and file of a form's entries, see Download PDFs and files.
File values
A file field (fFile) holds an array of file objects, one per uploaded file:
"cv": [
{
"name": "cv.png",
"size": 70,
"type": "image/png",
"s3Key": "form/<formId>/<uuid>/cv.png",
"s3Bucket": "<bucket>",
"url": "https://<bucket>.s3.eu-west-1.amazonaws.com/form/<formId>/<uuid>/cv.png?<presigned-query>"
}
]| Property | Meaning |
|---|---|
name | The file name. |
size | The size in bytes. It must match the uploaded file exactly. |
type | The MIME type. |
s3Key, s3Bucket | Where the file is stored. Copy them from the upload policy. |
url | A signed link that downloads the file without an API key. It stays valid for years, so treat it like the file itself: anyone who has the link can download the file. |
deleted | Only in what you send. true removes this file from the entry when you save it, and deletes the stored file too, unless the form's keepUploadedFiles includes "deleted". Reading an entry never returns it. |
Upload a file
Files don't travel in the body of an API request. You upload the file straight to storage, then
put its file object in the entry's data:
- Ask for an upload policy for the form.
- Upload the file with that policy.
- Save or submit the entry with the file object. Submit entries from your server covers the submit itself.
POST /form/{formId}/s3UploadPolicy isn't listed in the
API reference. This page is its documentation.
1. Ask for an upload policy
POST /form/{formId}/s3UploadPolicy takes the file's name, size in bytes and MIME type. It
needs no API key: the form's own fill page calls it for each file a person attaches. It works only
for a form that has a file field.
curl -X POST https://api.tepez.co.il/v1.0/form/<formId>/s3UploadPolicy \
-H "Content-Type: application/json" \
-d '{"name": "cv.png", "size": 70, "type": "image/png"}' \
-o policy.json{
"bucket": "<bucket>",
"key": "form/<formId>/<uuid>/cv.png",
"AWSAccessKeyId": "<accessKeyId>",
"acl": "private",
"policy": "<policy>",
"signature": "<signature>",
"success_action_status": 201,
"url": "https://<bucket>.s3.eu-west-1.amazonaws.com/form/<formId>/<uuid>/cv.png?<presigned-query>",
"Content-Type": "image/png"
}The policy allows one upload of exactly this file, to exactly this key, within 2 hours. The file
must have exactly the size and Content-Type you declared.
2. Upload the file
Send the file as a multipart/form-data POST to the storage host, the scheme and host of the
policy's url: https://<bucket>.s3.eu-west-1.amazonaws.com/. Include these fields of the policy
as form fields, and put the file last, in a field named file: key, acl, Content-Type,
AWSAccessKeyId, policy, signature and success_action_status.
p() { jq -r ".[\"$1\"]" policy.json; }
curl -X POST "$(p url | cut -d/ -f1-3)/" \
-F "key=$(p key)" \
-F "acl=$(p acl)" \
-F "Content-Type=$(p Content-Type)" \
-F "AWSAccessKeyId=$(p AWSAccessKeyId)" \
-F "policy=$(p policy)" \
-F "signature=$(p signature)" \
-F "success_action_status=$(p success_action_status)" \
-F "file=@cv.png"A successful upload answers 201 with a short XML document:
<?xml version="1.0" encoding="UTF-8"?>
<PostResponse><Location>https://<bucket>.s3.eu-west-1.amazonaws.com/form%2F<formId>%2F<uuid>%2Fcv.png</Location><Bucket><bucket></Bucket><Key>form/<formId>/<uuid>/cv.png</Key><ETag>"<etag>"</ETag></PostResponse>3. Put the file object in the entry
Build the file object from the policy and the file: name, size and type as you declared
them, s3Key from the policy's key, s3Bucket from its bucket, and its url. A file field
holds an array, even for one file.
curl -X POST https://api.tepez.co.il/v1.0/entry/submit \
-H "x-api-key: $TEPEZ_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq '{form: "<formId>", data: {
fullName: "Dana Levi",
cv: [{name: "cv.png", size: 70, type: "image/png",
s3Key: .key, s3Bucket: .bucket, url: .url}]
}}' policy.json)"The entry stores the file object:
{
"entry": {
"id": "<entryId>",
"status": "submitted",
"data": {
"fullName": "Dana Levi",
"cv": [
{
"name": "cv.png",
"size": 70,
"type": "image/png",
"s3Key": "form/<formId>/<uuid>/cv.png",
"s3Bucket": "<bucket>",
"url": "https://<bucket>.s3.eu-west-1.amazonaws.com/form/<formId>/<uuid>/cv.png?<presigned-query>"
}
]
}
}
}When the entry is checked against the form's rules, which every submit does, Tepez confirms each
file exists in storage with exactly the declared size. The field's own limits, such as the
allowed file types, size and number of files, apply at the same time. So upload each file before
you submit, and never build a file object by hand for a file you didn't upload through this flow.
The archived PDF
When the form is set to archive PDFs, Tepez renders the entry to PDF after the matching action and
keeps the file on the entry, in archivedRenders. A form is usually set to archive a PDF on submit. The form's
postSubmit.renders (see Forms) shows whether it does.
"archivedRenders": [
{
"id": "<renderId>",
"action": "<submitActionId>",
"type": "template",
"template": { "type": "auto" },
"file": {
"name": "Job application.pdf",
"size": 86525,
"type": "application/pdf",
"s3Key": "<formId>/<entryId>/<renderId>/Job application.pdf",
"s3Bucket": "<bucket>",
"url": "https://<bucket>.s3.eu-west-1.amazonaws.com/<formId>/<entryId>/<renderId>/Job%20application.pdf?<presigned-query>"
}
}
]- Download the PDF from
archivedRenders[].file.url. The item itself has nourl. The link needs no API key and stays valid for years. actionis theidof the entry action that produced the PDF, so you can tell a PDF archived on submit from one archived on a later edit.typeistemplatefor a PDF of one layout, andconcatenationfor one PDF that combines several layouts.template.namenames the layout when the form defines several.- An archived PDF never changes. It shows the data and the form as they were at that moment. A later change to the form's PDF layout doesn't touch PDFs already archived.
archivedRendersis already filled in the response of the submit request. You don't need to read the entry again.
Render a PDF on demand
A render produces a fresh PDF from the entry's current data and the form's current layout.
POST /entry/{entryId}/render renders an
existing entry:
curl -X POST https://api.tepez.co.il/v1.0/entry/<entryId>/render \
-H "x-api-key: $TEPEZ_API_KEY"{
"url": "https://<bucket>.s3.eu-west-1.amazonaws.com/<uuid>/Job%20application.pdf?<presigned-query>"
}POST /entry/render renders data that isn't in any
entry. Send the form and the data:
curl -X POST https://api.tepez.co.il/v1.0/entry/render \
-H "x-api-key: $TEPEZ_API_KEY" \
-H "Content-Type: application/json" \
-d '{"form": "<formId>", "data": {"fullName": "Dana Levi", "position": "dev"}}'The response has the same shape: { "url": "…" }.
- The
urlis valid for about 2 days. Download the PDF within that time, and render again if you need it later. Limits lists the lifetime of every kind of link. - A render doesn't add to the entry's
archivedRenders, doesn't add an action, and sends no email. Only the form's archive rules add archived PDFs. - The render prints the data as stored. Values the form calculates in the browser aren't calculated here.
- A form can be set to keep the data of every render as an entry with status
render. Most forms don't. The form'ssaveRenderedEntriesshows the setting:nonekeeps nothing.
PDF layouts at a remote URL
A form's PDF layout can be a PDF file that Tepez downloads from your server each time it renders, for example a document your system prepares per entry. Tepez's PDF service downloads it from changing IP addresses, with no fixed IP. So the URL must:
- be public HTTPS, reachable from anywhere on the internet
- not depend on an IP allowlist, a VPN or a firewall rule, which can't work here
To keep such a file private, use an unguessable URL that expires after a few days.
Troubleshooting
| Response | Cause and fix |
|---|---|
403 You cannot upload this file for this form from s3UploadPolicy | The form has no file field. Uploads work only for forms with one. |
400 Invalid file name from s3UploadPolicy | The name contains one of \ / : * ? " < > |, starts with a dot, ends with a space or a dot, is a reserved name such as con or nul, or is over 255 characters. |
400 EntityTooLarge or EntityTooSmall from the storage upload | The file's size differs from the size you declared in the policy request. Ask for a new policy with the exact size in bytes. |
403 AccessDenied from the storage upload | The upload doesn't match the policy: a different Content-Type, a field missing or out of order (the file must be last), or a policy older than 2 hours. The message names the condition that failed. Ask for a new policy. |
400 entry_failed_validation, Reported size … does not match actual size … | The size in the file object differs from the uploaded file. Send the exact size in bytes. |
400 entry_failed_validation, Invalid data type. on a file field | The file object lacks s3Key, s3Bucket or url, or the field's value isn't an array. |
500 on submit with a file | A file object points at a file that doesn't exist in storage. Upload the file first and copy key and bucket from its policy. |
The PDF link returns 403 or AccessDenied | A render's url expired after about 2 days. Render again. For the archived PDF, use archivedRenders[].file.url. |
No archivedRenders on a submitted entry | The form doesn't archive PDFs on submit, or the entry was edited to submitted instead of submitted. See Entries. |
Errors explains the error format and lists more messages.
Every response from the API carries an x-tepez-request-id header. Quote it when you contact us.
Question about this page? Email support@tepez.co.il