Developer reference

Build with the DynaMail API

Create and manage widgets, push ordered live values, and deliver dynamic images from a small HTTP API. The production base URL is https://api.dynamail.io. Routes are currently unversioned.

Authentication

Open Dashboard → API Keys, create a named key, and store the full dm_sk_... value when shown. DynaMail stores its hash and cannot display it again. Send the key in the X-API-Key header.

GET /me is the smallest connection test and requires a valid key but no scope. Use the returned data.id as the userId when creating a widget.

Connection test

curl https://api.dynamail.io/me \
  -H "X-API-Key: dm_sk_your_key_here"

200 response

{
  "success": true,
  "data": { "id": "YOUR_ACCOUNT_ID" }
}

Quickstart

  1. 1

    Create API keys

    In Dashboard → API Keys, create a Full access key for the widget creation step. For recurring pushes, create a separate Automation key and keep each key in your credential store.

  2. 2

    Find your account ID

    Call GET /me. The returned data.id is YOUR_ACCOUNT_ID in the next request.

    Request

    curl https://api.dynamail.io/me \
      -H "X-API-Key: dm_sk_your_key_here"

    200 response

    {
      "success": true,
      "data": { "id": "YOUR_ACCOUNT_ID" }
    }
  3. 3

    Create a Stat Counter

    POST /widgets requires widgets:write, which is included in the Full access preset.

    Request

    curl --request POST 'https://api.dynamail.io/widgets' \
      --header 'Content-Type: application/json' \
      --header 'X-API-Key: dm_sk_your_key_here' \
      --data '{"name":"Signups","type":"stat_counter","userId":"YOUR_ACCOUNT_ID","config":{"value":1250,"label":"Signups this week"}}'

    201 response (trimmed)

    {
      "success": true,
      "data": {
        "id": "WIDGET_ID",
        "userId": "YOUR_ACCOUNT_ID",
        "type": "stat_counter",
        "name": "Signups",
        "config": {
          "value": 1250,
          "label": "Signups this week",
          "dslLayout": "…",
          "tokens": "…"
        },
        "createdAt": "2026-09-12T12:34:56.789Z",
        "updatedAt": "2026-09-12T12:34:56.789Z"
      }
    }

    The API stamped the default layout and tokens, so the minimal configuration is ready to render. Save data.id as WIDGET_ID.

  4. 4

    Embed the image

    API calls use api.dynamail.io; delivered images use img.dynamail.io. The stamped Stat Counter is 340×184.

    HTML embed

    <img src="https://img.dynamail.io/widgets/WIDGET_ID/render.png" width="340" height="184" alt="Signups this week">
  5. 5

    Push a live value

    Use the Automation key, which includes live-value:write. Increase sourceVersion with each newer upstream event.

    Request

    curl --request PUT 'https://api.dynamail.io/widgets/WIDGET_ID/live-value' \
      --header 'Content-Type: application/json' \
      --header 'X-API-Key: dm_sk_your_key_here' \
      --data '{"value":1250,"sourceVersion":1}'

    200 receipt

    {
      "success": true,
      "data": {
        "widgetId": "WIDGET_ID",
        "revision": 1,
        "active": true,
        "value": 1250,
        "sourceVersion": 1,
        "updatedAt": "2026-09-12T12:34:56.789Z",
        "applied": true,
        "reason": "applied"
      }
    }

    Check data.applied and data.reason; a successful HTTP response can report a stale or unchanged event.

  6. 6

    Optional: personalize by recipient

    Add the same recipient identifier to the update and the image URL.

    Recipient request

    curl --request PUT 'https://api.dynamail.io/widgets/WIDGET_ID/live-value' \
      --header 'Content-Type: application/json' \
      --header 'X-API-Key: dm_sk_your_key_here' \
      --data '{"value":1250,"sourceVersion":1,"recipient":"person@example.com"}'

    Recipient HTML embed

    <img src="https://img.dynamail.io/widgets/WIDGET_ID/render.png?recipient=person%40example.com" width="340" height="184" alt="Signups this week">

    For many recipients, PUT /widgets/WIDGET_ID/live-value/bulk accepts 1–500 unique recipient entries in one request.

Scopes

Choose the narrowest preset that supports the integration. When an endpoint lists more than one acceptable scope, the rule is ANY-of: the key needs one, not all. Keys created before scoped keys were introduced retain full access.

ScopeAccess
widgets:readList owned widgets and widget summaries.
widgets:writeCreate, update, and delete owned widgets.
live-value:readRead an owned widget's pushed live value.
live-value:writeSet, increment, bulk-set, or clear pushed live values; also list widget summaries.

Automation

Push live values and read widgets. Use for Zapier, n8n, and scripts.

live-value:read, live-value:write, widgets:read

Read only

List and read widgets and live values.

widgets:read, live-value:read

Full access

Create, edit, and delete widgets too.

widgets:read, widgets:write, live-value:read, live-value:write

403 missing-scope response

{
  "error": {
    "code": "FORBIDDEN",
    "message": "API key lacks the required scope",
    "context": {
      "required": ["widgets:write"],
      "granted": ["widgets:read", "live-value:read", "live-value:write"]
    }
  }
}

Endpoint reference

Schema names identify each operation's request and response shapes in the downloadable OpenAPI document. An empty scopes field means the endpoint is public or only requires a valid key, as described in its summary.

Fundraising

post/widgets/{id}/gifts201

Add a gift to a fundraising campaign

Scopes
live-value:write
Schemas
AddWidgetGiftRequestBody → WidgetGiftReceiptEnvelope
Extra statuses
409, 429

Account

get/me200

Get the authenticated API account identity

Scopes
None listed
Schemas
ApiIdentityEnvelope
Extra statuses
429, 503

Latest Content

get/latest-content/{id}200

Get the current published latest content item

Scopes
None listed
Schemas
LatestContentPublicEnvelope
Extra statuses
None
get/latest-content/{id}/image.png200

Get the generation-bound latest content image

Scopes
None listed
Schemas
Binary response
Extra statuses
None

Widgets

post/widgets201

Create widget

Scopes
widgets:write
Schemas
CreateWidgetRequestBody → WidgetEnvelope
Extra statuses
429, 503
get/widgets200

List widgets

Scopes
widgets:read
Schemas
WidgetListEnvelope
Extra statuses
429, 503
get/widgets/summaries200

List owned widget metadata without configuration

Scopes
widgets:read or live-value:write
Schemas
WidgetSummaryListEnvelope
Extra statuses
429, 503
get/widgets/{id}200

Get widget by ID

Scopes
None listed
Schemas
PublicWidgetEnvelope
Extra statuses
None
patch/widgets/{id}200

Update widget

Scopes
widgets:write
Schemas
UpdateWidgetRequestBody → WidgetEnvelope
Extra statuses
409, 429, 503
delete/widgets/{id}200

Delete widget

Scopes
widgets:write
Schemas
SuccessMessageResponse
Extra statuses
429, 503
get/widgets/{id}/live-value200

Get the live value (Stat Counter, Progress Bar, Radial Progress, Gauge Progress, Vertical Progress)

Scopes
live-value:read
Schemas
LiveNumericValueReadEnvelope
Extra statuses
429, 503
put/widgets/{id}/live-value/bulk200

Set recipient live values in bulk (Stat Counter, Progress Bar, Radial Progress, Gauge Progress, Vertical Progress)

Scopes
live-value:write
Schemas
BulkSetLiveNumericValueRequestBody → BulkLiveNumericValueEnvelope
Extra statuses
409, 429, 503
put/widgets/{id}/live-value200

Set the live value (Stat Counter, Progress Bar, Radial Progress, Gauge Progress, Vertical Progress)

Scopes
live-value:write
Schemas
SetLiveNumericValueRequestBody → LiveNumericValueMutationEnvelope
Extra statuses
409, 429, 503
post/widgets/{id}/live-value/increment200

Increment the live value (Stat Counter, Progress Bar, Radial Progress, Gauge Progress, Vertical Progress)

Scopes
live-value:write
Schemas
IncrementLiveNumericValueRequestBody → LiveNumericValueMutationEnvelope
Extra statuses
409, 429, 503
delete/widgets/{id}/live-value/recipients200

Clear all recipient live values (Stat Counter, Progress Bar, Radial Progress, Gauge Progress, Vertical Progress)

Scopes
live-value:write
Schemas
DeleteLiveRecipientValuesEnvelope
Extra statuses
429, 503
delete/widgets/{id}/live-value200

Clear the live value (Stat Counter, Progress Bar, Radial Progress, Gauge Progress, Vertical Progress)

Scopes
live-value:write
Schemas
LiveNumericValueMutationEnvelope
Extra statuses
409, 429, 503

Render

get/widgets/{id}/render.png200

Render widget as PNG

Scopes
None listed
Schemas
Binary response
Extra statuses
None
get/widgets/{id}/render.gif200

Render widget as GIF

Scopes
None listed
Schemas
Binary response
Extra statuses
None
get/widgets/{id}/render.webp200

Render widget as WebP

Scopes
None listed
Schemas
Binary response
Extra statuses
None
get/widgets/{id}/render.apng200

Render widget as animated PNG

Scopes
None listed
Schemas
Binary response
Extra statuses
None
get/widgets/{id}/render.jxl200

Render widget as JPEG XL

Scopes
None listed
Schemas
Binary response
Extra statuses
None
get/widgets/{id}/render.jpg200

Render widget as JPEG

Scopes
None listed
Schemas
Binary response
Extra statuses
None
get/widgets/{id}/render.jpeg200

Render widget as JPEG (alias)

Scopes
None listed
Schemas
Binary response
Extra statuses
None

Meta

get/openapi.json200

Get the public OpenAPI document

Scopes
None listed
Schemas
OpenApiDocument
Extra statuses
None

Health

get/health/live200

Liveness check

Scopes
None listed
Schemas
LivenessResponse
Extra statuses
None
get/health200

Health check

Scopes
None listed
Schemas
HealthResponse
Extra statuses
None

Assets

get/assets/{userId}/{hashDotExt}200

Get a user image by content hash

Scopes
None listed
Schemas
Binary response
Extra statuses
None

Backgrounds

get/backgrounds/{category}/{filename}200

Get a background image by category and filename

Scopes
None listed
Schemas
Binary response
Extra statuses
None
get/backgrounds/id/{imageId}200

Get a background image by ID

Scopes
None listed
Schemas
Binary response
Extra statuses
None

Live values

Render precedence is an explicit URL ?value= or ?current= override, then a non-tombstoned recipient row, the global pushed value, a pull source, and finally authored configuration or 0.

value must be an integer from -1,000,000,000,000 through 1,000,000,000,000. sourceVersion is a positive safe integer that increases with the upstream system’s ordering. Prefer its revision or change sequence; retries of one event must reuse the same version. Do not derive it from request arrival time. Older writes return stale, identical retries return unchanged, and an equal version with a different value returns 409.

Count events. Post a non-zero integer delta when each trigger should add to or subtract from the current value. Increments are atomic. Send a unique eventId so retries count once per widget within 7 days; without one, every request counts.

Request

curl --request POST 'https://api.dynamail.io/widgets/WIDGET_ID/live-value/increment' \
  --header 'Content-Type: application/json' \
  --header 'X-API-Key: dm_sk_your_key_here' \
  --data '{"delta":1,"eventId":"order-123"}'

Recipient keys are URL-decoded once, trimmed, normalized to Unicode NFC, and compared case-insensitively. DynaMail stores only a salted, widget-specific hash. A bulk request accepts 1–500 unique normalized recipients, processes them in input order, and retains at most 10,000 recipient rows per widget.

For Progress Bar, Radial Progress, Gauge, and Vertical Progress widgets, the pushed value fills current. Keep target and styling in the embed URL. Negative values render as 0, and a value past the target fills the widget while its text shows the real total. An explicit ?current= always wins. Step Progress, Wave, and Timeline do not support pushed live values.

Clearing preserves the widget design and writes a tombstone with the last accepted source version, so a delayed retry cannot restore the value. A later update needs a strictly greater version. Recipient clears fall through to the global value.

StatusMeaning
400 Bad RequestThe request body, query, or widget type is invalid.
401 UnauthorizedThe API key is missing, malformed, unknown, or revoked.
403 ForbiddenThe key lacks every acceptable scope for the operation.
404 Not FoundThe resource is missing or belongs to another account.
409 ConflictA revision, source-version, source, or data-source conflict occurred.
429 Too Many RequestsThe authenticated account quota is exhausted.
503 Service UnavailableShared quota storage is unavailable.

Errors and rate limits

StatusMeaning
400 Bad RequestThe request body, query, or widget type is invalid.
401 UnauthorizedThe API key is missing, malformed, unknown, or revoked.
403 ForbiddenThe key lacks every acceptable scope for the operation.
404 Not FoundThe resource is missing or belongs to another account.
409 ConflictA revision, source-version, source, or data-source conflict occurred.
429 Too Many RequestsThe authenticated account quota is exhausted.
503 Service UnavailableShared quota storage is unavailable.

Hourly account quota

TierRequests per hour
Indie100
Growth500
Business2,500
Enterprise10,000

Authenticated widget calls share one hourly allowance across all of an owner’s keys and client IPs. Unresolved key attempts also have a 300-per-minute, per-IP protection. For both 429 quota exhaustion and 503 quota-storage outages, honor Retry-Afterand retry unchanged input; a 503 currently reports a five-second delay. For most widget types, PATCH /widgets/:id replaces the complete stored config.

Automations

Use manual HTTP connections for Make, n8n, Zapier, or your own worker. DynaMail is not in the Zapier directory. Keep one named key per integration so it can be revoked independently.

Frequently asked questions

Which key do I need to create a widget?

Creating a widget requires widgets:write, which is included in the Full access preset. Recurring live-value pushes only need the Automation preset.

Are API routes versioned?

No. The production base URL is https://api.dynamail.io and routes are currently unversioned.

How do API key scopes work?

When an endpoint lists multiple acceptable scopes, the rule is ANY-of: the key needs one, not all, of those scopes.

Can I safely retry a live-value update?

Yes. Reuse the same sourceVersion for the same upstream event and follow Retry-After for 429 or 503 responses.

What does PATCH do to widget configuration?

For most widget types, PATCH replaces the stored config as a whole. Read and preserve the complete current config before editing it.

Is there a native Zapier app?

No. DynaMail is not in the Zapier directory; use API by Zapier with an X-API-Key header.