Automated email delivery
AI Presentations emails the presentation link directly to homeowners when a presentation is created from a journey ingest, and on demand when you call the send endpoint. Each deck has its own delivery configuration with a custom sender name, reply-to address, subject line, preview text, body template, and CTA label. Every send is recorded as a delivery attempt regardless of outcome.A homeowner with no email address cannot receive the presentation email. Manual sends with override recipients bypass this check; recipient suppression still applies.
skipped in two cases:
- The homeowner record has no email address. Manual sends with override recipients bypass this check.
- A recipient is suppressed. Before every send, AI Presentations checks each recipient against delivery outcomes from the last 90 days, across every organization. Addresses match case-insensitively. An address is suppressed after a hard bounce, a spam report, an unsubscribe, or a SendGrid drop with the reason
Bounced Address,Invalid,Spam Reporting Address, orUnsubscribed Address. Blocked bounces, deferrals, and other drop reasons do not suppress an address. If any recipient is suppressed, AI Presentations skips the whole send rather than emailing the remaining recipients.
failed, not skipped, and nothing is sent.
A send is also failed when AI Presentations could not load the homeowner’s information — for example, when the underlying Demand IQ lookup is unreachable. The attempt row records the true reason instead of blaming the lead’s data, and manual senders see an error toast rather than a blank-personalization email going out. There is no separate enable toggle — if a deck is connected to a journey or you call the send endpoint, the email goes out.
Configure per-deck delivery settings from the Email tab in the deck editor, or via the API:
GET /api/decks/{deckId}/delivery— read the current delivery config asdelivery, plus the sender the deck’s email goes out from assender.sender.emailis the from address, andsender.defaultNameis the sender name used whenfromNameis unset. AI Presentations resolves both the same way it does at send time. See Sender and reply-to.PUT /api/decks/{deckId}/delivery— set sender name (fromName), reply-to address (replyToEmail), subject (emailSubject), preview text (emailPreheader), reminder subject (reminderEmailSubject), reminder switch (reminderEnabled), body (emailBodyTemplate), and CTA label (ctaLabel). AI Presentations trimsreplyToEmailand stores a blank value as unset. IfreplyToEmailis not a valid email address, the request returns400with{ "error": "Reply-to must be a valid email address" }and nothing is saved.reminderEnabledmust betrueorfalse. Any other value returns400with{ "error": "reminderEnabled must be true or false" }. If you omitreminderEnabled, AI Presentations storesfalse.emailHomePhotoEnabledturns the home photo on or off and must betrueorfalse. Any other value returns400with{ "error": "emailHomePhotoEnabled must be true or false" }. If you omitemailHomePhotoEnabled, AI Presentations keeps the stored value. New decks start with it off.GET /api/decks/{deckId}/email-preview— return the presentation picture the deck’s next email would carry, as{ "url": "https://..." }.urlisnullwhen the email would go out without a picture. The request returns404with{ "error": "Deck not found" }for a deck outside your company.
emailBodyTemplate or ctaLabel matches the current default exactly, AI Presentations stores it as unset. The deck then follows future changes to the default copy.
Five text fields have a maximum length:
If any of these fields is too long, the request returns
400 with { "error": "<message>" } naming the field, and nothing is saved. A value exactly at the limit saves. In the Email tab, these inputs stop accepting characters at the limit. If inserting a token chip would push a field past its limit, the editor refuses the insert and shows a toast. It never truncates the token, because a clipped {{token}} would be sent literally.
Follow-up sends
After ingest sends the first email, callPOST /api/presentations/send to send a follow-up, such as a reminder. Authenticate with a Bearer token in the Authorization header. Name the presentation with the presentation_id that ingest returned. A company credential only reaches its own company’s presentations.
- Instead of
presentation_id, you can send thedeck_idandsource_event_idyou passed to ingest. Both fields are then required. A request that includespresentation_idtogether withdeck_idorsource_event_idreturns400. idempotency_keyis required and must be 1 to 64 characters. Each key gets one delivery attempt and at most one email.skip_ifis optional and defaults to[].openedskips the send if the homeowner opened an earlier email. Machine opens don’t count.viewedskips the send if the homeowner viewed the presentation.subject_variantmust bereminder. Ingest sends the first email, soinitialreturns400.
skip_if contains. AI Presentations reads the deck’s reminder switch at send time. If reminders are off for the deck, or the deck has no saved email settings, the reminder is skipped with the reason reminder_disabled. Turning the switch off therefore cancels reminders that are already scheduled.
A repeated call with the same key never sends a second email. Once the first call finishes, a repeat returns its original response. If a call stops mid-send, AI Presentations lets a new call with the same key take over after 10 minutes. Every skip except
no_presentation is recorded as a delivery attempt and emits one email.sent event with its skipReason.
Sender and reply-to
Every homeowner email comes fromno-reply@demand-iq.com. The sender name shown to the homeowner is the first of these that is set:
- The deck’s sender name (
fromName) - Your organization’s name
Demand IQ
- The deck’s reply-to address (
replyToEmail) - Your company contact email
no-reply@demand-iq.com. AI Presentations skips a stored address that is not a valid email instead of sending with it.
The company name in the email header and footer is the first of these that is set:
- The deck’s Company Name from the Branding tab
- Your organization’s name
- The deck’s sender name (
fromName) Demand IQ
Unsubscribe link
Every presentation email includes an unsubscribe link, whether it is the initial send, a reminder, or a manual send. The HTML footer reads “Don’t want these emails? Unsubscribe”, and the plain-text version ends with an unsubscribe line. AI Presentations also sends the one-click unsubscribe headers that Gmail and Yahoo require, so inbox clients can show their own Unsubscribe button next to the sender. The unsubscribe covers presentation emails only. Other Demand IQ email to the same address is unaffected. Once a homeowner unsubscribes, AI Presentations suppresses the address and skips later sends with the reasonsuppressed. See Automated email delivery. Test sends carry the link too, so clicking it in your own inbox suppresses your address.
The email preview in the Email tab shows the footer link, but it does not unsubscribe anyone.
Default subject line
When a deck has no subject line of its own, AI Presentations uses a default at send time:Dana, your presentation is readywhen the homeowner’s first name is knownYour presentation is readywhen the first name is empty or missing
{{homeowner.first_name}}, check this out! goes to a homeowner with no first name as Your presentation is ready, not , check this out!. Because of this fallback, subject tokens never park a presentation at missing_data.
AI Presentations checks the deck’s copy once, when it creates the presentation. Every later send, including a manual resend and the release of a parked presentation, renders the deck’s current subject. The fallback covers a subject you save after the presentation was created.
In the deck editor, the subject field starts empty and shows the default as its placeholder. If you save the Email tab with an empty subject, AI Presentations stores no subject and sends the default. The preview header fills in subject tokens with sample values and applies the same fallback as the sender, so it shows the subject AI Presentations will send. For a deck with no subject yet, the preview shows the default subject.
Reminder subject line
Reminders are off by default. To turn them on for a deck, switch on Send a reminder if the first email isn’t opened in 2 days in the Reminder email section of the Email tab, or setreminderEnabled to true through the API. The switch applies to every presentation sent from the deck. While the switch is off, the Reminder subject line field is disabled.
A reminder is the same email with a different subject line. When a deck has no reminder subject, AI Presentations uses Reminder: followed by the first email’s subject at send time. For example, a deck with the subject Your solar quote, {{homeowner.first_name}} sends the reminder Reminder: Your solar quote, Dana. When the deck has no first-email subject either, AI Presentations uses the default subject:
Reminder: Dana, your presentation is readywhen the homeowner’s first name is knownReminder: Your presentation is readywhen the first name is empty or missing
Reminder: followed by the subject the homeowner received first.
To override the default, fill in the optional Reminder subject line field, or set reminderEmailSubject through the API. The field’s placeholder shows the default reminder subject. The field accepts the same tokens as the subject line. A saved reminder subject renders as entered. A blank value falls back to the default. If any token in a saved reminder subject renders empty or is unknown, AI Presentations sends the default reminder subject instead. Reminder subject tokens never park a presentation at missing_data.
The first email never uses the reminder subject. A reminder reuses the deck’s message body, CTA label, and presentation picture.
Preview text
Preview text is the short line inbox clients show next to the subject, before the homeowner opens the email. AI Presentations places it in a hidden block at the top of the email, so it appears in the inbox row but not in the email body. It is not included in the plain-text version of the email. When a deck has no preview text of its own, AI Presentations uses a default at send time:Prepared for 123 Main St, with answers to your questions any timewhen the homeowner’s address is knownYour custom results are inside, with answers to your questions any timewhen the address is empty or missing
emailPreheader through the API. The field accepts the same tokens as the subject line. A blank value saves as unset, and the default applies. If any token in saved preview text renders empty or is unknown, AI Presentations sends the default preview text instead. Preview text tokens never park a presentation at missing_data.
The preview header in the deck editor shows the subject and preview text together as an inbox row.
Presentation picture
When the presentation picture is enabled for your organization, presentation and reminder emails show a linked picture of the presentation with a play button. The picture sits between the email header and the greeting, and links to the homeowner’s presentation. Its alt text names your company. The CTA button stays below the message body. AI Presentations builds the picture from the first slide with a usable image. Product slides are skipped. If AI Presentations can’t build the picture, the email sends without it and leaves no empty gap. The email preview in the Email tab shows the same picture the sender would use.Home photo
The Photo of their home switch, marked Beta, sits under Your presentation preview in the Email tab. It is off by default. The switch appears only when the home photo feature is turned on for your account. Contact Demand IQ to turn it on. When the switch is on, AI Presentations looks up a photo of the homeowner’s home at send time. The lookup needs a street address for the homeowner. If AI Presentations finds a photo, the email leads with it in place of the presentation picture:- The photo is 4:3 and carries the same play button as the presentation picture. It links to the homeowner’s presentation.
- Its alt text names the home, for example
Your home at 412 Maple Drive. Watch your presentation. - The street address appears as a caption under the photo.
- The reminder email reuses the same photo.
A missing home photo never stops the email from sending.
Default message body
When a deck has no message body of its own, AI Presentations uses a default at send time. It uses the most personal version whose tokens all render for the homeowner:Hi Dana,followed byWe put together a short presentation for your home at 123 Main St.when the first name and address are knownHi Dana,followed byWe put together a short presentation just for you.when only the first name is knownHi,followed byWe put together a short presentation for your home at 123 Main St.when only the address is knownHi,followed byWe put together a short presentation just for you.when neither is known
Watch it, then ask anything in the chat. You'll get an answer right away. A body that contains only whitespace counts as no body, and the default is never saved to the deck.
Default CTA label
When a deck has no CTA label of its own, the email button readsWatch Your Presentation →.
When a deck has a saved body, AI Presentations renders it for each homeowner at send time. If a contact or journey token in the saved body renders empty, AI Presentations sends the default body instead. For example, a body that opens with Hi {{homeowner.first_name}}, never reaches a homeowner with no first name as Hi ,. Because of this fallback, contact and journey tokens in the body never park a presentation at missing_data.
Roof and cost tokens in the body are the exception. They stay blocking: a lead whose roof report has not run parks instead of sending. See Roof and cost tokens are not offered in email.
The Email tab trims the body before saving it. The email preview applies the same fallback as the sender.
Subject, preview text, and body tokens
The subject line, preview text, and message body accept{{token}} placeholders that AI Presentations resolves per recipient at send time. Under each field, five inline chips insert the most common tokens at your cursor. View all opens a popover with the full list, and typing {{ still opens the autocomplete dropdown.
Datetime tokens resolve from the presentation’s
datetimeSnapshot (America/Los_Angeles, en-US), the same basis slide narration uses, so a template that says {{today}} renders as January 21, 2026.
The CTA button label does not accept tokens.
Roof and cost tokens are not offered in email
The picker deliberately omitsroof_squares, roof_area_sqft, roof_pitch, estimated_cost, min_cost, max_cost, cost_per_square, and ten_year_financed_cost. These fields only exist once the roof report has run. Unlike contact tokens, an unresolved roof or cost token in an email body is blocking, with no fallback to the default body: a lead whose report has not run parks instead of sending. Since lazy generation only starts when the homeowner opens the link (POST /api/presentations/{presentationId}/start), an email that never sends is a presentation that never generates — one roof token in a shared template can stall every unmeasured lead for the full 24-hour escalation window. Use these fields in slide narration instead, where a missing value costs one sentence rather than the entire send.
Templates that already include one of these tokens keep working — the sender still resolves them so no homeowner receives raw {{braces}}. New templates should leave them out.
Trigger a send (or resend) for a specific presentation:
POST /api/presentations/{presentationId}/send— send the homeowner email using the deck’s delivery config. Optionally pass{ "emails": ["jordan.lee@example.com"] }to override the default recipients. AI Presentations trims each address and accepts up to 20. If any address is invalid or the list has more than 20 entries, the request returns400with{ "error": "Enter up to 20 valid email addresses" }and nothing is sent. An empty list or no body sends to the homeowner. Returns adelivery_idand a status ofsent,skipped, orfailed. When a recipient is suppressed, the response has statusskippedand anerrornaming the address and the reason, for example"dana.ortiz@example.com can't be emailed — the recipient unsubscribed". When the suppression check can’t run, the response has statusfailedand theerror"Could not check this address — try again shortly". If the presentation’s own generation failed, the send is refused with a409and codePRESENTATION_FAILED— there’s nothing usable to deliver.
email.sent event. When SendGrid reports the recipient has opened the message, AI Presentations records the open and emits an email.opened event — subscribe to it from your existing notification subscriptions to track opens in real time. email.opened fires once per delivery, on the first open SendGrid reports. Opens triggered by mail-client privacy features (such as Apple Mail Privacy Protection prefetching images) count as opens. When you fetch these entries from GET /api/notifications/events, each email.opened entry carries a top-level machineOpen flag: true when every open SendGrid reported for the delivery was automatic (for example, an MPP prefetch), so you can render them as “Email Opened (automatic)” and separate them from real reads.
AI Presentations also records every delivery outcome SendGrid reports, including delivered, deferred, bounced, dropped, spam report, unsubscribe, and click events. Failures that need your attention emit their own events: email.bounced, email.dropped, email.complained, and email.unsubscribed. Subscribe to them to find out when a presentation email never reached the homeowner or when a homeowner opts out. There is no email.delivered event; email.sent already covers the send itself.
Compare email copy performance
CallGET /api/delivery/template-performance to compare versions of a template’s email copy. AI Presentations records a new template version when you save a template with changed subject, preview text, body, CTA label, or sender name. Each template has one active version, which is the copy it sends now. A version stores the copy before tokens are filled in. For each version, the response returns how many first emails were sent, delivered, and opened, and how many homeowners started the presentation.
- AI Presentations counts each presentation once, under the version used for its first email to a recipient outside Demand IQ and your organization’s own users. Manual sends, including sends to override recipients, count. Opens of a later reminder don’t count toward any version.
- Compare versions on
emailOpened, which counts opens by a person over delivered emails. Automatic opens are excluded. startedcounts homeowners who started the presentation after the first email, over first emails a person opened. A start is the first time the viewer shows slides to a homeowner. Visits by Demand IQ staff, by members of your organization, in rep mode, and by link scanners such as Microsoft Safe Links don’t count. Opened emails sent before AI Presentations began recording starts count asunknown.- A version with fewer than
minimumDelivereddelivered emails hasreportable: false. Treat its rates as directional. - First sends whose subject or preview text fell back to the default are reported in
fallbackSentand left out of the rates, because the homeowner never saw that version’s copy. - Sends where every recipient is a Demand IQ staff (
@demand-iq.com) address are excluded, as are sends where every recipient is one of your organization’s own users, and deleted presentations. A test send to a staff address before the homeowner’s email doesn’t count as the first email. - Delivered and opened counts come only from events for recipients outside Demand IQ. An open by a staff co-recipient doesn’t count.
- First sends made before AI Presentations recorded template versions appear in a final row with
templateVersionId: null. - Each version’s
copyincludes the subject, preview text, body, CTA label, and sender name.fromNameisnullwhen the version used your organization’s name. kindtells you where the copy came from:deckfor copy saved on a template,defaultfor the current Demand IQ default,previous_defaultfor an earlier Demand IQ default, andunknownfor the row with no recorded copy.- When you pass
deckId,current: truemarks the template’s active version. WithoutdeckId,currentis alwaysfalse.
deckId to list every version one template has had, with all-time counts. The active version comes first and is listed from the moment it’s saved, even before its first send. Each version also carries firstSavedAt and firstSentAt, the dates this template first saved and first sent it. since and until are ignored when you pass deckId.
Without deckId, the response covers your whole organization. Pass since and until to bound when the first email was sent, or omit both for the last 30 days.
Manage a template’s versions with these endpoints:
POST /api/delivery/template-performance/{versionId}/activate?deckId=…makes an earlier version active. AI Presentations writes that version’s copy to the template’s saved email copy, and its counts continue. Returns204.DELETE /api/delivery/template-performance/{versionId}?deckId=…removes an earlier version from the template’s list for good. Saving identical copy later creates a new version with zero counts. PassuntrackedasversionIdto remove the row of sends made before versions were recorded. Removing the Demand IQ default copy affects only that template. Removing the active version returns409.
404 when the version isn’t on the template’s list or the template belongs to another organization.
If email template performance isn’t enabled for your organization, these endpoints return 404.
Event types
AI Presentations emits these events:Question and email events
Question and email events
presentationId and relevant context. For example, slide.viewed includes the slide ID and how long the viewer spent on it. AI Presentations records one slide.viewed event per slide visit, including fast flips. There is no separate skip event. A view under 2 seconds carries skipped: true, so you can measure which slides viewers skip and filter skips out of dwell-time metrics. If the viewer hides the tab mid-visit, AI Presentations records the time so far and then updates the same event with the full duration when the viewer leaves the slide. narration.played is a companion to slide.viewed for narrated slides: narrationStarted is true only when play() actually resolved (so autoplay-policy failures do not count as a start), narrationCompleted is true when audio played to the end, and secondsPlayed credits only the remaining clip duration on resumed or retried attempts so the total never exceeds secondsAvailable. secondsAvailable is null when the audio duration was never known (for example, playback failed before metadata loaded). Both email.sent and email.opened include the delivery attempt ID (deliveryId) so you can correlate opens back to a specific send.
narration.played is high-volume — one row per narrated slide exit. If your activity feed doesn’t need it, pass excludeEventTypes=narration.played on GET /api/notifications/events to hide it without allowlisting every other type.
Event payloads carry identifiers, never contact details. No event payload includes a homeowner’s email address, phone number, or name. The one exception is question.asked / question.escalated, whose payloads include the question and answer text for 90 days (see Event retention). When you query the event log, email.sent entries include top-level recipientEmail, skipReason, and variant fields alongside the payload so you can still see who received each send, why a send was skipped, and whether it was a reminder.
The four email failure events (email.bounced, email.dropped, email.complained, email.unsubscribed) carry the presentationId, the delivery attempt ID (deliveryId), and SendGrid’s message and event identifiers (sgMessageId, sgEventId). email.bounced adds a bounceType (bounce or blocked) and SendGrid’s bounce classification; email.dropped adds SendGrid’s drop reason (for example Bounced Address or Unsubscribed Address). These payloads never include the recipient’s email address. Email notifications for these events resolve the affected recipient for you at send time.
question.escalated carries the same payload shape as question.asked plus an escalationReason field: no_answer when the AI deflected to a human callback, or low_confidence when the AI answered but flagged the answer as uncertain. Any given question emits exactly one of question.asked or question.escalated, so a single question never triggers two notifications.
The question.asked payload can also include an optional degradedReason field: no_client, request_failed, empty_response, or invalid_response. It is set when the viewer received a canned answer because of a temporary issue on AI Presentations’s side, not a gap in your content. These answers never emit question.escalated, so no one on your team is asked to follow up. The field is additive and optional, so existing consumers are unaffected.
Deck and slide identity on question events
question.asked and question.escalated carry four optional fields that identify the template and slide the question was asked on: deckId, deckSlideId, slideTitle, and slideIndex (the slide’s 0-based position, the same base as slide.viewed’s slideIndex). The payload’s slideId identifies a slide within a single presentation, so it can’t be compared across presentations. deckId and deckSlideId can, which lets you answer questions like “which slide draws the most questions across this template”.
AI Presentations resolves these fields on the server from the presentation itself, never from the request. Each field is omitted when it can’t be resolved — for example, deckId is absent once the template has been deleted. Questions asked outside a presentation (in the template editor or preview) carry none of these fields.
These fields ship as payload version 2. The webhook envelope’s event_version is 2 for events that carry them and 1 for older entries, where the fields were never recorded. The change is additive: version 1 payloads are unchanged, and existing consumers keep working without modification.
Conversation and modality on question events
question.asked and question.escalated also carry two optional fields describing how the question was asked and which browser tab asked it: modality (either voice or text, depending on whether speech recognition or typing produced the question) and conversationId (a UUID that persists for the lifetime of the viewer’s tab). Together they let you group interleaved questions from concurrent viewers on the same link into distinct conversations, and split the voice-versus-text mix.
Both fields are populated by the viewer’s browser, so each can legitimately be absent:
modalityis absent when the client could not determine the input source.conversationIdis absent when browser storage is unavailable, when the question was asked from an older client that did not send the field, or when the deck is being demoed by a sales rep rather than viewed by a homeowner. When it is absent, group the question bypresentationIdand timestamp instead.
event_version is 3 for events that carry them, 2 for entries that carry only the deck and slide identity fields, and 1 for older entries. The change is additive: version 1 and version 2 payloads are unchanged, and existing consumers keep working without modification. Both fields survive the 90-day archive alongside the other classification fields.
Started presentations
presentation.started fires when a viewer presses Start on a presentation’s intro screen. Presentations whose template has no intro, and visits that skip the intro, never emit it. Presentation previews never emit it either.
The payload carries presentationId and deckId. deckId is null when the template has been deleted. AI Presentations doesn’t send an email notification for this event, and it has no toggle in the template editor’s Settings tab.
Completed presentations
presentation.completed fires when a viewer reaches the last slide of a ready presentation. Reaching the last slide is what counts, so a viewer who skims ahead still completes the presentation. Narration listen-through doesn’t factor in; use narration.played to measure it. The event fires on every visit that reaches the last slide, the same way presentation.opened fires on every open. Rewatches, runs driven by a sales rep, and reloads on the last slide each emit another completion. To calculate a completion rate, count distinct presentationId values rather than events. Presentation previews never emit it.
The payload carries presentationId, deckId, and slideId (the last slide). deckId is null when the template has been deleted. The email subscription for this event is off by default. To receive it, turn on Presentation Completed in the template editor’s Settings tab. When it’s on, you get an email for every completion.
Blocked and failed presentations
presentation.blocked fires 24 hours after a presentation parks at missing_data, once for that presentation. The payload carries presentationId, deckId, a missingFields array of the token field names the journey never supplied (for example, email or roof_squares), and a reason of either missing_lead_data or deck_deleted. The field names identify what the journey failed to collect; no lead values are included. The email subscription for this event is on by default because the cause is a journey configuration your team can fix.
presentation.generation_failed fires once per presentation when generation dies against an upstream provider. The payload carries presentationId, deckId, the phase where the work stopped, and an errorMessage. The email subscription for this event is off by default: the cause is on our side rather than yours, and an upstream outage would otherwise email every company about every affected lead. Any organization that wants to see these can turn the subscription on in the template editor’s Settings tab.
Create a subscription
AI Presentations provisions a default set of email subscriptions for your organization the first time a user signs in, so the standard “someone opened your presentation” and “someone asked your AI a question” emails start working without any setup. When new event types ship, they’re added to existing organizations on the next sign-in and inherit the org’s prevailing posture — if you’ve muted every other email, the new type is muted too. To add or change recipients, open the Settings tab in the template editor. You can also create subscriptions via the API. Each subscription needs:- Name — a label for your reference, e.g. “Slack — new presentation opened”
- Event type — which event to listen for (e.g.
presentation.opened) - Channel — how to deliver the notification
- Channel config — channel-specific settings (see below)
- Template scope (optional) — limit to events from a specific template, or leave blank for all templates
Delivery channels
- Webhook
- Email
- Console
Sends an HTTP POST request to your URL with the event payload as JSON.Channel config:The webhook payload follows this structure:
Verify the sender
Signing is optional and must be configured for your subscription; creating a subscription through the API does not provision a signing secret. Contact Demand IQ to arrange signing and obtain the shared secret. Until signing is configured, validate a secretAuthorization header configured on your subscription over HTTPS. Do not accept an unsigned request as a signed event.Signed requests include X-Webhook-Signature: t=<unix-seconds>,v1=<hex-digest>. Compute HMAC-SHA256 with your shared secret over the timestamp, a period, and the raw request body: timestamp + "." + rawBody. Compare the hexadecimal digest with v1 using a constant-time comparison. Reject missing or malformed signatures and timestamps outside your chosen tolerance (for example, five minutes). Parse the JSON only after verification, and deduplicate by event_id.Retries and recovery
Return a2xx promptly after durably accepting the event. Requests time out after 10 seconds. Responses with 429 or 5xx, timeouts, and recognized connection failures are retryable; other HTTP errors are terminal.The current delivery configuration allows five queue processing attempts, normally about two minutes apart (roughly eight minutes from first to fifth attempt). This is not an exact delivery deadline: processing time, backlog, and worker restarts can extend it, and an attempt can fail before reaching your endpoint.After retryable failures exhaust the processing limit, the event is retained for recovery for up to 14 days. Non-retryable failures stop immediately and do not enter that recovery queue. There is no public replay endpoint. Contact Demand IQ with the event_id to investigate recovery; changing a subscription alone does not replay a terminal failure. The event log lets you reconcile activity independently of webhook delivery.Event versions
event_version identifies the payload schema for that event_type, not the API version or delivery attempt. Version 1 is the initial schema. Later versions are additive unless the change is called out on this page, so accept higher versions you don’t recognize and ignore unknown fields. Review payload changes before relying on new fields. A retry keeps the recorded event version.Event log
AI Presentations keeps a log of all events for your organization. You can query the log to see what happened during a presentation, filter by event type, or look at events within a time range. Foremail.sent events, each entry includes top-level recipientEmail, skipReason, and variant fields. AI Presentations resolves all three from the delivery attempt referenced by the payload’s deliveryId; the stored payload itself never contains the address. skipReason is no_email when the homeowner had no email address, suppressed when a recipient previously bounced, reported spam, or unsubscribed, and null for sends that were not skipped. The activity log shows a suppressed skip as “Email Not Sent · previously bounced or unsubscribed”.
variant is initial for the first presentation email and reminder for the follow-up sent to a homeowner who never opened it. Entries written before reminders existed return initial, or null when no delivery attempt matches. The presentation activity log labels reminder entries by status and skip reason:
A reminder doesn’t change a presentation’s delivery
sentAt. sentAt stays the earliest accepted send across all attempts.
For email.opened events, each entry includes a top-level machineOpen boolean. It is true when every open SendGrid reported for the delivery attempt was automatic (for example, an Apple Mail Privacy Protection prefetch of the tracking pixel) and false when at least one human open has been recorded. Render machine opens as “Email Opened (automatic)” to keep them separate from real reads.
When you scope the query to a single presentation with aggregateId, the response may include a derived presentation.link_opened entry synthesized from the presentation’s first-viewed timestamp. It rides along as an extra row rather than taking a page slot, so it never displaces a stored row and never becomes a nextCursor. Its id is presentation.link_opened:{presentationId} and its payload is { presentationId }. Every presentation that has been opened at least once carries this line, including presentations opened before the derivation shipped — the org-wide feed (/api/activity) still shows stored events only.
The event log supports filtering by:
- Event type — e.g. only
contract.signedevents - Exclude event types — comma-separated list, e.g.
narration.playedto hide a high-volume type from an activity feed - Aggregate type — e.g. only presentation-level events
- Aggregate ID — events for a specific presentation
- Time range —
sinceanduntilparameters - Pagination —
limit(max 100, default 25) plus a keysetcursorfrom the previous response’spage.nextCursor. Poll for new activity withrecent=1for the last 90 seconds on the server clock.offsetis no longer accepted and requests that send it are rejected with a400.
Event retention
Event log entries keep their full payloads for 90 days. After that, AI Presentations archives each entry and reduces its payload to identifier and classification fields — for examplepresentationId, slideIndex, durationMs, the skipped flag, ctaType, and delivery status. The entry itself stays in the log, so activity timelines keep their full history, and skip-rate and dwell-time metrics stay consistent for the life of the presentation.
Archived entries carry an archivedAt timestamp. For question.asked and question.escalated, the question and answer text is removed when the entry is archived; the classification fields (such as answerSource and escalationReason), the deck and slide identity fields (deckId, deckSlideId, slideTitle, slideIndex), and the conversation fields (conversationId, modality) remain, so per-slide question metrics and conversation grouping stay consistent past the 90-day window. Archived entries are deleted from the log entirely after two years.
AI Presentations keeps an internal archive copy of each entry’s original payload for 9 months after archiving, then deletes it. No raw question or answer text remains in any store AI Presentations controls more than 12 months after the event occurred.
Q&A answer retention
When a homeowner asks a question during a presentation, AI Presentations caches the generated answer, along with its spoken audio, so similar questions on the same deck can be answered instantly. Cached answers follow the same retention limits as the event log. AI Presentations deletes a cached answer after 90 days without use, or 12 months after it was created, whichever comes first. The spoken audio is deleted with it. Expiry has no visible effect during presentations: the next matching question generates a fresh answer.Managing subscriptions
You can enable, disable, update, or delete subscriptions at any time. Disabling a subscription (isActive: false) stops deliveries without losing the configuration.
API reference
Key notification endpoints:GET/POST /api/notifications/subscriptions— list or create subscriptionsGET/PUT/DELETE /api/notifications/subscriptions/{subscriptionId}— manage a specific subscriptionGET /api/notifications/events— query the event log with filters and keyset pagination. Page forward withcursorfrom the previous response’spage.nextCursor, or poll withrecent=1for the last 90 seconds on the server clock. The defaultlimitis 25 (cap 100);offsetis no longer accepted and requests that send it are rejected with a400.GET/PUT /api/decks/{deckId}/delivery— manage per-deck email delivery settingsPOST /api/presentations/{presentationId}/send— send or resend the homeowner emailPOST /api/presentations/send— send a follow-up email, such as a reminder, for a presentation created by ingestGET /api/delivery/template-performance— compare sent, delivered, and opened counts per email template versionPOST /api/delivery/template-performance/{versionId}/activate— make an earlier email template version activeDELETE /api/delivery/template-performance/{versionId}— remove an earlier email template version from a template’s list