provisionesta/google-api-client

Google API Client for Laravel

4.1 2024-03-22 12:37 UTC

This package is auto-updated.

Last update: 2024-04-22 12:56:00 UTC


README

[[TOC]]

Overview

The Google API Client is an open source Composer package for use in Laravel applications for connecting to Google for provisioning and deprovisioning of resources, particularly in Google Workspace (Admin SDK) and Google Cloud.

This is maintained by the open source community and is not maintained by any company. Please use at your own risk and create merge requests for any bugs that you encounter.

Problem Statement

The official Google API client for PHP is challenging to use and does not use common conventions or namespacing used by other Composer packages in the Laravel ecosystem. This is primarily since it was created using another language and the PHP library is automatically generated rather than human generated. Due to the vastness of the Google products and services and different APIs, this results in a clunky experience and the documentation is difficult to find and API response results are difficult to parse. The authors of this package have spent hundreds of hours trying to consume less than 10 endpoints and gave up and decided to build something to scratch our own itch and help other Laravel Artisans.

Instead of providing an SDK method for every endpoint in the API documentation, we have taken a simpler approach by providing a universal ApiClient that can perform GET, POST, PUT, and DELETE requests to any endpoint that you find in the Google API documentation.

This builds upon the simplicity of the Laravel HTTP Client that is powered by the Guzzle HTTP client to provide "last lines of code parsing" for Google API responses to improve the developer experience.

The value of this API Client is that it handles the API request logging, response pagination, and 4xx/5xx exception handling for you. Rate limit errors will be thrown, however rate limit backoff is not available due to non-standardized variations across various Google API services.

Example Usage

use Provisionesta\Google\ApiClient;

// Create a group
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/insert
$group = ApiClient::post(
    url: "https://admin.googleapis.com/admin/directory/v1/groups",
    scope: "https://www.googleapis.com/auth/admin.directory.group",
    form_data: [
        "name" => "Hack the Planet Engineers",
        "email" => "elite-engineers@example.com"
    ],
);

// {
//     +"data": {
//       +"id": "0a1b2c3d4e5f6g7",
//       +"email": "elite-engineers@example.com",
//       +"name": "Hack the Planet Engineers",
//       +"description": "",
//       +"adminCreated": true,
//     },
//     +"headers": [
//        ...
//     ],
//     +"status": {
//       +"code": 200,
//       +"ok": true,
//       +"successful": true,
//       +"failed": false,
//       +"serverError": false,
//       +"clientError": false,
//     },
//   }

// Get a list of records
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/list
$groups = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups',
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    query_data: [],
    query_keys: ['customer']
);

foreach($groups->data as $group) {
    dd($group);
}

// {
//     +"kind": "admin#directory#group",
//     +"id": "0a1b2c3d4e5f6g7",
//     +"email": "elite-engineers@example.com",
//     +"name": "Hack the Planet Engineers",
//     +"directMembersCount": "0",
//     +"description": "",
//     +"adminCreated": true,
//     +"nonEditableAliases": [
//         "elite-engineers@example.com.test-google-a.com",
//     ],
// },

// Get a specific record
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/get
// $group_id = '0a1b2c3d4e5f6g7';
$group_id = 'elite-engineers@example.com';
$existing_group = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
);

dd($existing_group);

// {
//     +"data": {
//         +"id": "0a1b2c3d4e5f6g7",
//         +"email": "elite-engineers@example.com",
//         +"name": "Hack the Planet Engineers",
//         +"directMembersCount": "0",
//         +"description": "",
//         +"adminCreated": true,
//         +"nonEditableAliases": [
//             "elite-engineers@example.com.test-google-a.com",
//         ],
//     },
//     +"headers": [
//         ...
//     ],
//     +"status": {
//         +"code": 200,
//         +"ok": true,
//         +"successful": true,
//         +"failed": false,
//         +"serverError": false,
//         +"clientError": false,
//     },
// }

$group_name = $group->data->name;
// Hack the Planet Engineers

// Update a group (patch)
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/patch
// $group_id = '0a1b2c3d4e5f6g7';
$group_id = 'elite-engineers@example.com';
$response = ApiClient::patch(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    form_data: [
        'description' => 'This group contains engineers that have liberated the garbage files.'
    ],
);

// Delete a group
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/delete
// $group_id = '0a1b2c3d4e5f6g7';
$group_id = 'elite-engineers@example.com';
$response = ApiClient::delete(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
);

Issue Tracking and Bug Reports

We do not maintain a roadmap of feature requests, however we invite you to contribute and we will gladly review your merge requests.

Please create an issue for bug reports.

Contributing

Please see CONTRIBUTING.md to learn more about how to contribute.

Maintainers

NameGitLab HandleEmail
Jeff Martin@jeffersonmartinprovisionesta [at] jeffersonmartin [dot] com

Contributor Credit

Installation

Requirements

RequirementVersion
PHP^8.0
Laravel^8.0, ^9.0, ^10.0, ^11.0

Upgrade Guide

See the changelog for release notes.

Still using glamstack/*? This package is a replacement for glamstack/google-auth-sdk, glamstack/google-workspace-sdk, and glamstack/google-cloud-sdk, however has been fully refactored and requires re-implementation for all previous usage.

Add Composer Package

composer require provisionesta/google-api-client:^4.0

If you are contributing to this package, see CONTRIBUTING.md for instructions on configuring a local composer package with symlinks.

Publish the configuration file

This is optional. The configuration file specifies which .env variable names that that the API connection is stored in. You only need to publish the configuration file if you want to rename the GOOGLE_API_* .env variable names.

php artisan vendor:publish --tag=google-api-client

Environment Variables

Add the following variables to your .env file. You can add these anywhere in the file on a new line, or add to the bottom of the file (your choice).

Any commented out variables are optional and show the default value if not set.

# GOOGLE_API_CUSTOMER="my_customer"
# GOOGLE_API_DOMAIN=""
# GOOGLE_API_EXCEPTIONS=true
# GOOGLE_API_KEY_PATH=""
GOOGLE_API_SUBJECT_EMAIL=""

GOOGLE_API_CUSTOMER

The customer number of the Google Account that the API's will be run on. This will need to match the customer number that the Service Account is associated with. Google provides an alias my_customer that uses the customer ID of the service account by default.

GOOGLE_API_DOMAIN

The domain in the Google Workspace organization that you want to filter results to. This should be set to your primary email domain name. When working with Workspace Groups and Users, the domain can be added to the ApiClient request query_params array to filter results to only this domain name. You can also leave this blank for testing.

GOOGLE_API_EXCEPTIONS

Whether to throw exceptions when a 4xx or 5xx error is received. If false, you can catch and handle exceptions based on the status array returned in each response.

GOOGLE_API_KEY_PATH

You can specify the full operating system path to the JSON key file. For security reasons, this should be saved outside the Laravel directory unless you have configured proper file permissions in storage/keys or similar.

You should not be storing and using JSON key files unless you cannot use the gcloud CLI or attached service account approaches. If your architecture supports it, you should store your variables in CI/CD variables or a secrets vault (ex. Ansible Vault, AWS Parameter Store, GCP Secrets Manager, HashiCorp Vault, etc.) instead of locally on the server.

GOOGLE_API_SUBJECT_EMAIL

The email of the address to run the Google Workspace API as. This is not related to permissions or granting access, it just needs to be a valid user email that has permissions for the same action in the Admin UI.

API Credentials

API authentication with Google Cloud, Google Workspace and other Google APIs is complex and can be confusing. These instructions are designed to get you started as quickly and securely as possible. See the Google API authentication documentation to learn more.

This package uses the GOOGLE_APPLICATION_CREDENTIALS environment variable, a JSON key file, or a JSON string passed into a connection array with a database value at runtime, and will generate bearer tokens using OAuth2 JWT automatically that is used for calling an API endpoint.

Known Limitation: Due to architectural and technical discovery challenges, you might be able to use GCP instance-level attached service accounts, however we have not been able to sufficiently test this.

In most cases, this will either be a GCP Project Service account or Application Default Credentials.

If you have the GOOGLE_APPLICATION_CREDENTIALS environment variable specified, leave the GOOGLE_API_KEY_FILE variable commented out. If you have downloaded the JSON key from GCP, see instructions for using the GOOGLE_API_KEY_PATH variable.

If you have your connection secrets stored in your database or secrets manager, you can override the config/google-api-client.php configuration or provide a connection array on each request. See connection arrays to learn more.

API Key Precedence

Each API request checks for the existence of the key in the following order and will use the first value that it finds.

  1. The key_string key exists in an array passed to the connection parameter for a GET, POST, PATCH, PUT, or DELETE request.
  2. The key_path key exists in an array passed to the connection parameter of a request.
  3. The key_path key is set in the .env file or as a GOOGLE_API_KEY_PATH environment variable.
  4. The GOOGLE_APPLICATION_CREDENTIALS environment variable is set either on you or your configuration-as-code on your server, or using the gcloud auth application-default login command.
  5. (Untested) Metadata server credentials for GCP instance, cluster, container, CloudRun, etc.

If no valid JSON key can be found, an log message will be created and Provisionesta\Google\Exceptions\ConfigurationException will be thrown with the google.api.validate.error.empty event type.

Security Best Practices

No Shared Tokens

Do not use an API service account or key that you have already created for another application or script. You should generate a new service account and key for each use case.

This is helpful during security incidents when a key needs to be revoked on a compromised system and you do not want other systems that use the same user or service account to be affected since they use a different key that wasn't revoked.

API Token Storage

Best Practice: You should not be storing and using JSON key files unless you cannot use the gcloud CLI or attached service account approaches. If your architecture supports it, you should store your variables in CI/CD variables or a secrets vault (ex. Ansible Vault, AWS Parameter Store, GCP Secrets Manager, HashiCorp Vault, etc.) instead of locally on the server.

Do not add your API key to any config/*.php files to avoid committing to your repository (secret leak).

All JSON API keys should be usually be saved as a .json file in a secure location in the filesystem and referenced by the GOOGLE_API_KEY_PATH .env variable.

You can also use the GOOGLE_APPLICATION_CREDENTIALS environment variable which is used if no values are set in the .env file.

See the Google documentation for additional best practices:

Create a JSON Key

There are several methods to statically or dynamically use JSON key credentials. This is similar in concept to using AWS IAM Access Keys and Secrets (a JSON credential file) or IAM Role Assumption.

Follow the steps that are most applicable to your environment.

There are additional advanced use cases with secrets managers that are not covered here. Regardless of method, the API Client simply needs either the GOOGLE_APPLICATION_CREDENTIALS environment variable or GOOGLE_API_KEY_PATH environment variable or .env variable value to be set.

Local Development Environment with gcloud CLI

This is recommended method for getting started. You may need to reauthenticate from time to time, especially if you use gcloud actively to manage multiple GCP projects or resources. Simply use the gcloud auth login and/or gcloud auth application-default login commands needed.

  1. (Prerequisite) Install and initialize the gcloud SDK on your computer.
  2. Run gcloud auth login to authenticate with your Google Cloud organization.
  3. Run gcloud auth application-default login to automatically configure the GOOGLE_APPLICATION_CREDENTIALS environment variable on your machine.
  4. You can see the key that was configured in ~/.config/gcloud/application_default_credentials.json. Do not copy this to your Laravel repository or another location. > The format of this key looks slightly different than a JSON key that you would download, however Google can parse it anyway during the bearer token generation behind the scenes.
  5. You're all set! You do not need to touch or move any JSON keys or update the .env variables. All API calls use your email address's assigned roles and permissions. > Keep in mind that the API client automatically uses the GOOGLE_APPLICATION_CREDENTIALS environment variable if no GOOGLE_API_KEY_PATH or GOOGLE_API_KEY_STRING value is set.

See the Google documentation if you need additional assistance.

GCP Infrastructure with Attached Service Account IAM Authentication

Known Limitation: Due to architectural and technical discovery challenges, you might be able to use GCP instance-level attached service accounts, however we have not been able to sufficiently test this. Please fall back to GCP Project Service Account Key and setting the GOOGLE_APPLICATION_CREDENTIALS environment variable if needed.

If your application is running on Google Cloud infrastructure or managed service, you can use IAM authentication at the machine/resource level instead of the application level. Many Google Cloud services (ex. Compute Engine virtual machines, Google Kubernetes Engine clusters, Cloud Run deployments, AppEngine, etc.) let you attach a service account that can be used to provide credentials for accessing Google Cloud APIs. If ADC does not find credentials it can use in either the GOOGLE_APPLICATION_CREDENTIALS environment variable or the well-known location for Google Account credentials, it uses the metadata server to get credentials for the service where the code is running.

Using the credentials from the attached service account is the preferred method if your Laravel application is running on Google Cloud.

See the Google documentation and best practices to learn more.

GCP Project Service Account Key

This is the manual method and is not recommended unless you cannot use the gcloud CLI or attached service account approaches.

Create a Key
  1. (Prerequisite) Create a GCP project or choose an existing project that you can create a service account user in.
  2. Navigate to https://console.cloud.google.com/iam-admin/serviceaccounts.
  3. Choose your project from the dropdown menu in the top left corner.
  4. Click the Create Service Account button at the top of the page.
  5. Use the following recommended values or choose your own.
    • Service Account Name: Laravel Google API Client
    • Service Account ID: laravel-app
    • Description: (blank)
    • Project Roles: (see service account project roles instructions)
    • Grant Users: (blank)
  6. Click the Done button.
  7. Click on the linked name of your new service account in the table. If you have a long list of service accounts in this GCP project, you can use the search bar (ex. laravel-app).
  8. Click the Keys tab at the top of the page.
  9. Click the Add Key > Create a new key and choose JSON type.
  10. The key will be automatically downloaded.
Storing your Key on Mac

Best Practice: If possible, you should set the GOOGLE_APPLICATION_CREDENTIALS environment variable with the JSON key contents instead of saving the JSON key file to the local disk.

All of the steps below are recommended getting started instructions. You can choose to store your JSON key wherever you like as long as it is in a secure location with appropriate permissions (ex. 0600) and not accidentally committed it into your code repository.

  1. Open your Terminal or iTerm.
  2. Run mkdir -p ~/.config/gcloud/service-accounts make a directory for service accounts.
  3. Run cd ~/.config/gcloud/service-accounts to change to the new directory.
  4. Run cp ~/Downloads/{project-name}-###########.json . to move your service account to the new (current) directory. > You can use tab complete (or optionally rename the file).
  5. Run realpath {project-name}-###########.json to get the full path to this file.
Updating your Laravel Environment Variable
  1. Use your IDE (ex. VS Code) to open your Laravel application repository.
  2. Open and edit the .env file.
  3. Uncomment GOOGLE_API_KEY_PATH and set the value to the file path from your Terminal.
GOOGLE_API_KEY_PATH="/Users/dmurphy/.config/gcloud/service-accounts/{project-name}-###########.json"
Grant Permissions to Key

You will need to add permissions for Google Workspace and/or add permissions for Google Cloud depending on the API endpoints that you will be using.

Service Account Project Roles

  • If you will be performing API calls that manage resources in this GCP project, set this to Basic > Editor as a starting point (or one or more granular GCP roles).
  • Leave this blank and continue without assigning a role if this will be used for GCP organization-level management, folder-level management of child projects, or a different GCP project's resources.
  • Leave this blank and continue without assigning a role if this will be used for Google Workspace management.

Add Permissions for Google Workspace

To use Google Workspace (Admin API) endpoints for Groups and Users, you will need to add the Oauth Client ID of the service account to Domain-wide delegation with the respective scopes for the endpoints that you want to call. There is alternative approach with adding a service account to an admin role, however it is not as intuitive and does not always work as expected.

See the Google documentation for domain wide delegation step by step instructions. See API Scopes for more details on which scopes to grant to the service account.

See Enabling APIs in Google Cloud for the steps to enable the Admin SDK API and any other APIs that you may need.

Add Permissions for Google Cloud

For Google Cloud, you will need to add the gcloud user email address (ex. dmurphy@example.com) or service account email address (ex. laravel-app@{project}.iam.gserviceaccount.com) as an IAM principal at the organization-level, folder-level or project-level (or multiple as needed) with either a custom role or a pre-defined role when managing resources in any given organization, folder, or project respectively.

See the Google documentation to learn more about granting, changing, and revoking access to GCP organization, folders, and projects.

Enabling APIs in Google Cloud

If you are using a service account (not using gcloud auth), you need to enable the respective service API in the GCP project where the service account is created in (ex. @{project}.iam.gserviceaccount.com). This is needed for Google Workspace service account too.

  1. Navigate to https://console.cloud.google.com/apis/library.
  2. Search for the API name (common examples below).
    • Admin SDK API (for Google Workspace Groups and Users)
    • Google Drive API
    • Google Sheets API
    • Cloud Resource Manager API
    • Identity and Access Management (IAM) API
  3. Click the Enable button. This can take a few moments to complete. > If the Manage button is visible, the API is already enabled and no action is required.

API Token Permissions and Scopes

Google uses OAUTH scopes for permission management within Google Workspace.

You need to specify one of the scopes for that specific endpoint (that your API key has been authorized to use). You will find a list of scopes on the REST API documentation for the specific endpoint that you are calling.

Here are some common ones that you will likely use to get you started:

  • https://www.googleapis.com/auth/admin.directory.group
  • https://www.googleapis.com/auth/admin.directory.user
  • https://www.googleapis.com/auth/cloud-identity
  • https://www.googleapis.com/auth/cloud-platform

You can see a full list of scopes in the Google documentation.

Connection Arrays

The variables that you define in your .env file are used by default unless you set the connection argument with an array containing the required arguments.

This approach is recommended if your Google service account JSON key is stored in your database (ex. multiple-tenant architecture or isolated service accounts with granular permissions).

The key_string parameter can be used for passing the JSON array as a string.

Security Warning: Do not commit a hard coded API token into your code base. This should only be used when using dynamic variables that are stored in your database or secrets manager.

use App\Models\GoogleServiceAccount;
use Provisionesta\Google\ApiClient;

class MyClass
{
    private array $connection;

    public function __construct($service_account_id)
    {
        $service_account = GoogleServiceAccount::findOrFail($service_account_id);

        $this->connection = [
            'customer' => 'my_customer',
            'exceptions' => true,
            'key_string' => $service_account->json_key,
            'subject_email' => $service_account->subject_email
        ];
    }

    public function listGroups($group_key)
    {
        return ApiClient::get(
            url: 'https://admin.googleapis.com/admin/directory/v1/groups',
            scope: 'https://www.googleapis.com/auth/admin.directory.group',
            query_keys: ['customer'],
            connection: $this->connection,
        )->data;
    }
}

API Requests

You can make an API request to any of the resource endpoints in the Google API documentation.

Just getting started? Explore the Google Workspace groups, Google Workspace users, Cloud Identity Groups, and Google Cloud Compute Engine endpoints.

Dependency Injection

If you include the fully-qualified namespace at the top of of each class, you can use the class name inside the method where you are making an API call.

use Provisionesta\Google\ApiClient;

class MyClass
{
    public function getGroup($group_id)
    {
        return ApiClient::get(
            url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
            scope: 'https://www.googleapis.com/auth/admin.directory.group'
        )->data;
    }
}

If you do not use dependency injection, you need to provide the fully qualified namespace when using the class.

class MyClass
{
    public function getGroup($group_id)
    {
        return \Provisionesta\Google\ApiClient::get(
            url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
            scope: 'https://www.googleapis.com/auth/admin.directory.group'
        )->data;
    }
}

Class Instantiation

We transitioned to using static methods in v4.0 and you do not need to instantiate the ApiClient class.

use Provisionesta\Google\ApiClient;

ApiClient::get(...);
ApiClient::post(...);
ApiClient::patch(...);
ApiClient::put(...);
ApiClient::delete(...);

Named vs Positional Arguments

You can use named arguments/parameters (introduced in PHP 8) or positional function arguments/parameters.

It is recommended is to use named arguments.

Learn more in the PHP documentation for function arguments, named parameters, and this helpful blog article.

use Provisionesta\Google\ApiClient;

// Named Arguments
ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups',
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    query_keys: ['customer'],
)->data;

// Positional Arguments
ApiClient::get(
    'https://admin.googleapis.com/admin/directory/v1/groups',
    'https://www.googleapis.com/auth/admin.directory.group',
    [],
    ['customer'],
    []
)->data;

GET Requests

Since Google's APIs are spread across multiple domains and subdomains, you need to provide the full URL of the API endpoint in the url named argument/parameter.

See API Token Permissions and Scopes to learn more about the finding the scope for each endpoint.

GET Method Parameters

ParameterTypeDescription
urlstringThe full URL of the API endpoint
scopestringOne of the scopes required by the API endpoint that your API key has been authorized to use.
query_dataarray (optional)Array of query data to apply to request
query_keysarray (optional)Array of connection configuration keys (customer, domain, subject_email) that should be included with API requests for specific endpoints (ex. Google Workspace Directory API) to merge into the query_data.
connectionarray (optional)An array with API connection variables. If not set, config('google-api-client') uses the GOOGLEAPI* variables from your .env file.

GET Example Usage

use Provisionesta\Google\ApiClient;

// Get a list of records
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/list
$groups = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups',
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    query_data: [],
    query_keys: ['customer']
);

You can also use variables or database models to get data for constructing your endpoints. Some endpoints require an ID while others allow a human friendly alias (ex. resource name or email address).

use Provisionesta\Google\ApiClient;

// Get a specific record using a variable
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/get
// $group_id = '0a1b2c3d4e5f6g7';
$group_id = 'elite-engineers@example.com';
$existing_group = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    query_data: [],
    query_keys: ['customer']
);

// Get a specific record using a database value
// This assumes that you have a database column named `api_group_id` that
// contains the string with the Google ID `0a1b2c3d4e5f6g7`.
$database_group = \App\Models\GoogleGroup::where('id', $id)->firstOrFail();
$api_group = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $google_group->api_group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    query_data: [],
    query_keys: ['customer']
);

GET Requests with Query String Parameters

The third positional argument or query_data named argument of a get() method is an optional array of parameters that is parsed by the API Client and the Laravel HTTP Client and rendered as a query string with the ? and & added automatically.

Query Keys

Many endpoints will require the customer, domain, and/or subject_email to be passed in the query string. To improve your developer experience, you do not need to add these values or merge query string arrays yourself. You simply need to provide the keys that the endpoint needs in the query_keys array. The values are pulled from your connection configuration (or .env file) and the key/value pairs are automatically merged with the query_data array to form a query string.

See the getConnectionQueryParams() method in ApiClient.php to learn more.

use Provisionesta\Google\ApiClient;

$group = ApiClient::post(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups',
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    query_data: [],
    query_keys: ['customer']
    // query_keys: ['customer', 'domain', 'subject_email']
);
https://admin.googleapis.com/admin/directory/v1/groups?customer=my_customer
API Request Filtering

The Google API uses child arrays for several resources. When searching for values, you use dot notation (ex. profile.name) to access to these attributes.

API Response Filtering

You can also use Laravel Collections to filter and transform results, either using a full data set or one that you already filtered with your API request.

See Using Laravel Collections in the API Responses documentation.

POST Requests

The post() method works almost identically to a get() request, with the addition of the form_data array parameter. This is industry standard and not specific to the API Client.

Since many Google endpoints require one of the query_keys and some use additional query string arguments so we cannot consolidate to just a data parameter.

You can learn more about request data in the Laravel HTTP Client documentation.

use Provisionesta\Google\ApiClient;

// Create a group
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/insert
$group = ApiClient::post(
    url: "https://admin.googleapis.com/admin/directory/v1/groups",
    scope: "https://www.googleapis.com/auth/admin.directory.group",
    form_data: [
        "name" => "Hack the Planet Engineers",
        "email" => "elite-engineers@example.com"
    ],
    query_keys: ["customer"]
);
ParameterTypeDescription
urlstringThe full URL of the API endpoint
scopestringOne of the scopes required by the API endpoint that your API key has been authorized to use.
form_dataarray (optional)Array of form data that will be converted to JSON
query_dataarray (optional)Array of query data to apply to request
query_keysarray (optional)Array of connection configuration keys (customer, domain, subject_email) that should be included with API requests for specific endpoints (ex. Google Workspace Directory API) to merge into the query_data.
connectionarray (optional)An array with API connection variables. If not set, config('google-api-client') uses the GOOGLEAPI* variables from your .env file.

PATCH Requests

The patch() method is used for updating one or more attributes on existing records. A patch is used for partial updates. If you want to update and replace the attributes for the entire existing record, you should use the put() method.

You need to ensure that the ID of the record that you want to update is provided in the URL. In most applications, this will be a variable that you get from your database or another location and won't be hard-coded.

PATCH Method Parameters

ParameterTypeDescription
urlstringThe full URL of the API endpoint
scopestringOne of the scopes required by the API endpoint that your API key has been authorized to use.
form_dataarray (optional)Array of form data that will be converted to JSON
query_dataarray (optional)Array of query data to apply to request
query_keysarray (optional)Array of connection configuration keys (customer, domain, subject_email) that should be included with API requests for specific endpoints (ex. Google Workspace Directory API) to merge into the query_data.
connectionarray (optional)An array with API connection variables. If not set, config('google-api-client') uses the GOOGLEAPI* variables from your .env file.

PATCH Example Usage

use Provisionesta\Google\ApiClient;

// Update a group (patch)
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/patch
// $group_id = '0a1b2c3d4e5f6g7';
$group_id = 'elite-engineers@example.com';
$response = ApiClient::patch(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    form_data: [
        'description' => 'This group contains engineers that have liberated the garbage files.'
    ],
    query_keys: ['customer']
);

PUT Requests

The put() method is used for updating and replacing the attributes for an entire existing record. If you want to update one or more attributes without updating the entire existing record, use the patch() method. For most use cases, you will want to use the patch() method to update records.

This may require fetching the record and overriding the value of the specific key in the array and passing the entire array back to the API client form_data argument.

You need to ensure that the ID of the record that you want to update is provided in the first argument (URI). In most applications, this will be a variable that you get from your database or another location and won't be hard-coded.

PUT Method Parameters

ParameterTypeDescription
urlstringThe full URL of the API endpoint
scopestringOne of the scopes required by the API endpoint that your API key has been authorized to use.
form_dataarray (optional)Array of form data that will be converted to JSON
query_dataarray (optional)Array of query data to apply to request
query_keysarray (optional)Array of connection configuration keys (customer, domain, subject_email) that should be included with API requests for specific endpoints (ex. Google Workspace Directory API) to merge into the query_data.
connectionarray (optional)An array with API connection variables. If not set, config('google-api-client') uses the GOOGLEAPI* variables from your .env file.

PUT Example Usage

use Provisionesta\Google\ApiClient;

// Get a specific record using a variable
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/get
// $group_id = '0a1b2c3d4e5f6g7';
$group_id = 'elite-engineers@example.com';
$existing_group = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
);

// {
//     +"data": {
//         +"id": "0a1b2c3d4e5f6g7",
//         +"email": "elite-engineers@example.com",
//         +"name": "Hack the Planet Engineers",
//         +"directMembersCount": "0",
//         +"description": "",
//         +"adminCreated": true,
//         +"nonEditableAliases": [
//             "elite-engineers@example.com.test-google-a.com",
//         ],
//     },
//     +"headers": [
//         ...
//     ],
//     +"status": {
//         +"code": 200,
//         +"ok": true,
//         +"successful": true,
//         +"failed": false,
//         +"serverError": false,
//         +"clientError": false,
//     },
// }

$existing_group->data->description = 'This group contains engineers that have revealed to the world their elite skills.';

// Update a group (put)
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/update
// $group_id = '0a1b2c3d4e5f6g7';
$group_id = 'elite-engineers@example.com';
$response = ApiClient::patch(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    form_data: $existing_group->data->description
);

DELETE Requests

The delete() method is used for methods that will destroy the resource based on the ID that you provide.

Keep in mind that delete() methods will return different status codes depending on the vendor (ex. 200, 201, 202, 204, etc). Google's API will return a 204 status code for successfully deleted resources. You should use the $response->status->successful boolean for checking results.

DELETE Method Parameters

ParameterTypeDescription
urlstringThe full URL of the API endpoint
scopestringOne of the scopes required by the API endpoint that your API key has been authorized to use.
form_dataarray (optional)Array of form data that will be converted to JSON
query_dataarray (optional)Array of query data to apply to request
query_keysarray (optional)Array of connection configuration keys (customer, domain, subject_email) that should be included with API requests for specific endpoints (ex. Google Workspace Directory API) to merge into the query_data.
connectionarray (optional)An array with API connection variables. If not set, config('google-api-client') uses the GOOGLEAPI* variables from your .env file.

DELETE Example Usage

use Provisionesta\Google\ApiClient;

// Delete a group
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/delete
// $group_id = '0a1b2c3d4e5f6g7';
$group_id = 'elite-engineers@example.com';
$response = ApiClient::delete(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group'
);

Class Methods

The examples above show basic inline usage that is suitable for most use cases. If you prefer to use classes and constructors, the example below will be helpful.

<?php

use Provisionesta\Google\ApiClient;
use Provisionesta\Google\Exceptions\NotFoundException;

class GoogleGroupService
{
    private $connection;

    public function __construct(array $connection = [])
    {
        $this->connection = $connection;
    }

    public function listGroups($query = [])
    {
        $groups = ApiClient::get(
            url: 'https://admin.googleapis.com/admin/directory/v1/groups',
            scope: 'https://www.googleapis.com/auth/admin.directory.group',
            query_data: $query,
            query_keys: ['customer'],
            connection: $this->connection
        );

        return $groups->data;
    }

    public function getGroup($id, $query = [])
    {
        try {
            $existing_group = ApiClient::get(
                url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $id,
                scope: 'https://www.googleapis.com/auth/admin.directory.group',
                query_data: $query,
                connection: $this->connection
            );
        } catch (NotFoundException $e) {
            // Custom logic to handle a record not found. For example, you could
            // redirect to a page and flash an alert message.
        }

        return $group->data;
    }

    public function storeGroup($request_data)
    {
        $group = ApiClient::post(
            url: "https://admin.googleapis.com/admin/directory/v1/groups",
            scope: "https://www.googleapis.com/auth/admin.directory.group",
            form_data: $request_data,
            connection: $this->connection
        );

        // To return an object with the newly created group
        return $group->data;

        // To return the ID of the newly created group
        // return $group->data->id;

        // To return the status code of the form request
        // return $group->status->code;

        // To return a bool with the status of the form request
        // return $group->status->successful;

        // To throw an exception if the request fails
        // throw_if(!$group->status->successful, new \Exception($group->error->message, $group->status->code));

        // To return the entire API response with the data, headers, and status
        // return $group;
    }

    public function updateGroup($id, $request_data)
    {
        try {
            $group = ApiClient::patch(
                url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $id,
                scope: 'https://www.googleapis.com/auth/admin.directory.group',
                form_data: $request_data,
                connection: $this->connection
            );
        } catch (NotFoundException $e) {
            // Custom logic to handle a record not found. For example, you could
            // redirect to a page and flash an alert message.
        }

        // To return an object with the updated group
        return $group->data;

        // To return a bool with the status of the form request
        // return $group->status->successful;
    }

    public function deleteGroup($id)
    {
        try {
            $group = ApiClient::delete(
                url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
                scope: 'https://www.googleapis.com/auth/admin.directory.group',
                connection: $this->connection
            );
        } catch (NotFoundException $e) {
            // Custom logic to handle a record not found. For example, you could
            // redirect to a page and flash an alert message.
        }

        return $group->status->successful;
    }
}

Rate Limits

Due to the vast amount services available in Google API endpoints, this API client cannot provide rate limit approaching detection and backoff. An exception is logged and thrown if a rate limit is exceeded and the request will fail.

See the rate limit documentation for the specific service that you are calling for any rate limiting needed, or consider adding sleep(#) helpers if needed.

API Responses

This API Client uses the Provisionesta standards for API response formatting.

// API Request
$group = ApiClient::get('groups/00g1ab2c3D4E5F6G7h8i');

// API Response
$group->data; // object
$group->headers; // array
$group->status; // object
$group->status->code; // int (ex. 200)
$group->status->ok; // bool (is 200 status)
$group->status->successful; // bool (is 2xx status)
$group->status->failed; // bool (is 4xx/5xx status)
$group->status->clientError; // bool (is 4xx status)
$group->status->serverError; // bool (is 5xx status)

Response Data

The data property contains the contents of the Laravel HTTP Client object() method that has been parsed and has the final merged output of any paginated results.

$group_id = 'elite-engineers@example.com';
$group = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group'
);

$group->data;
{
    +"id": "0a1b2c3d4e5f6g7",
    +"email": "elite-engineers@example.com",
    +"name": "Hack the Planet Engineers",
    +"directMembersCount": "0",
    +"description": "This group contains engineers that have liberated the garbage files.",
    +"adminCreated": true,
    +"nonEditableAliases": [
        "elite-engineers@example.com.test-google-a.com",
    ],
},

Access a single record value

You can access these variables using object notation. This is the most common use case for handling API responses.

$group_id = 'elite-engineers@example.com';
$group = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group'
)->data;

$group_name = $group->name;
// Hack the Planet Engineers

Looping through records

If you have an array of multiple objects, you can loop through the records. The API Client automatically paginates and merges the array of records for improved developer experience.

$groups = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups',
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    query_data: [],
    query_keys: ['customer']
)->data;

foreach($groups as $group) {
    dd($group->name);
    // Hack the Planet Engineers
}

Caching responses

The API Client does not use caching to avoid any constraints with you being able to control which endpoints you cache.

You can wrap an endpoint in a cache facade when making an API call. You can learn more in the Laravel Cache documentation.

use Illuminate\Support\Facades\Cache;
use Provisionesta\Google\ApiClient;

$groups = Cache::remember('google_groups', now()->addHours(2), function () {
    return ApiClient::get(
        url: 'https://admin.googleapis.com/admin/directory/v1/groups',
        scope: 'https://www.googleapis.com/auth/admin.directory.group',
        query_keys: ['customer']
    )->data;
});

foreach($groups as $group) {
    dd($group->name);
    // Hack the Planet Engineers
}

When getting a specific ID or passing additional arguments, be sure to pass variables into use($var1, $var2).

$group_id = '0a1b2c3d4e5f6g7';
// $group_id = 'elite-engineers@example.com';

$groups = Cache::remember('google_group_' . $group_id, now()->addHours(12), function () use ($group_id) {
    return ApiClient::get(
        url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
        scope: 'https://www.googleapis.com/auth/admin.directory.group'
    )->data;
});

Date Formatting

You can use the Carbon library for formatting dates and performing calculations.

use Carbon\Carbon;
$created_date = Carbon::parse($group->data->creationTime)->format('Y-m-d');
// 2023-01-01
$created_age_days = Carbon::parse($group->data->creationTime)->diffInDays();
// 265

Using Laravel Collections

You can use Laravel Collections which are powerful array helper tools that are similar to array searching and SQL queries that you may already be familiar with.

Response Headers

The headers are returned as an array instead of an object since the keys use hyphens that conflict with the syntax of accessing keys and values easily.

$group_id = 'elite-engineers@example.com';
$group = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group'
)->data;

$group->headers;
[
    "ETag" => "vd5VuYRc7EabgWBin1OmNozuzPe13OUXakGXQzUmnHA/AKpK7DnSvYzi2eNow9bdQ2H0TcE",
    "Content-Type" => "application/json; charset=UTF-8",
    "Vary" => [
        "Origin",
        "X-Origin",
        "Referer",
    ],
    "Date" => "Tue, 27 Feb 2024 03:30:20 GMT",
    "Server" => "ESF",
    "Content-Length" => "400",
    "X-XSS-Protection" => "0",
    "X-Frame-Options" => "SAMEORIGIN",
    "X-Content-Type-Options" => "nosniff",
    "Alt-Svc" => "h3=":443"; ma=2592000,h3-29=":443"; ma=2592000",
],

Getting a Header Value

$content_type = $group->headers['Content-Type'];
application/json; charset=UTF-8

Response Status

See the Laravel HTTP Client documentation to learn more about the different status booleans.

$group_id = 'elite-engineers@example.com';
$group = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group'
)->data;

$group->status;
{
  +"code": 200 // int (ex. 200)
  +"ok": true // bool (is 200 status)
  +"successful": true // bool (is 2xx status)
  +"failed": false // bool (is 4xx/5xx status)
  +"serverError": false // bool (is 4xx status)
  +"clientError": false // bool (is 5xx status)
}

API Response Status Code

$group_id = 'elite-engineers@example.com';
$group = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    query_data: [],
    query_keys: ['customer']
)->data;

$status_code = $group->status->code;
// 200

Error Responses

An exception is thrown for any 4xx or 5xx responses. All responses are automatically logged.

Exceptions

CodeException Class
N/AProvisionesta\Google\Exceptions\AuthenticationException
N/AProvisionesta\Google\Exceptions\ConfigurationException
400Provisionesta\Google\Exceptions\BadRequestException
401Provisionesta\Google\Exceptions\UnauthorizedException
403Provisionesta\Google\Exceptions\ForbiddenException
404Provisionesta\Google\Exceptions\NotFoundException
405Provisionesta\Google\Exceptions\MethodNotAllowedException
409Provisionesta\Google\Exceptions\ConflictException
412Provisionesta\Google\Exceptions\PreconditionFailedException
422Provisionesta\Google\Exceptions\UnprocessableException
429Provisionesta\Google\Exceptions\RateLimitException
500Provisionesta\Google\Exceptions\ServerErrorException
503Provisionesta\Google\Exceptions\ServiceUnavailableException

Catching Exceptions

You can catch any exceptions that you want to handle silently. Any uncaught exceptions will appear for users and cause 500 errors that will appear in your monitoring software.

use Provisionesta\Google\Exceptions\NotFoundException;

try {
    $group_id = 'elite-engineers@example.com';
    $group = ApiClient::get(
        url: 'https://admin.googleapis.com/admin/directory/v1/groups/' . $group_id,
        scope: 'https://www.googleapis.com/auth/admin.directory.group',
        query_data: [],
        query_keys: ['customer']
    )->data;
} catch (NotFoundException $e) {
    // Group is not found. You can create a log entry, throw an exception, or handle it another way.
    Log::error('Google group could not be found', ['google_group_id' => $group_id]);
}

Disabling Exceptions

If you do not want exceptions to be thrown, you can globally disable exceptions for the Google API Client and handle the status for each request yourself. Simply set the GOOGLE_API_EXCEPTIONS=false in your .env file.

GOOGLE_API_EXCEPTIONS=false

Log Examples

This package uses the provisionesta/audit package for standardized logs.

Event Types

The event_type key should be used for any categorization and log searches.

  • Format: google.api.{method}.{result/log_level}.{reason?}
  • Methods: get|post|patch|put|delete
Status CodeEvent TypeLog Level
N/Agoogle.api.validate.errorCRITICAL
N/Agoogle.api.validate.error.arrayCRITICAL
N/Agoogle.api.validate.error.emptyCRITICAL
N/Agoogle.api.auth.errorCRITICAL
N/Agoogle.api.auth.successDEBUG
N/Agoogle.api.get.process.pagination.startedDEBUG
N/Agoogle.api.get.process.pagination.finishedDEBUG
N/Agoogle.api.{method}.error.http.exceptionERROR
200google.api.{method}.successDEBUG
201google.api.{method}.successDEBUG
202google.api.{method}.successDEBUG
204google.api.{method}.successDEBUG
400google.api.{method}.warning.bad-requestWARNING
401google.api.{method}.error.unauthorizedERROR
403google.api.{method}.error.forbiddenERROR
404google.api.{method}.warning.not-foundWARNING
405google.api.{method}.error.method-not-allowedERROR
412google.api.{method}.error.precondition-failedDEBUG
422google.api.{method}.error.unprocessableDEBUG
429google.api.{method}.critical.rate-limitCRITICAL
500google.api.{method}.critical.server-errorCRITICAL
501google.api.{method}.error.not-implementedERROR
503google.api.{method}.critical.server-unavailableCRITICAL

Successful Requests

GET Request Log

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiToken::sendAuthRequest Success {"event_type":"google.api.auth.success","method":"App\\Actions\\Connections\\Google\\ApiToken::sendAuthRequest"}

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::get Success {"event_type":"google.api.get.success","method":"App\\Actions\\Connections\\Google\\ApiClient::get","count_records":5,"event_ms":810,"event_ms_per_record":162,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/users?customer=my_customer"}}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::get Success {"event_type":"google.api.get.success","method":"App\\Actions\\Connections\\Google\\ApiClient::get","count_records":29,"event_ms":334,"event_ms_per_record":11,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/users/0a1b2c3d4e5f6g7?customer=my_customer"}}

GET Paginated Request Log

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiToken::sendAuthRequest Success {"event_type":"google.api.auth.success","method":"App\\Actions\\Connections\\Google\\ApiToken::sendAuthRequest"}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::get Success {"event_type":"google.api.get.success","method":"App\\Actions\\Connections\\Google\\ApiClient::get","count_records":100,"event_ms":966,"event_ms_per_record":9,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/users?customer=my_customer"}}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::get Paginated Results Process Started {"event_type":"google.api.get.process.pagination.started","method":"App\\Actions\\Connections\\Google\\ApiClient::get","metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/users"}}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::getPaginatedResults Success {"event_type":"google.api.getPaginatedResults.success","method":"App\\Actions\\Connections\\Google\\ApiClient::getPaginatedResults","count_records":100,"event_ms":613,"event_ms_per_record":6,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/users?customer=my_customer&pageToken=REDACTED"}}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::getPaginatedResults Success {"event_type":"google.api.getPaginatedResults.success","method":"App\\Actions\\Connections\\Google\\ApiClient::getPaginatedResults","count_records":100,"event_ms":1352,"event_ms_per_record":13,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/users?customer=my_customer&pageToken=REDACTED"}}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::getPaginatedResults Success {"event_type":"google.api.getPaginatedResults.success","method":"App\\Actions\\Connections\\Google\\ApiClient::getPaginatedResults","count_records":100,"event_ms":2151,"event_ms_per_record":21,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/users?customer=my_customer&pageToken=REDACTED"}}
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::getPaginatedResults Success {"event_type":"google.api.getPaginatedResults.success","method":"App\\Actions\\Connections\\Google\\ApiClient::getPaginatedResults","count_records":100,"event_ms":2835,"event_ms_per_record":28,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/users?customer=my_customer&pageToken=REDACTED"}}
...
[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::get Paginated Results Process Complete {"event_type":"google.api.get.process.pagination.finished","method":"App\\Actions\\Connections\\Google\\ApiClient::get","count_records":3820,"duration_ms":29423,"duration_ms_per_record":7,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/users"}}

POST Request Log

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::post Success {"event_type":"google.api.post.success","method":"App\\Actions\\Connections\\Google\\ApiClient::post","count_records":5,"event_ms":1054,"event_ms_per_record":210,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/groups?customer=my_customer"}}

PATCH Request Log

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::patch Success {"event_type":"google.api.patch.success","method":"App\\Actions\\Connections\\Google\\ApiClient::patch","count_records":6,"event_ms":775,"event_ms_per_record":128,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/groups/0a1b2c3d4e5f6g7?customer=my_customer"}}

PUT Success Log

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::put Success {"event_type":"google.api.put.success","method":"App\\Actions\\Connections\\Google\\ApiClient::put","count_records":6,"event_ms":623,"event_ms_per_record":103,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/groups/0a1b2c3d4e5f6g7?customer=my_customer"}}

DELETE Success Log

[YYYY-MM-DD HH:II:SS] local.DEBUG: ApiClient::delete Success {"event_type":"google.api.delete.success","method":"App\\Actions\\Connections\\Google\\ApiClient::delete","event_ms":584,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/groups/0a1b2c3d4e5f6g7?customer=my_customer"}}

Errors

Authentication Scopes Missing Error

[YYYY-MM-DD HH:II:SS] local.CRITICAL: ApiToken::sendAuthRequest Error {"event_type":"google.api.auth.error","method":"Provisionesta\\Google\\ApiToken::sendAuthRequest","errors":["Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested."]}

Missing Customer ID Error

The most frequent error that you will see is mysteriously generic 400 Bad Request that is caused by not adding query_keys: ['customer'] to your request, especially with Google Workspace related API calls. This is particularly common with get() requests with a large number of results.

// Before
$groups = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups',
    scope: 'https://www.googleapis.com/auth/admin.directory.group'
);

// After
$groups = ApiClient::get(
    url: 'https://admin.googleapis.com/admin/directory/v1/groups',
    scope: 'https://www.googleapis.com/auth/admin.directory.group',
    query_keys: ['customer'],
);

400 Bad Request

See the Missing Customer ID Error for initial troubleshooting.

[YYYY-MM-DD HH:II:SS] local.WARNING: ApiClient::get Client Error {"event_type":"google.api.get.warning.bad-request","method":"App\\Actions\\Connections\\Google\\ApiClient::get","event_ms":468,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/groups"}}

404 Not Found

[YYYY-MM-DD HH:II:SS] local.WARNING: ApiClient::get Client Error {"event_type":"google.api.get.warning.not-found","method":"App\\Actions\\Connections\\Google\\ApiClient::get","event_ms":354,"metadata":{"url":"https://admin.googleapis.com/admin/directory/v1/users/user@example.com?customer=my_customer"}}

429 Rate Limit Exception

[YYYY-MM-DD HH:II:SS] local.CRITICAL: ApiClient::get Client Error {"event_type":"google.api.get.critical.rate-limit","method":"Provisionesta\\Google\\ApiClient::get","errors":{"message":"Quota exceeded for quota metric 'Read requests' and limit 'Read requests per minute per user' of service 'sheets.googleapis.com' for consumer 'project_number:123456789012'."},"event_ms":129,"metadata":{"url":"https://sheets.googleapis.com/v4/spreadsheets/REDACTED"}}