Microsoft Teams  

Send Outlook Calendar Invites with Azure Functions

An HTTP-triggered Azure Function that accepts event details (subject, start/end time, attendees, etc.) and creates a calendar event in a chosen mailbox (organizer). When you create an event with attendees, Exchange automatically sends the invitations. You can also flag the event as a Teams online meeting.

  • Authentication: App-only (client credentials) with Microsoft Entra ID

  • API: Microsoft Graph – Calendar (Events)

  • Runtime: Azure Functions .NET 8 isolated

Prerequisites

Before starting, make sure you have:

  1. Azure subscription + Function App permissions

  2. Microsoft 365 tenant with Exchange Online (the organizer mailbox must exist)

  3. .NET 8 SDK and Azure Functions Core Tools (for local dev)

Step 1: Register an App in Entra ID (Azure AD)

This app will allow Azure Function to talk to Outlook on your behalf.

  1. Go to Entra ID portal.

  2. Navigate to App registrations → New registration.

  3. Give it a name like CalendarFunctionApp.

  4. Select Accounts in this organizational directory only.

  5. Click Register.

Now you’ll see:

  • Application (client) ID

  • Directory (tenant) ID

  1. Next, go to Certificates & secrets → New client secret.

    • Copy the generated secret value (you won’t see it again).

  2. Go to API Permissions → Add a permission → Microsoft Graph → Application permissions.

    • Search for Calendars.ReadWrite and add it.

    • Click Grant admin consent.

Now your app has permission to create events in Outlook.

Step 2: Create Azure Function

  1. Open Visual Studio / VS Code.

  2. Create a new Azure Functions project (C#, .NET).

  3. Choose HTTP Trigger (so you can call it from anywhere).

  4. Name it CreateCalendarEvent.

Step 3: Install Required Libraries

In your project, install these NuGet packages:

dotnet add package Microsoft.Graph
dotnet add package Azure.Identity

Step 4: Add Settings

In local.settings.json, add your values:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "TENANT_ID": "your-tenant-id",
    "CLIENT_ID": "your-client-id",
    "CLIENT_SECRET": "your-client-secret"
  }
}

Step 5: Write the Function Code

Models/EventRequest.cs

public sealed class EventRequest
{
    public string Subject { get; set; } = "";
    public string BodyHtml { get; set; } = "";
    public string TimeZone { get; set; } = "India Standard Time"; // Windows time zone
    public string Start { get; set; } = ""; // ISO 8601 local, e.g., "2025-09-05T15:30:00"
    public string End { get; set; } = "";   // ISO 8601 local
    public string? Location { get; set; }
    public string[] Attendees { get; set; } = Array.Empty<string>();
    public bool IsOnlineMeeting { get; set; } = true; // set false if you don't want Teams link
}

GraphClientFactory.cs

using Azure.Identity;
using Microsoft.Graph;

public static class GraphClientFactory
{
    public static GraphServiceClient Create()
    {
        var tenantId = Environment.GetEnvironmentVariable("TENANT_ID");
        var clientId = Environment.GetEnvironmentVariable("CLIENT_ID");
        var clientSecret = Environment.GetEnvironmentVariable("CLIENT_SECRET");

        var credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
        // App-only requires the /.default scope (consent is pre-configured on the app registration)
        return new GraphServiceClient(credential, new[] { "https://graph.microsoft.com/.default" });
    }
}

CreateInvite.cs

using System.Net;
using System.Text.Json;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Graph;
using Microsoft.Graph.Models;

public class CreateInvite
{
    private readonly ILogger _logger;

    public CreateInvite(ILoggerFactory loggerFactory) => _logger = loggerFactory.CreateLogger<CreateInvite>();

    [Function("CreateInvite")]
    public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
    {
        var body = await new StreamReader(req.Body).ReadToEndAsync();
        var request = JsonSerializer.Deserialize<EventRequest>(body, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

        if (request == null || string.IsNullOrWhiteSpace(request.Subject) || string.IsNullOrWhiteSpace(request.Start) || string.IsNullOrWhiteSpace(request.End))
        {
            var bad = req.CreateResponse(HttpStatusCode.BadRequest);
            await bad.WriteStringAsync("Invalid payload. Required: subject, start, end.");
            return bad;
        }

        var organizerUpn = Environment.GetEnvironmentVariable("ORGANIZER_UPN")!;
        var graph = GraphClientFactory.Create();

        var ev = new Event
        {
            Subject = request.Subject,
            Body = new ItemBody { ContentType = BodyType.Html, Content = request.BodyHtml ?? "" },
            Start = new DateTimeTimeZone { DateTime = request.Start, TimeZone = request.TimeZone ?? "India Standard Time" },
            End   = new DateTimeTimeZone { DateTime = request.End,   TimeZone = request.TimeZone ?? "India Standard Time" },
            Location = string.IsNullOrWhiteSpace(request.Location) ? null : new Location { DisplayName = request.Location },
            Attendees = request.Attendees?.Select(a => new Attendee
            {
                Type = AttendeeType.Required,
                EmailAddress = new EmailAddress { Address = a }
            }).ToList(),
            IsOnlineMeeting = request.IsOnlineMeeting,
            OnlineMeetingProvider = request.IsOnlineMeeting == true ? OnlineMeetingProviderType.TeamsForBusiness : (OnlineMeetingProviderType?)null
        };

        // This call creates the event in the organizer's calendar.
        // Because attendees are included, Exchange will send the invites automatically.
        // POST /users/{organizer}/events
        var created = await graph.Users[organizerUpn].Events.PostAsync(ev);

        var res = req.CreateResponse(HttpStatusCode.Created);
        await res.WriteStringAsync(JsonSerializer.Serialize(new
        {
            created?.Id,
            created?.Subject,
            created?.Organizer,
            created?.Start,
            created?.End,
            created?.IsOnlineMeeting,
            created?.OnlineMeeting,
            Message = "Event created; invites sent to attendees."
        }));
        return res;
    }
}

Program.cs (standard isolated host)

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureLogging(lb => lb.AddFilter("Microsoft", LogLevel.Warning))
    .Build();

host.Run();

Step 6: Test locally

Sample request body (IST timezone):

{
  "subject": "Customer Demo (Azure Functions)",
  "bodyHtml": "<p>Agenda: updates + demo.</p>",
  "timeZone": "India Standard Time",
  "start": "2025-09-05T15:30:00",
  "end": "2025-09-05T16:00:00",
  "location": "Teams",
  "attendees": ["[email protected]", "[email protected]"],
  "isOnlineMeeting": true
}

Output

  • The event is created in organizer@… calendar.

  • Because attendees are present, invitations are emailed automatically.

  • If isOnlineMeeting is true, a Teams meeting link is added.

Step 7: Deploy to Azure

  • Create a Function App (Windows/Linux, .NET 8 isolated).

  • In the Function App → Settings→ Environment variables, add the same keys as local:

    • TENANT_ID, CLIENT_ID, CLIENT_SECRET, ORGANIZER_UPN

  • Deploy from VS Code/CLI:

func azure functionapp publish <your-func-app-name>
  • Get the function URL (with code) and test with the same JSON payload.

Conclusion

This approach saves time, reduces manual effort, and ensures meetings are scheduled consistently and reliably. Whether you want to automate daily standups, customer calls, or internal reminders, this serverless pattern is flexible and easy to extend.

With Azure Functions, you can now integrate calendar scheduling into your applications or business processes—making them smarter, faster, and fully automated.