Upload an image asset

Goal: put an image on the platform’s CDN and reference it from a template’s HTML.
Prerequisites: an API key with the templates:write and templates:read scopes (see Quickstart); a PNG, JPEG, GIF or WebP file of at most 5 MiB; Templates for the template model.

Uploading takes two API calls with a direct upload in between: you reserve the asset and receive a short-lived upload URL, you send the bytes to that URL, then you confirm. The confirm step validates the file and publishes its CDN URL — an upload that is never confirmed never becomes an asset.

Steps

1. Reserve the upload

curl -X POST https://emails.routeon.io/api/v1/assets/presign 
  -H "Authorization: Bearer <API_KEY>" 
  -H "Content-Type: application/json" 
  -d '{
    "filename": "header-logo.png",
    "content_type": "image/png",
    "size_bytes": 48213
  }'

Expected response (201 Created):

{
  "status": "success",
  "http_code": 201,
  "payload": {
    "asset_id": "<ASSET_ID>",
    "upload_url": "<UPLOAD_URL>",
    "required_headers": { "Content-Type": "image/png" }
  },
  "meta": {}
}

content_type must be image/png, image/jpeg, image/gif or image/webp — SVG is not accepted. size_bytes must be the size of the file you upload, between 1 byte and 5 MiB. The declared type is only an up-front filter; the binding check happens at confirm, against the file’s actual content. The upload URL is short-lived — upload straight away rather than storing it.

2. Upload the bytes

PUT the file to upload_url, sending every header from required_headers verbatim. Do not send your API key here — the URL carries its own authorization.

curl -X PUT "<UPLOAD_URL>" 
  -H "Content-Type: image/png" 
  --data-binary @header-logo.png

Storage answers with a 2xx status and an empty body. A missing or altered required header is rejected by storage itself, not by the API.

3. Confirm the upload

curl -X POST https://emails.routeon.io/api/v1/assets/<ASSET_ID>/confirm 
  -H "Authorization: Bearer <API_KEY>"

Expected response (201 Created):

{
  "status": "success",
  "http_code": 201,
  "payload": {
    "cdn_url": "https://assets.emails.routeon.io/img/<TENANT_ID>/<RANDOM_ID>.png",
    "moderation_status": "approved"
  },
  "meta": {}
}

Confirm checks the file’s real type and size, moves it to its permanent location, runs image moderation and returns the CDN URL. The URL is generated by the platform and does not contain your original filename. Confirming an asset that is already stored returns the same cdn_url, so the call is safe to repeat. If confirm rejects the file (see the failures table), that reservation cannot be reused: start again with a new presign.

4. Use the asset in a template

Reference cdn_url from the template’s HTML like any other image:

curl -X PATCH https://emails.routeon.io/api/v1/templates/<TEMPLATE_ID> 
  -H "Authorization: Bearer <API_KEY>" 
  -H "Content-Type: application/json" 
  -d '{
    "format": "html",
    "subject": "What'''s new this month",
    "source": "<html><body><img src="https://assets.emails.routeon.io/img/<TENANT_ID>/<RANDOM_ID>.png" alt="Example Co" width="200"><p>Hi {{first_name}}</p><p><a href="{{unsubscribe_url}}">Unsubscribe</a></p></body></html>"
  }'

200 OK returns the template. Every content edit creates a new version and returns the template to status: "draft", even if it was active: verify the new version with POST /templates/{id}/verify ({"version": <NEW_VERSION>}) and activate it again with PATCH /templates/{id} and {"status": "active"} before sending with it. The new version number is current_version in the response.

Moderation statuses

moderation_status on a confirmed asset is one of:

Status Meaning
approved The image passed moderation and can be used.
rejected Moderation refused the image (confirm answers 422 image_rejected). Use different artwork.
pending No verdict could be obtained. Treat it as not approved rather than queued — call confirm again later.

A template’s links are checked separately from its images, when you verify a version. GET /templates/{id}/versions/{version}/links shows the stored result: each link has a status and a stage telling how far it got before its verdict was fixed — skipped (allowlisted or not a web address; counts as approved), reputation, content, or not_checked (the check ended before reaching this link).

Verify

List your stored assets:

curl https://emails.routeon.io/api/v1/assets 
  -H "Authorization: Bearer <API_KEY>"

payload.items lists only confirmed assets. Each item carries asset_id, cdn_url, filename, content_type, size_bytes, moderation_status, used_by_templates, used_by_campaigns and expires_at. An asset nothing references has a non-null expires_at and is eventually removed; referencing it from a template clears that.

Remove an asset you no longer need with DELETE /assets/{id}; 200 OK answers { "deleted": true }.

Common failures

Symptom Cause Fix
422 upload_not_found on confirm Nothing was uploaded to the upload URL, or it expired before the PUT completed Presign again and upload before confirming
422 invalid_image on confirm The file’s content is not one of the accepted image types, disagrees with the declared content_type, or is larger than 5 MiB Presign again with the real type and size of a supported image
422 image_rejected on confirm Moderation refused the image Use different artwork and presign again
422 VALIDATION_ERROR on presign content_type not accepted (SVG is not), or size_bytes is 0 or above 5 MiB Convert or resize the image
moderation_status is pending No moderation verdict was available Do not use the asset yet — call confirm again later
403 on presign or confirm The API key lacks the templates:write scope Use a key with that scope
409 asset_in_use on delete A template or campaign still references the asset Remove the references first — GET /assets lists them
422 template_not_active when sending after the edit The edited template is back in draft Verify and activate the new version