Skip to content
Norbelys
Esc
↑↓navigate↵open⌘Jpreview
On this page

Images in email and signatures

Upload small public images through the API and use permanent HTTPS URLs in authored HTML.

Images use ordinary <img> elements in html or footer.html. A signature remains optional: footer: null means no sender signature. A configured signature is appended automatically unless the authored body places it with {{ footer }}. An image does not change those rules or create a plain-text alternative.

Upload once through the API

POST /v1/images accepts a JSON object with data_base64: standard base64 of a PNG or JPEG file, without a data: prefix. Use the same workspace credential as your senders. The maximum decoded size is 512 KiB, with at most 2048 pixels per side and a 32 MiB decoding allocation limit. The entire image is decoded for validation, then stored unchanged. SVG, HTML, GIF, WebP and arbitrary documents are not accepted by this endpoint. Convert/optimize them before uploading if needed.

const response = await fetch("https://api.norbelys.com/v1/images", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.NORBELYS_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ data_base64: fileBytes.toString("base64") }),
});
const image = await response.json();
if (!response.ok) throw new Error(image.detail);
// image.url, image.file, image.content_type, image.bytes, image.width, image.height

The response is HTTP 200 and contains a permanent public url under the API’s configured HTTPS origin. Uploading the same bytes again in the same workspace returns the same file and URL: no upload-finalize job, database resource or campaign refresh is required. Different workspaces have isolated object prefixes. Validation has bounded concurrency; HTTP 429 means retry later.

Only upload images intended to be public. Anyone with the returned URL can read them, including mail-client image proxies. The storage bucket itself remains private; this public route only serves canonical uploaded image paths. CSVs, credentials, reports and other R2 objects are not exposed. Original image metadata is retained.

Use the returned URL

Paste the exact returned URL into authored HTML, or store it in sender.profile.custom_fields.logo_url and reference it as sender.custom_fields.logo_url. Merge that field into the existing profile: PATCHing profile replaces the profile object, so preserve the other fields.

<p>{{ sender.first_name }} {{ sender.last_name }}</p>
{% if sender.custom_fields.logo_url | default("") %}
<p><img src="{{ sender.custom_fields.logo_url }}"
        alt="Company logo" width="160"
        style="display:block;max-width:160px;height:auto;border:0"></p>
{% endif %}
<p><a href="https://{{ sender.domain }}">Visit our website</a></p>

Set this fragment as footer.html. Supply a readable footer.text with the sender/company/contact details; plain text does not display images. The same HTML works directly in a message or campaign variant. Put a link around an image when it should be clickable; campaign click tracking applies to that link, not the image download. An uploaded image is not an open-tracking pixel.

The body can choose any authored position:

<p>Hello, {{ recipient.given_name | default("there") }}.</p>
<p>Here is the information you requested.</p>
{{ footer }}
<p>P.S. You can reply with any questions.</p>

Without {{ footer }}, the selected sender’s configured signature is appended once. Without a configured signature, the token resolves to empty and nothing is appended. HTML and plain text choose their positions independently. A conditional containing the token can also omit that signature for the message; do not use triple braces or | safe. See sender profiles for the complete placement contract.

Norbelys or customer domain

The API’s returned Norbelys URL is the default: it has no expiring credential, requires no customer DNS work and can be reused across messages. Operators must set API_PUBLIC_URL to their canonical HTTPS origin when self-hosting; object storage uses the existing OBJECT_STORE_URL configuration.

A customer may instead use an existing public HTTPS image URL on their own domain in <img src>. The customer owns its TLS, availability and retention. Norbelys does not fetch or copy remote URLs during upload or rendering. A marketing redirect domain or tracking CNAME is not automatically an image host: do not replace the image URL’s hostname with sender.domain. This release does not provision a customer image subdomain; the customer must already serve/proxy that image there.

Avoid expiring R2 presigned GET URLs in sent mail: a recipient may open the message after they expire. Avoid embedding base64 data: images or assuming a remote image is a MIME/CID attachment. This feature serves linked images; it does not create CID attachments or guarantee that every client displays external images automatically. Gmail/Outlook may proxy, cache or block remote images. Keep essential copy in text, set descriptive alt text and explicit display dimensions, and verify a received example in the intended mail clients.

Replacement and removal

New bytes produce a new URL. Update the sender’s logo_url for future content, leaving old images available for already sent messages. The existing message snapshot/live-campaign rules still apply; do not rebuild the queue for image edits.

DELETE /v1/images/{file} removes only the authenticated workspace’s copy and returns 204 idempotently. Public responses cache for one hour; mail clients may cache longer. Deletion can break images in old mail and cannot erase copies already downloaded by recipients. Workspace deletion also removes owned image objects in bounded worker passes. There is no automatic age-based expiry of email images.

Was this page helpful?