Send Email in SharePoint with SharePoint REST API and PnPjs : What You Need to Know

SharePoint’s SP.Utilities.Utility.SendEmail lets you send email straight from a list workflow, a custom web part, or a script — without going through Outlook or Power Automate. Two ways to call it:


In this post: Real limitations · Option 1: raw REST API · Option 2: PnPjs · When would you actually use this? · Best practices


Real limitations

Worth knowing before you build around this: it doesn’t support file attachments — the SP.Utilities.EmailProperties object has no property for one. You can inline images hosted on the same SharePoint domain, but a real attachment isn’t possible through this API. It also can’t send to a distribution group/list, only individual mailboxes, and there’s a cap on total content size. If any of those are dealbreakers, that’s your sign to use Power Automate’s send-email action instead, which supports all three.


Option 1: raw REST API
function sendEmail() {
    var siteUrl = _spPageContextInfo.webAbsoluteUrl;

    $.ajax({
        contentType: 'application/json',
        url: siteUrl + "/_api/SP.Utilities.Utility.SendEmail",
        type: "POST",
        data: JSON.stringify({
            'properties': {
                '__metadata': { 'type': 'SP.Utilities.EmailProperties' },
                'To': { 'results': ['user@domain.com'] },
                'Subject': 'Custom Notification',
                'Body': 'Hello! This is a custom email from SharePoint.'
            }
        }),
        headers: {
            "Accept": "application/json;odata=verbose",
            "X-RequestDigest": $("#__REQUESTDIGEST").val()
        },
        success: function () {
            console.log('Email sent successfully!');
        },
        error: function (err) {
            console.error('Error sending email: ', err);
        }
    });
}
Option 2: PnPjs
import { sp } from "@pnp/sp/presets/all";

sp.utility.sendEmail({
    To: ["user@domain.com"],
    Subject: "PnPjs Email",
    Body: "This email was sent using PnPjs!",
}).then(_ => {
    console.log("Email Sent!");
}).catch(console.error);

PnPjs is less boilerplate if you’re already using it elsewhere in the project; the raw REST call is the way to go if you’re not pulling in the library just for this.

One thing worth flagging about the raw REST example above: it uses $.ajax and _spPageContextInfo, which only exist on classic SharePoint pages. Inside a modern SPFx web part, neither is available — use SPHttpClient from the SPFx context instead:

import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';

this.context.spHttpClient.post(
    `${this.context.pageContext.web.absoluteUrl}/_api/SP.Utilities.Utility.SendEmail`,
    SPHttpClient.configurations.v1,
    {
        headers: { 'Content-type': 'application/json;odata=verbose' },
        body: JSON.stringify({
            properties: {
                __metadata: { type: 'SP.Utilities.EmailProperties' },
                To: { results: ['user@domain.com'] },
                CC: { results: ['manager@domain.com'] },
                Subject: 'Custom Notification',
                Body: 'Hello! This is a custom email from SharePoint.'
            }
        })
    }
).then((response: SPHttpClientResponse) => {
    if (!response.ok) { console.error('Send failed', response.statusText); }
});

Same underlying endpoint, but SPHttpClient automatically handles the request digest for you — one less thing to wire up manually inside a web part.


Validate email addresses before sending — this API won’t do it for you.

When would you actually use this?
  • A list item hits a certain status and someone needs a notification — without spinning up a full Power Automate flow for something this small.
  • You’re building a custom SPFx web part and need to send a confirmation email as part of a form submission.
  • You need email sent from client-side code where a full workflow engine would be overkill.

Best practices
  • Validate email addresses before sending — this API won’t do it for you.
  • Sanitize any user input going into the HTML body to avoid injection.
  • Wrap calls in try/catch (or .catch()) — don’t let a failed send disappear silently.
  • Don’t fire these inside a tight loop without batching — you’ll hit throttling.
  • Log success/failure somewhere if this matters for audit purposes.

That’s the fix — let me know in the comments if it doesn’t quite match your setup.

Leave a Comment

Your email address will not be published. Required fields are marked *