Branded gift-card PDFs

Every company has a usable gift-card PDF from day one. Lifepeaks generates it from the onboarded company profile — name, logo where available, brand colors, locale, and currency — so nothing has to be uploaded before the first order.

Partners can replace that generated default with their own artwork. Uploading does not make anything live: publishing is a separate, explicit call, and it is the only step buyers can see the effect of.

Templates are stored per locale, and the default locale is da-DK.

Read the current template

GET /v2/pdf-templates — scope pdf_templates:read

bash
curl "https://api.lifepeaks.dk/v2/pdf-templates?lang=da-DK" \
  -H "Authorization: Bearer $LIFEPEAKS_API_KEY"

The response reports every stored version in templates, plus the one new orders render from in current. Each version carries source (default or partner), status, version, bytes, page_count, sha256, and published_at. Storage keys, buckets, and credentials are never returned.

GET /v2/pdf-templates/{template_id} returns a single version in the same shape.

The list omits versions with status uploading. Those exist only after a finalize that crashed part-way, can never be published, and are collected automatically — so what you see is the set of versions you can actually act on. Reading such a version by id still works and reports "status": "uploading".

Which version current reports

A published partner version always outranks a generated default. Lifepeaks resolves current — and the template a new order is issued with — in this order:

  1. published partner version for the exact locale;
  2. published partner version for the same base language, any region;
  3. published partner version for en-GB;
  4. the company default for that locale.

Tiers 2 and 3 let one published override serve every locale you sell in, so current for a locale you never uploaded artwork for can still report source: "partner". That is deliberate: most partners want one design everywhere, and only upload a second when they genuinely need one.

Publish partner artwork

The lifecycle is five calls: intent → upload → finalize → preview → publish. Declaring the file before uploading is what makes the upload verifiable — the bytes that arrive are checked against the checksum and size you committed to, so a truncated or substituted file is caught rather than published.

1. Create an upload intent

POST /v2/pdf-templates/upload-intents — scope pdf_templates:write

bash
FILE=gift-card.pdf

curl -sS -X POST https://api.lifepeaks.dk/v2/pdf-templates/upload-intents \
  -H "Authorization: Bearer $LIFEPEAKS_API_KEY" \
  -H "Idempotency-Key: pdf_template_autumn_intent_01" \
  -H "Content-Type: application/json" \
  -d "$(jq -n \
    --arg sha "$(shasum -a 256 "$FILE" | cut -d' ' -f1)" \
    --argjson size "$(wc -c < "$FILE")" \
    '{language: "da-DK", sha256: $sha, byte_size: $size, mime: "application/pdf"}')"

Response 201:

json
{
  "upload_intent": {
    "id": "pi_5a3c81e0d74b426f90ac17b5e2d8630f",
    "expires_at": "2026-08-27T10:46:04+00:00",
    "upload": {
      "url": "https://…",
      "method": "PUT",
      "headers": {
        "Content-Type": "application/pdf",
        "Content-Length": "284915",
        "x-amz-checksum-sha256": "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="
      }
    }
  }
}

The file may be at most 8,000,000 bytes. A larger byte_size is rejected here, and the size is checked again at finalize.

The intent expires 15 minutes after it is created. Upload and finalize inside that window; afterwards create a new intent rather than retrying the old one. An expired intent that was never finalized is garbage-collected — it and the object it reserved are deleted the next time your company creates an intent — so an abandoned upload cannot be picked up again later.

2. Upload the bytes

bash
# Send every header the intent listed, exactly as given.
curl -sS -X PUT "$UPLOAD_URL" \
  -H "Content-Type: application/pdf" \
  -H "Content-Length: $(wc -c < "$FILE")" \
  -H "x-amz-checksum-sha256: $CHECKSUM_FROM_INTENT" \
  --data-binary "@$FILE"

Storing the bytes is not the same as accepting them. Everything is verified in the next step.

Against the local development target, Content-Length is not merely signed but required: without it there is nothing to size-check the upload against before reading it, so a request that omits it answers 411 with error.code of length_required. Most HTTP clients set it for you when the body is a file. Deployed uploads already carry the value the intent listed, so this only bites while developing locally.

3. Finalize into a version

POST /v2/pdf-templates/upload-intents/{intent_id}/finalize — scope pdf_templates:write

The path carries the pi_… intent from step 1. The template version does not exist yet; this call is what creates it, and its pt_… id comes back in the response.

bash
curl -sS -X POST \
  "https://api.lifepeaks.dk/v2/pdf-templates/upload-intents/pi_5a3c81e0d74b426f90ac17b5e2d8630f/finalize" \
  -H "Authorization: Bearer $LIFEPEAKS_API_KEY" \
  -H "Idempotency-Key: pdf_template_autumn_finalize_01"

Response 201:

json
{
  "template": {
    "id": "pt_63c07e19a4b85d2f701ea9c6b3d4820f",
    "language": "da-DK",
    "source": "partner",
    "status": "validated",
    "version": 3,
    "bytes": 284915,
    "page_count": 1,
    "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "published_at": null
  }
}

Finalizing checks the declared checksum, byte size, and MIME type, then parses the document for real. A file that fails any check is rejected and no version is created, so invalid artwork can never reach a buyer:

error.codeStatusWhat to fix
upload_checksum_mismatch422The bytes do not match the declared sha256
upload_size_mismatch422The bytes do not match the declared byte_size
upload_mime_mismatch422The object is not application/pdf
uploaded_object_missing422Nothing was uploaded for this intent
upload_intent_expired422The 15-minute window closed; create a new intent
invalid_pdf_signature422The file does not begin as a PDF
unsupported_pdf_version422The PDF version is outside 1.3 to 1.7
pdf_page_limit_exceeded422More than 10 pages
encrypted_pdf_not_supported422The document is encrypted
active_content_not_supported422It embeds JavaScript or another active action
invalid_pdf_structure422The document could not be parsed
upload_intent_already_finalized409This intent already produced a version

Two failures are not your fault and are worth retrying. When the malware scanner or the PDF parser is temporarily down, finalize answers 503 with Retry-After: 30 and an error.code of malware_scanner_unavailable or pdf_validation_unavailable. Wait and retry with the same idempotency key; do not treat the document as invalid. Every 422 above is a real rejection and will fail identically on retry.

4. Preview before publishing

GET /v2/pdf-templates/{template_id}/preview — scope pdf_templates:read

bash
curl -sS -o preview.pdf \
  "https://api.lifepeaks.dk/v2/pdf-templates/pt_63c07e19a4b85d2f701ea9c6b3d4820f/preview" \
  -H "Authorization: Bearer $LIFEPEAKS_API_KEY"

The response is the document itself as application/pdf, served as an attachment under Content-Security-Policy: default-src 'none'; sandbox. Download the bytes and open them in a PDF viewer — pointing an iframe or embed at this endpoint will not render the file. No storage URL or credential is exposed, so a preview cannot be shared outside your authenticated integration. This is the last point at which a mistake costs nothing.

5. Publish

POST /v2/pdf-templates/{template_id}/publish — scope pdf_templates:write

bash
curl -sS -X POST \
  "https://api.lifepeaks.dk/v2/pdf-templates/pt_63c07e19a4b85d2f701ea9c6b3d4820f/publish" \
  -H "Authorization: Bearer $LIFEPEAKS_API_KEY" \
  -H "Idempotency-Key: pdf_template_autumn_publish_01"

The version becomes published and the one it replaces becomes superseded.

A validated or superseded partner version can be published, so artwork retired by a revert or by a later publish can be brought back without re-uploading it. Publishing a version that is already live is a no-op and returns it unchanged. Only two things are refused, both 409: a Lifepeaks-generated default (partner_template_required), and a version that never passed validation (validated_template_required).

Revert to the generated default

POST /v2/pdf-templates/revert-to-default — scope pdf_templates:write

bash
curl -sS -X POST https://api.lifepeaks.dk/v2/pdf-templates/revert-to-default \
  -H "Authorization: Bearer $LIFEPEAKS_API_KEY" \
  -H "Idempotency-Key: pdf_template_revert_dadk_01" \
  -H "Content-Type: application/json" \
  -d '{"language": "da-DK"}'

This is the rollback for a publish that turned out wrong.

Nothing is deleted — superseded partner versions stay in the history and can be published again later without re-uploading. The call creates no new version and is safe to repeat.

Retries

upload-intents, finalize, publish, and revert-to-default all require an Idempotency-Key header. Generate one per logical action, store it, and reuse it unchanged when retrying, exactly as on the headless checkout path. A replay returns the original result instead of acting twice, so a timeout can never leave you unsure whether artwork went live. Use a separate key for each step — reusing the intent key on finalize answers 409.

The upload itself takes no key: the intent it targets already identifies the action, and re-sending the same bytes to the same target is harmless.

Which template an order uses

A new order is issued with whatever current reports for its locale, following the same four-tier chain described under Which version current reports.

Orders snapshot their template at creation. A v2 order records the version in force at that moment, so publishing new artwork later never rewrites a document a buyer has already received. Once an order is fulfilled you can fetch each line's document from its gift_card_pdf link — see Headless checkout.

An experience may bring its own artwork. When a company has configured a PDF template on one of its experiences, the cards bought from that experience are branded with it, and every other line of the same order uses the company's published v2 template. The cards of one order need not look alike, which is why each line has its own download.

Changing the generated default

The generated default is derived from the company's brand profile. Updating the profile or its assets through the brand endpoints regenerates the default as a new version — it never changes which template is live, so a company running a published partner override keeps it until it reverts explicitly.