"Service accounts cannot invite attendees without Domain-Wide Delegation of Authority"
Updated
TL;DR
Google Calendar returns 403 forbiddenForServiceAccounts when a service account creates an event with attendees while acting as itself. Domain-wide delegation fixes it: a Workspace super administrator grants your service account scopes, and your code impersonates a user in that domain by putting their email in the token's sub claim. It only reaches users of a domain whose administrator granted it, so for personal Gmail accounts or customers' calendars, use per-user OAuth instead.
The error
Creating an event with an attendees list while authenticated as a service account fails with a 403. Developers report this body:
{
"error": {
"errors": [
{
"domain": "calendar",
"reason": "forbiddenForServiceAccounts",
"message": "Service accounts cannot invite attendees without Domain-Wide Delegation of Authority."
}
],
"code": 403,
"message": "Service accounts cannot invite attendees without Domain-Wide Delegation of Authority."
}
}This reason is not in Google's Calendar API error reference, so the body above comes from developer reports, such as this Google issue tracker thread. The same event without attendees succeeds.
Why a service account can't invite people
A service account is not a person. Google describes it as what an application uses "to work with its own data rather than a user's data", in its service account guide. Reaching users' data takes delegation: to access data "for users in your Google Workspace account, then delegate domain-wide access".
An invitation is sent from someone. When an event has attendees, Google emails them on behalf of the organizer, and the event appears on their calendars as an invitation from that organizer. Google won't let a service account be that organizer while acting as itself. The error message names the one way around it: domain-wide delegation, so that the service account acts as a real user, and the invitation comes from that user.
Google hasn't published more of its reasoning than the error message.
What domain-wide delegation is
Domain-wide delegation lets a service account act as users in a Google Workspace domain. Google's guide: once it is set up, "Your application now has the authority to make API calls as users in your Workspace domain."
Who grants it
Only a super administrator of the Workspace domain. Google's admin help says "You must be signed in as a super administrator for this task." In the Admin console, the setting is under Security, Access and data control, API controls, Manage Domain Wide Delegation. The administrator enters the service account's client ID and the OAuth scopes it may use, which Google says "should be appropriately narrow". For calendar invitations, that is usually https://www.googleapis.com/auth/calendar.events.
Google warns the administrator about what they are granting: "the app has access to the data belonging to all of your users".
How your code uses it
Your code names the user to impersonate. In the token request, you "include the email address of the user in the JWT claim set as the value of the sub field". Client libraries do this for you. Google's Python example, with a calendar scope:
from google.oauth2 import service_account
SCOPES = ['https://www.googleapis.com/auth/calendar.events']
SERVICE_ACCOUNT_FILE = '/path/to/ServiceAccountKey.json'
credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE, scopes=SCOPES)
delegated_credentials = credentials.with_subject('user@example.org')Requests made with delegated_credentials run as user@example.org. Events land on that user's calendar, and invitations go out from them.
Delegation by itself is not enough. Developers who granted it in the Admin console but built credentials without a subject still get forbiddenForServiceAccounts, because the service account is still acting as itself. If the grant is missing, Google says "the response to an access token request that includes the sub field will be an error".
What it can't do
The impersonated user's permissions still apply. Google: "It cannot access data that the impersonated user themselves cannot access." Access is limited by "the permissions of the impersonated user and the OAuth scopes you authorize in the Admin console".
Delegated requests also count against Google's per-user quota for the impersonated user. When one service account acts for many users, Google's quota page suggests the quotaUser URL parameter, or the x-goog-quota-user header, to say which user a request is for. See Calendar API quotas.
Why it only works inside one Workspace domain
Every part of delegation is tied to a domain. The grant is made by that domain's super administrator, it covers "users in your Workspace domain", and the sub must be one of those users.
That has three consequences:
- Personal Gmail accounts can't be impersonated. They have no Workspace administrator to grant anything.
- Your own domain works. An internal tool that books meetings for your own staff is the case delegation fits.
- Customers' domains need their own grant. The Admin console accepts any service account's client ID, so a customer's super administrator could grant yours. They would be giving your app access to calendar data for every user in their organization, which many administrators will refuse, and it has to be repeated for each customer.
So for a product whose users bring their own calendars, delegation is rarely the answer.
The alternatives
Per-user OAuth
Each user connects their own Google account through OAuth, and your app creates events with their token. Invitations go out from the user, attendees work, and it doesn't matter whether the account is Gmail or Workspace.
It comes with the work service accounts avoid. Calendar scopes are sensitive, so the app needs Google's OAuth verification before it can serve more than 100 users without the unverified app warning. You also store refresh tokens per user and handle the ones that stop working. See why refresh tokens expire.
Share a calendar with the service account, without attendees
A user can share a calendar with the service account's email address, with permission to make changes. The service account can then create events on that calendar without delegation, as long as the events have no attendees.
Nobody is invited. Anyone who can see the shared calendar sees the events, but no one gets an invitation email or an RSVP. That works for a team calendar of bookings or shifts, and not for meetings with outside guests.
How Pyas handles it
Pyas uses per-user OAuth. Each user connects their own Google or Outlook calendar, and events are created on their calendar with pyas.events.create.
With Pyas Auth, users approve Pyas on Google's own consent screen. Google has already verified Pyas, so you don't go through Google's OAuth review, and Pyas stores and refreshes the tokens. That holds on every plan, the free one included. When a connection stops working, the connection.broken webhook fires and GET /v2/accounts/{accountId}/status says why and how to fix it.
For Outlook work accounts, Microsoft's default consent policy can still require an administrator to approve calendar access, with Pyas Auth too. See Outlook admin approval.
Common questions
I set up domain-wide delegation and still get the error. Why?
Most often the credentials have no subject, so the service account still acts as itself. Set the subject to a user's email in that domain. Check also that the scopes granted in the Admin console include the scope your code requests.
Can I use domain-wide delegation with a personal Gmail account?
No. Delegation is granted by a Workspace super administrator and covers users of that Workspace domain. Personal Gmail accounts have neither.
Do service accounts have Google's 100 refresh token limit?
No. Google exempts service accounts from it. They sign their own token requests instead of using refresh tokens.