# Changelog

All notable changes to the Wolfeo API & Webhooks are documented here.


# 2026-05-09

# Breaking — Resource id field name standardised

Every CRUD response on the public API now exposes the resource's own identifier as id instead of <resource>_id. Many-to-many link payloads (/contact-tag, /contact-sequence, /contact-custom-field, /contacts-bulk-tag) keep their {contact_id, tag_id} / {contact_id, sequence_id} shape because they describe an edge between two resources, not a single resource.

Before

{ "data": { "contact_id": 79 } }
{ "data": { "tag_id": 48, "name": "VIP" } }
{ "data": { "campaign_id": 42, "status": "draft" } }
{ "data": { "funnel_id": 17, "name": "..." } }
{ "data": { "stage_id": 55, "sequence_id": 12 } }

After

{ "data": { "id": 79 } }
{ "data": { "id": 48, "name": "VIP" } }
{ "data": { "id": 42, "status": "draft" } }
{ "data": { "id": 17, "name": "..." } }
{ "data": { "id": 55, "sequence_id": 12 } }

Affected endpoints: contacts, tags, sequences, sequence stages, campaigns, funnels, funnel steps, webhooks. The GET /v1/funnels paginated list and funnel.steps[] arrays are also updated (previously returned funnel_id / step_id).

Input parameter names are unchanged — keep sending contact_id, tag_id, sequence_id, campaign_id, funnel_id, step_id, stage_id, webhook_id in your requests.

# Breaking — Contact response cleanups

  • gdpr (lowercase) replaces GDPR in contact responses, matching the existing input field.
  • contact.tags and contact.sequences are now int[] instead of string[] (e.g. [10164] instead of ["10164"]).

# Breaking — /sales-amount envelope

GET /v1/sales-amount now returns the standard { "success": true, "data": { "amount": ... } } envelope. It used to return a raw array [{ "amount": ... }] and ad-hoc { "success": false } payloads on errors.

# Breaking — Validation errors on missing identifiers

Endpoints that accept either email or contact_id (/contact, /contact-stats, contact delete / unsubscribe, /contact-tag, /contact-sequence, /contacts-tags, /contacts-sequences, /contact-custom-field) now return 422 VALIDATION_ERROR when both are missing, instead of 404 Contact not found. The validation details field tells you which parameter to send.

# Added — SEQUENCE_HAS_NO_STAGES error

POST /v1/contact-sequence now returns 422 SEQUENCE_HAS_NO_STAGES when the target sequence has zero stages. Previously the call returned 201 success while silently skipping the enrolment because there was nothing to queue.

# Fixed — /v1/contact-tag is read-after-write consistent

POST / DELETE /v1/contact-tag used to return immediately and let a queue worker apply the change a few seconds later — a follow-up GET /v1/contact could show stale tags. The single-contact endpoint now runs inline so the response reflects the new state right away. POST /v1/contacts-bulk-tag is still asynchronous (it can target up to 500 contacts at once).

# Fixed — DELETE /v1/funnel no longer hangs

The endpoint used to scan historical analytics tables synchronously and could take 15+ seconds to return on busy funnels. The funnel and its pages are now deleted in the request (so the resource leaves listings immediately) and visitor / popup analytics are cleaned up by a background job. The response shape is { "id": <funnel_id>, "deleted": true }.

# Fixed — /v1/account

The endpoint used to leak a raw HTTP 500 when a fatal exception occurred. Errors are now caught and routed through the standard { "success": false, "error": { ... } } envelope.


# 2026-04-29

# Added — Merge Fields API support

Pages built with the Wolfeo builder now support dynamic merge fields ({{FIELD}} tokens) in Text and Title elements. When a contact is identified via the wolfeo_c cookie (set at opt-in), or via query parameters (?firstname=&lastname=&email=), the following fields resolve automatically on public pages:

Contact fields

  • {{FIRST_NAME}} — contact's first name
  • {{LAST_NAME}} — last name
  • {{EMAIL}} — email address
  • {{PHONE}} — mobile phone number
  • {{COMPANY}} — company name
  • {{CITY}} — city
  • {{COUNTRY}} — country
  • {{SIGNUP_DATE}} — date the contact joined (formatted, locale-aware)
  • {{DAYS_SINCE_SIGNUP}} — number of days since signup

Runtime fields (no contact required)

  • {{TODAY_DATE}}, {{TOMORROW_DATE}}, {{DAY_NAME}}, {{MONTH_NAME}}, {{YEAR}}, {{TIME}}

Visitor fields (IP geolocation via ipstack)

  • {{VISITOR_CITY}}, {{VISITOR_COUNTRY}}

Fallback syntax — append :default value to any token:

{{FIRST_NAME:dear visitor}}

If the field cannot be resolved, the fallback is rendered instead. If neither is available, the token is removed cleanly (no residual whitespace or <br>).

XSS — all resolved values are escaped with htmlspecialchars before injection into the page HTML.