SendGrid
Send form submissions as SendGrid emails built from one of your dynamic templates, and add the people who submit to a SendGrid marketing list.
SendGrid is an add-on for Forms. Once your SendGrid account is connected, every form you edit gains a SendGrid panel in its sidebar where you describe one or more actions. When the form is submitted, each action either sends an email built from one of your SendGrid dynamic templates, or adds the person who submitted to one of your SendGrid marketing lists.
Nothing else about the form changes: validation, messages, redirects, the notification emails Forms sends itself and the Submissions list all behave as they did before.
Where to find it
Two places, because there are two kinds of setting. The account connection is site-wide, at XPAC → Integrations on the SendGrid tab. What each form sends is per-form, in the SendGrid panel of the document sidebar while you edit that form. It sits below the panels Forms provides itself, among the other add-ons, and the order within that group is not fixed.
Forms has to be active
The plugin does nothing on its own. Without XPAC Forms it registers no settings tab, no panel and no sending, and shows an admin notice asking for Forms instead.
Connecting your SendGrid account
Give it enough access. Read the next section before you choose — a key that can only send mail is not enough to make the editor work.
Paste it into XPAC → Integrations → SendGrid in the Api Key box and save. That one key serves every form on the site.
What the key has to be allowed to do
The plugin asks SendGrid for four different things every time you open the panel, and does two more when a form is submitted:
| The plugin needs to | So that |
|---|---|
| Read your verified senders | The Sender dropdown can be filled |
| Read your dynamic templates | The Template dropdown can be filled |
| Read your marketing lists | The Select list dropdown can be filled |
| Read your marketing contact fields | The Remote field dropdown can be filled |
| Send mail | A Send Email action can go out |
| Add marketing contacts | An Add Contact action can go out |
A key with full access covers all six. If you would rather restrict the key, it needs every one of them — including the two marketing permissions, even on a site that will only ever send email.
One missing read permission affects every picker
The panel waits for all four account lookups. If one is refused it shows that error, but your saved actions remain visible. Give the key all four read permissions before you try to edit account-backed choices.
So a key that can send mail but cannot read your marketing lists makes the SendGrid panel unusable for email too. If the panel shows an error where the controls should be, the key is the first thing to check.
Missing credentials do not hide your actions
The SendGrid panel and its saved actions remain visible when the key is missing or revoked. A submission still creates its Forms entry before the queued delivery is marked failed. Correct the key and its scopes, then retry that delivery from XPAC → Forms → Deliveries.
Sending an email
Edit the form, open the SendGrid panel and press the round + button (Add action). A settings modal opens with a dropdown in its header.
Choose the Sender. The list is your verified senders in SendGrid; a sender you have not verified there does not appear here. Choosing one also fills in the name the email is sent under.
Choose the Template — one of your SendGrid dynamic templates. The subject line and the whole body of the email come from that template, not from WordPress, so this is where you design the email.
Set To Email. It arrives pre-filled with {{ADMIN_EMAIL}}, which sends the
email to your site's administration address. Replace it with a fixed address,
or with one of the variables described below, to send somewhere else.
Optionally set To Name, Reply-To Name and Reply-To Email.
Press Save in the modal — it stays disabled until Sender, Template and To Email all have values and you have changed something — then update the form. Updating the form is what saves the action.
The controls
| Control | What it sets | Required |
|---|---|---|
| Sender | Which of your verified SendGrid senders the email comes from. | Yes |
| Template | Which SendGrid dynamic template supplies the subject and body. | Yes |
| To Name | A display name for the recipient. | No |
| To Email | Where the email goes. Pre-filled with {{ADMIN_EMAIL}}. | Yes |
| Reply-To Name | A display name for replies. | No |
| Reply-To Email | Where a reply goes, if not to the sender. | No |
To Name and To Email each accept several values separated by commas, which is how one action reaches more than one person. They are paired up in order, so the first name goes with the first address, the second with the second, and so on. A list of three addresses with only two names leaves the third address without one.
Variables
The Available Variables link above the controls opens a list of everything you can paste into those boxes. Clicking one copies it to your clipboard. There are two kinds:
| Variable | What it becomes |
|---|---|
{{ADMIN_EMAIL}} | Your site's administration email address |
{{NOREPLY_EMAIL}} | wordpress@ followed by your site's domain |
{{SITE_TITLE}} | Your site's title |
{{REFERRER}} | The address of the page the form was submitted from |
| One per form field | Whatever the visitor typed in that field |
A field variable is how a confirmation email reaches the person who filled the form in:
put your form's email field variable in To Email instead of {{ADMIN_EMAIL}}.
These variables work in the boxes above and inside your template
Every variable in the list is sent to SendGrid alongside the email, under the name
shown in the table without the braces, so a Dynamic Template can reference it
too: write {{SITE_TITLE}} in the template to get your site's title, or
{{your_field_name}} to get what the visitor typed.
The four system variables used to be sent under a name no template could match, so they rendered blank inside a template while form field placeholders worked. That is fixed. If a spot in your template is still blank, check that the name matches the form field exactly — it is case-sensitive — and that you are editing a Dynamic Template rather than a legacy one.
An unknown variable is left visible
A variable typed by hand and misspelled, or one naming a field you later renamed,
is left as its literal {{token}}. That makes the mistake diagnosable, but it is not
a valid recipient address. Paste variables from Available Variables and re-check
action recipients after renaming a field.
File uploads
A file upload field arrives in the email as a list of links, one per file, under that field's variable. Turn on Attach uploaded files in the action to include readable uploads as real attachments as well. The attachment budget defaults to 5 MiB of original files across one message; files that are missing, unreadable or beyond the remaining budget are omitted rather than turning the whole delivery into an invalid SendGrid request.
Template variables still contain public links
The field variable remains a list of upload URLs whether or not attachments are on. Those links work for anyone who has them for as long as the file exists. On a local, staging or password-protected site an external recipient may be unable to open a link, while an attached file still travels with the message.
Adding someone to a marketing list
Open the SendGrid panel, press +, and change the dropdown in the modal's header from Send Email to Add Contact.
Add one mapping row per piece of information you want to store. Local field is a field in your form; Remote field is a field on the SendGrid contact. Both your reserved contact fields and your own custom fields appear in the second list.
Include a row that puts the visitor's email address into SendGrid's Email field. Nothing works without it — see below.
An Email row is required
SendGrid identifies a contact by email. The editor will not save an Add Contact action until one mapping targets Email. If the saved field later disappears or submits no usable address, the queued delivery records a visible configuration failure instead of silently dropping the action.
This is not an opt-in workflow
Add Contact upserts a marketing contact; it does not ask SendGrid to send a confirmation message and it does not establish that the person consented. Use an explicit form control and Conditional Logic where consent is required, and keep the Forms entry as the record of what was submitted.
Date fields are normalized to calendar dates
Only a custom field that SendGrid identifies as a Date is normalized to its
YYYY-MM-DD calendar format. Text and number custom fields are sent as submitted,
so a short reference such as 1234 is not guessed to be a date. An older saved
action that predates stored field types still recognizes SendGrid's _D date-field
suffix for compatibility.
What appears in the dropdowns
The Template list reads SendGrid's dynamic templates page by page until there are no more. Legacy templates remain excluded, so a template visible in SendGrid but not here is usually a legacy one.
The Sender list holds your verified senders only. The Select list and Remote field lists come straight from your SendGrid marketing account, so a list or a custom field you add in SendGrid appears here after you reload the form editor.
Switching an action off, and removing one
Each row in the panel shows the action's type — Send Email or Add Contact — a pencil (Edit action) that reopens its modal, and a red X (Remove action) that asks Are you sure you want to remove the item? before acting.
Use the switch on a row to pause or resume that action without losing its settings. The X removes the selected row after confirmation; other actions keep their order and settings. With Conditional Logic installed, a still-enabled action can also run only for submissions that match its conditions.
Deliveries, retries and duplicate limits
Forms writes the entry first and schedules SendGrid afterwards. The visitor does not wait, and a remote failure cannot erase the submission. The Deliveries screen retains SendGrid's explanation. Each accepted action is marked done, so a retry of one failed action does not repeat its successful siblings.
Configuration, authentication and validation failures reported as 400/401/403/404/410/422 are left failed for an administrator to fix. Rate limits, 5xx replies and transport failures are retried after about 1, 5, 30 and 120 minutes, for at most five attempts. Restore the key or action configuration, then retry a permanent failure manually.
Marketing contact writes are upserts and converge on contact state. Transactional
mail/send has no provider idempotency key. If SendGrid accepted an email but the site
timed out before seeing the response, a later attempt can send that message again;
Forms can prevent repeats only after it receives an acceptance response.
For developers
The API key is stored in the xpac option under the sendgrid_api_key key. That
option is deliberately not exposed on /wp/v2/settings; it is readable and writable
over the settings framework's own wp-react-settings/xpac/read and
wp-react-settings/xpac/update routes, both gated on manage_options. The field is a
password control in the settings UI; changing its control type does not change the
stored option name or value.
Per-form actions live in the form's form_settings post meta on the xpac-form post
type, under a sendgrid key, registered on the Forms meta schema and therefore
readable and writable over the REST API at /wp-json/wp/v2/xpac-form/<id>. The
default is an empty items array:
{
"sendgrid": {
"items": [
{
"status": true,
"action": "send",
"sender": "[email protected]",
"senderName": "Example",
"template": "d-…",
"receiverName": "",
"receiverEmail": "{{ADMIN_EMAIL}}",
"replyToName": "",
"replyToEmail": ""
},
{
"status": true,
"action": "addContact",
"list": "…",
"map": [{ "local": "email", "remote": "email" }],
"customFields": ["e1_T"]
}
]
}
}action is send or addContact. sender, senderName, template and the four
receiver/reply-to keys are read on the send path; list, map and customFields
on the addContact path. An item created in the editor carries the send keys
whichever type it is, because the modal starts from one shared set of defaults.
customFields holds the subset of map[].remote ids that are custom rather than
reserved fields, maintained by the editor as you change a mapping row; the server uses
it to split the contact payload between contacts[0] and contacts[0].custom_fields.
Items are declared additionalProperties: true, and two keys the runtime depends on
are not declared:
action— read directly on the submit path. An item without it matches neither branch and is dropped.condition— the Conditional Logic plugin's own structure. Absent means unconditional.
status is declared, but nothing in the editor ever writes a value other than the
true it is created with, because the panel does not enable the shared list's toggle.
Forms' shared action-item filter reads it without a fallback, so an item written over
REST without status raises a warning, and on a site with Conditional Logic active it
raises a TypeError inside the submit request.
Four REST routes back the editor UI, all requiring manage_options:
GET /wp-json/xpac/v1/form/sendgrid/senders
GET /wp-json/xpac/v1/form/sendgrid/templates
GET /wp-json/xpac/v1/form/sendgrid/lists
GET /wp-json/xpac/v1/form/sendgrid/fieldsEach route returns a provider message when its lookup fails. All four are requested as soon as the panel mounts, whichever action type you are configuring; once resolved, the actions list stays rendered alongside any connection error.
Sending happens inside Forms' existing public submit route:
POST /wp-json/xpac/v1/form/<form-id>/<instance>/submitClient talks to https://api.sendgrid.com/v3/ with a bearer token. Its timeout
defaults to ten seconds and is filterable through xpac_sendgrid_request_timeout:
GET /verified_senders, GET /templates?page_size=20&generations=dynamic,
GET /marketing/lists and GET /marketing/field_definitions for the editor;
POST /mail/send and PUT /marketing/contacts during queued delivery. Every method
checks transport and status errors and preserves the provider explanation and status.
Extension surface
It fires one filter of its own, and that is the whole surface:
apply_filters( 'xpac_sendgrid_email_field_value', $data_value, $type, $key, $submission );It is reached only from the default branch of the field-type switch that builds the
template variables, so checklist, dropdown, radio, confirmation and file fields never
pass through it — each of those builds its own display value on its own branch.
The plugin registers no blocks. It attaches to eight PHP hooks: plugins_loaded,
admin_notices and rest_api_init from WordPress core for its bootstrap, its
missing-Forms notice and its four routes; then xpac_forms_init to initialise,
xpac_forms_post_meta_schema and xpac_forms_post_meta_default_values for the shape
of its settings, xpac_forms_post_script_dependencies to load its editor panel, and
xpac_forms_submit_callbacks for the delivery callback. Forms' dispatcher schedules
that callback after the entry logger runs; Deliveries\Runner invokes it off the
visitor request and records its classified outcome. Airtable, Brevo, Campaign Monitor,
GetResponse, Klaviyo, Mailchimp, TouchBasePro, Zoho and Forms Submission to Post use
the same queued contract.
On the editor side the panel is contributed from JavaScript through the
xpac-forms-form-panels filter, under panel id and settings key sendgrid. It
declares no priority, which is why it sorts below every core Forms panel and why its
position relative to other add-ons' panels is not stable. Its action modal can be
extended through the xpac-forms-after-settings-modal-content filter, which receives
the action's current options with an onChange callback and is passed
xpac-sendgrid as the module it is filtering — that is how Conditional Logic adds its
condition builder to the modal.