# 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) replacesGDPRin contact responses, matching the existing input field.contact.tagsandcontact.sequencesare nowint[]instead ofstring[](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.