Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"prettier.semi": true,
"prettier.tabWidth": 2,
"prettier.singleQuote": false,
"prettier.trailingComma": "es5"
}
"prettier.trailingComma": "es5",
"typescript.tsdk": "node_modules/typescript/lib"
}
35 changes: 29 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ npm install google-ads-api
- [Create a client](#create-a-client)
- [Create a customer instance](#create-a-customer-instance)
- [List accessible customers](#list-accessible-customers)
- [Service Account Support](#service-account-support)
- Reporting
- [Retrieve Campaigns with metrics](#retrieve-campaigns-with-metrics)
- [Retrieve Campaigns using GAQL](#retrieve-campaigns-using-gaql)
Expand Down Expand Up @@ -91,6 +92,28 @@ const customer = client.Customer({

---

## Service Account Support

Instead of OAuth2, you can use the Google Ads API with service account authentication. See [SERVICE_ACCOUNT_AUTH.md](SERVICE_ACCOUNT_AUTH.md) for more information.

```ts
// Create GoogleAdsApi client with service account auth
const client = new GoogleAdsApi({
auth_client: authClient as ServiceAccountAuth,
developer_token: "<DEVELOPER-TOKEN>",
// no client_id or client_secret needed
});

// Create a customer instance (no refresh_token needed)
const customer = client.Customer({
customer_id: "1234567890",
login_customer_id: "<LOGIN-CUSTOMER-ID>",
// no refresh_token needed
});
```

---

## List accessible customers

This is a special client method for listing the accessible customers for a given refresh token, and is equivalent to [CustomerService.listAccessibleCustomers](https://developers.google.com/google-ads/api/reference/rpc/v20/CustomerService#listaccessiblecustomers). It returns the resource names of available customer accounts.
Expand Down Expand Up @@ -549,7 +572,7 @@ ResourceNames.accountBudget(customer.credentials.customer_id, 123);

The library provides hooks that can be executed before, after or on error of a query, stream or a mutation.

### Query/stream hooks:
### Query/stream hooks

- `onQueryStart`
- `onQueryError`
Expand All @@ -563,7 +586,7 @@ These hooks also have access to the `query` argument, containing the GAQL query

These hooks also have access the the `reportOptions` argument. This will be undefined when using the `query` method.

### Mutation hooks:
### Mutation hooks

- `onMutationStart`
- `onMutationError`
Expand All @@ -573,7 +596,7 @@ These hooks have access to the `customerCredentials` argument, containing the `c

These hooks also have access to the `method` argument, containing the mutation method as a string.

### Service hooks:
### Service hooks

- `onServiceStart`
- `onServiceError`
Expand All @@ -583,7 +606,7 @@ These hooks have access to the `customerCredentials` argument, containing the `c

These hooks also have access to the `method` argument, containing the mutation method as a string.

### Pre-request hooks:
### Pre-request hooks

- `onQueryStart` - `query` and `report`
- `onStreamStart` - `reportStream` and `reportStreamRaw`
Expand Down Expand Up @@ -617,7 +640,7 @@ const customer = client.Customer(
);
```

### On error hooks:
### On error hooks

- `onQueryError` - `query` and `report`
- `onStreamError` - `reportStream` (but **not** `reportStreamRaw`)
Expand All @@ -642,7 +665,7 @@ const customer = client.Customer(
);
```

### Post-request hooks:
### Post-request hooks

- `onQueryEnd` - `query` and `report`
- `onMutationEnd`
Expand Down
76 changes: 76 additions & 0 deletions SERVICE_ACCOUNT_AUTH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Service Account Authentication Support

This document explains how to use the Google Ads API with Google Cloud Service Account authentication instead of OAuth2. A service account is an account that belongs to your app instead of to an individual end user. Service accounts enable server-to-server interactions between a web app and a Google service. Your app calls Google APIs on behalf of the service account, so users aren't directly involved.

## Overview

The Google Ads API library now supports both OAuth2 and service account authentication methods:

1. **OAuth2 (existing)**: Uses `client_id`, `client_secret`, and `refresh_token`, good for user-facing applications
2. **Service Account (new)**: Uses a service account key file, good for server-to-server applications

[Depending on your application type](https://developers.google.com/google-ads/api/docs/get-started/choose-application-type) choose between using User Authentication with OAuth2 or a using a service account.

### Key Differences

#### OAuth2 Authentication

- Requires `client_id`, `client_secret`, `developer_token`
- Requires `refresh_token` for each customer
- Good for user-facing applications

#### Service Account Authentication

- Requires service account key file and `developer_token`
- No `refresh_token` needed
- Good for server-to-server applications

See more in [this guide](https://developers.google.com/google-ads/api/docs/get-started/choose-application-type).

## Preparation

Follow [this guide](https://developers.google.com/google-ads/api/docs/oauth/service-accounts#setting_up_service_account_access) on how to set up a service account and share your Google Ads account with it. Make sure you also have [the `developer_token`](https://developers.google.com/google-ads/api/docs/get-started/dev-token) from your Google Ads manager account and that the [Google Ads API is enabled](https://developers.google.com/google-ads/api/docs/get-started/oauth-cloud-project#enable-api) in your Google Cloud project.

## Using Service Account Authentication

```typescript
import { GoogleAdsApi } from "google-ads-api";
import { auth, JWT } from "google-auth-library";

// Service account key (should not be in your code but usually loaded from secret, environment or file)
// This is also the JSON you download when creating the service account in Google Cloud Console
const serviceAccountKey = {
type: "service_account",
project_id: "your-project-id",
private_key_id: "your-private-key-id",
private_key: "your-private-key",
client_email: "your-client-email",
client_id: "your-client-id",
auth_uri: "https://accounts.google.com/o/oauth2/auth",
token_uri: "https://accounts.google.com/o/oauth2/token",
auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs",
client_x509_cert_url: "your-cert-url",
};

// Create JWT auth client from service account key
const authClient = auth.fromJSON(serviceAccountKey) as JWT;
authClient.scopes = ["https://www.googleapis.com/auth/adwords"];
await authClient.authorize();

// Create GoogleAdsApi client with service account auth
const client = new GoogleAdsApi({
auth_client: authClient,
developer_token: "YOUR_DEVELOPER_TOKEN",
});

// Create a customer instance (no refresh_token needed)
const customer = client.Customer({
customer_id: "CUSTOMER_ID",
login_customer_id: "LOGIN_CUSTOMER_ID", // optional
});

// Use as normal
const campaigns = await customer.query(
"SELECT campaign.id, campaign.name FROM campaign"
);
```
66 changes: 66 additions & 0 deletions examples/serviceAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { auth, JWT } from "google-auth-library";
import { GoogleAdsApi } from "../src";

const LOGIN_CUSTOMER_ID = "xxx"; // Adwords manager account (optional)
const GOOGLE_DEVELOPER_TOKEN = "yyy"; // Your developer token
const LINKED_CUSTOMER_ID = "zzz"; // The customer account you want to access

async function main() {
// Service account key (should not be in your code but usually loaded from secret, environment or file)
// See https://youtu.be/8MYzuG7JzLs?si=0aoBVEQI7wa-b3uh on how to get one
const serviceAccountKey = {
type: "service_account",
project_id: "your-project-id",
private_key_id: "your-private-key-id",
private_key: "your-private-key",
client_email: "your-client-email",
client_id: "your-client-id",
auth_uri: "https://accounts.google.com/o/oauth2/auth",
token_uri: "https://accounts.google.com/o/oauth2/token",
auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs",
client_x509_cert_url: "your-cert-url",
};

// Create JWT auth client from service account key
const authClient = auth.fromJSON(serviceAccountKey) as JWT;
authClient.scopes = ["https://www.googleapis.com/auth/adwords"];
await authClient.authorize();

console.log(
"Successfully authenticated with Google Ads API using service account"
);

// Create GoogleAdsApi client with service account auth
const client = new GoogleAdsApi({
auth_client: authClient,
developer_token: GOOGLE_DEVELOPER_TOKEN,
});

// Create a customer instance - no refresh_token needed for service accounts
const customer = client.Customer({
customer_id: LINKED_CUSTOMER_ID,
login_customer_id: LOGIN_CUSTOMER_ID, // optional
});

try {
// Example: Search for campaigns
const campaigns = await customer.query(`
SELECT
campaign.id,
campaign.name,
campaign.network_settings.target_content_network
FROM campaign
ORDER BY campaign.id
`);

console.log("Campaigns:", campaigns);

// Example: List accessible customers using service account
const accessibleCustomers = await client.listAccessibleCustomers();
console.log("Accessible customers:", accessibleCustomers);
} catch (error) {
console.error("Error:", error);
}
}

main().catch(console.error);
7 changes: 2 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"axios": "^1.6.7",
"circ-json": "^1.0.4",
"google-ads-node": "18.0.0",
"google-auth-library": "^9.15.1",
"google-auth-library": "^10.3.0",
"google-gax": "^5.0.1",
"long": "^4.0.0",
"map-obj": "^4.0.0",
Expand Down Expand Up @@ -61,8 +61,5 @@
"adwords javascript",
"gads"
],
"resolutions": {
"google-auth-library": "^9.15.1"
},
"packageManager": "[email protected]+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72"
}
}
101 changes: 101 additions & 0 deletions scripts/fix-esm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import * as fs from "fs";
import * as path from "path";

const buildDir = path.join(process.cwd(), "build/esm");

const RELATIVE_IMPORT_RE = /(from\s+['"])(\.?\.\/[^'"\n]+)(['"])/g;
const RELATIVE_SIDE_EFFECT_IMPORT_RE =
/(import\s+['"])(\.?\.\/[^'"\n]+)(['"])/g;

const KNOWN_EXTENSIONS = [".js", ".mjs", ".cjs", ".json", ".node"] as const;

function hasKnownExtension(specifier: string) {
return KNOWN_EXTENSIONS.some((ext) => specifier.endsWith(ext));
}

function resolveWithExtension(fromFile: string, specifier: string) {
if (!specifier.startsWith("./") && !specifier.startsWith("../")) {
return specifier;
}

if (hasKnownExtension(specifier)) {
return specifier;
}

const dirname = path.dirname(fromFile);
const absoluteBase = path.resolve(dirname, specifier);

for (const ext of [".js", ".mjs", ".cjs"]) {
const candidate = `${absoluteBase}${ext}`;
if (fs.existsSync(candidate)) {
return `${specifier}${ext}`;
}
}

if (fs.existsSync(absoluteBase) && fs.statSync(absoluteBase).isDirectory()) {
for (const indexName of ["index.js", "index.mjs", "index.cjs"]) {
const indexPath = path.join(absoluteBase, indexName);
if (fs.existsSync(indexPath)) {
const separator = specifier.endsWith("/") ? "" : "/";
return `${specifier}${separator}${indexName}`;
}
}
}

return `${specifier}.js`;
}

function appendExtension(fromFile: string) {
return (match: string, prefix: string, specifier: string, suffix: string) => {
const resolved = resolveWithExtension(fromFile, specifier);
if (resolved === specifier) {
return match;
}
return `${prefix}${resolved}${suffix}`;
};
}

function addJsExtension(filePath: string) {
const content = fs.readFileSync(filePath, "utf8");
const replacedFrom = content.replace(
RELATIVE_IMPORT_RE,
appendExtension(filePath)
);
const newContent = replacedFrom.replace(
RELATIVE_SIDE_EFFECT_IMPORT_RE,
appendExtension(filePath)
);

if (content !== newContent) {
fs.writeFileSync(filePath, newContent);
console.log(`Updated imports in ${filePath}`);
}
}

function traverseDir(dir: string) {
const files = fs.readdirSync(dir);

for (const file of files) {
const fullPath = path.join(dir, file);
const stat = fs.statSync(fullPath);

if (stat.isDirectory()) {
traverseDir(fullPath);
} else if (file.endsWith(".js")) {
addJsExtension(fullPath);
}
}
}

if (fs.existsSync(buildDir)) {
console.log(`Fixing ESM imports in ${buildDir}...`);
traverseDir(buildDir);
const esmPackageJsonPath = path.join(buildDir, "package.json");
const esmPackageJson = { type: "module" };
fs.writeFileSync(esmPackageJsonPath, JSON.stringify(esmPackageJson, null, 2));
console.log(`Wrote ${esmPackageJsonPath}`);
console.log("Done.");
} else {
console.error(`Build directory ${buildDir} not found.`);
process.exit(1);
}
Loading