Diese technische Dokumentation ist auf Englisch verfügbar.
Leitfäden

Attach files to an expiry

Add, list and remove files (certificates, contracts, reports) on an expiry.

Before you start

  • Needs an API key - see API keys.
  • Listing works for any role. Adding or removing files needs Editor or Admin.
  • You need the expiry's id (in the app's address bar, or from any API response).
export BASE="https://api.expiryedge.com/v1"
export TOKEN="ee_live_..."
export EXPIRY="exp_4Tq9sLm2"

How uploading works

  1. Ask for an upload link.
  2. PUT the file to it (works once, for 15 minutes).
  3. Confirm, and the file is attached.

Max 50 MB per file, up to 50 files per expiry. HTML, SVG and XML are blocked.

size is the exact size in bytes (wc -c < file.pdf).

curl -s "$BASE/getExpiryAttachmentUploadUrl" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"expiryId":"'"$EXPIRY"'","file_name":"Gas Safety Certificate 2026.pdf","content_type":"application/pdf","size":184022}'
{
  "upload_id": "pUp7x1",
  "upload_url": "https://storage.googleapis.com/...",
  "headers": { "Content-Type": "application/pdf", "x-goog-content-length-range": "0,184022" },
  "expires_at": "2026-09-27T14:20:00.000Z",
  ...
}

Step 2: Send the file

PUT to upload_url with exactly the returned headers. Don't send your Authorization header here.

curl -s -X PUT "PASTE-upload_url-HERE" \
  -H "Content-Type: application/pdf" \
  -H "x-goog-content-length-range: 0,184022" \
  --data-binary @"Gas Safety Certificate 2026.pdf"

No output means success.

Step 3: Confirm the upload

curl -s "$BASE/completeExpiryAttachmentUpload" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"upload_id":"pUp7x1"}'
{
  "attachment": { "index": 1, "name": "Gas Safety Certificate 2026.pdf", "url": "https://firebasestorage.googleapis.com/..." },
  "attachments": [ ... ]
}

The file now shows on the expiry in the app, and in its Activity log.

Step 4: List the files

curl -s "$BASE/getExpiryAttachments?expiryId=$EXPIRY" -H "Authorization: Bearer $TOKEN"
{ "data": [ { "index": 0, "name": "Gas Safety Certificate 2025.pdf", "url": "https://firebasestorage.googleapis.com/..." }, ... ] }

Anyone with a file's url can open it. Keep it private.

Step 5: Remove a file

curl -s "$BASE/deleteExpiryAttachment" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"expiryId":"'"$EXPIRY"'","url":"PASTE-THE-OLD-FILE-url-HERE"}'

The file is deleted from storage and its space freed. You can send "index": 0 instead of url, but url is safer if the list changed meanwhile.

Complete script

Run with TOKEN=... node attach.mjs exp_4Tq9sLm2 "Gas Safety Certificate 2026.pdf".

JavaScript (Node 18+), attach.mjs:

import { readFile } from 'node:fs/promises';
import { basename } from 'node:path';

const BASE = 'https://api.expiryedge.com/v1';
const auth = { Authorization: `Bearer ${process.env.TOKEN}`, 'Content-Type': 'application/json' };
const [expiryId, filePath] = process.argv.slice(2);

async function api(name, body) {
  const res = await fetch(`${BASE}/${name}`, { method: 'POST', headers: auth, body: JSON.stringify(body) });
  const json = await res.json();
  if (!res.ok) throw new Error(`${name}: ${res.status} ${json.code} ${json.error}`);
  return json;
}

const bytes = await readFile(filePath);
const ticket = await api('getExpiryAttachmentUploadUrl', {
  expiryId, file_name: basename(filePath), content_type: 'application/pdf', size: bytes.length,
});
const put = await fetch(ticket.upload_url, { method: 'PUT', headers: ticket.headers, body: bytes });
if (!put.ok) throw new Error(`upload failed: ${put.status}`);
const done = await api('completeExpiryAttachmentUpload', { upload_id: ticket.upload_id });
console.log('Attached:', done.attachment.name);

Python (pip install requests), attach.py:

import os, sys, requests

BASE = "https://api.expiryedge.com/v1"
AUTH = {"Authorization": f"Bearer {os.environ['TOKEN']}"}
expiry_id, file_path = sys.argv[1], sys.argv[2]

def api(name, body):
    r = requests.post(f"{BASE}/{name}", json=body, headers=AUTH)
    if not r.ok:
        raise SystemExit(f"{name}: {r.status_code} {r.text}")
    return r.json()

data = open(file_path, "rb").read()
ticket = api("getExpiryAttachmentUploadUrl", {
    "expiryId": expiry_id, "file_name": os.path.basename(file_path),
    "content_type": "application/pdf", "size": len(data),
})
requests.put(ticket["upload_url"], data=data, headers=ticket["headers"]).raise_for_status()
done = api("completeExpiryAttachmentUpload", {"upload_id": ticket["upload_id"]})
print("Attached:", done["attachment"]["name"])

For other files change content_type (e.g. image/jpeg, image/png).

Common problems

These upload errors apply to every file guide.

SymptomFix
403 "requires admin or editor permissions"You're a Viewer. Use an Editor key.
403 STORAGE_LIMIT_REACHEDPlan storage is full. Remove files or upgrade.
404 Expiry / Attachment not foundWrong ID or url, or another organization. List again.
409 TOO_MANY_ATTACHMENTSThe expiry already has 50 files.
413 FILE_TOO_LARGEOver 50 MB. Compress or split it.
400 UNSUPPORTED_FILE_TYPEHTML, SVG and XML aren't allowed.
Step 2 returns 403 from storageHeaders didn't match exactly, or the file is bigger than size.
410 UPLOAD_EXPIREDOver 15 minutes since step 1. Start again.
422 UPLOAD_MISSINGDo step 2 before step 3.
422 UPLOAD_MISMATCHFile size or type differs from what you declared. Start again.
409 "already completed"Already done - the file is attached.

Reference

Full details: openapi.yaml.

  • GET getExpiryAttachments?expiryId= - list files.
  • POST getExpiryAttachmentUploadUrl - step 1: get an upload link.
  • POST completeExpiryAttachmentUpload - step 3: attach the file.
  • POST or DELETE deleteExpiryAttachment - remove a file (by url or index).