# Gpc Sdk Usage

> Use when building applications that programmatically interact with the Google Play Developer API using GPC's TypeScript SDK packages. Make sure to use this skill whenever the user mentions @gpc-cli/api, @gpc-cli/auth, PlayApiClient, createApiClient, resolveAuth, listReports, downloadStatsReport, downloadFinancialReport, STORAGE_READ_ONLY_SCOPE, Google Play API client, TypeScript SDK, programmatic access, API client, HTTP client, rate limiter, pagination, edit lifecycle in code, Node.js Google Play, server-side Play Store, backend integration — even if they don't explicitly say 'SDK.' Also trigger when someone wants to build a backend service, custom dashboard, automation script, or any TypeScript/JavaScript application that interacts with Google Play programmatically rather than through the CLI. For CLI usage, see other gpc-* skills. For building plugins, see gpc-plugin-development.

- Skill: `yasserstudio/gpc-sdk-usage` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add yasserstudio/gpc-sdk-usage`
- Raw SKILL.md: https://api.skillmd.com/api/skills/yasserstudio/gpc-sdk-usage/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: yasserstudio (https://skillmd.com/u/yasserstudio)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/yasserstudio/gpc-sdk-usage

---


# gpc-sdk-usage

Use @gpc-cli/api and @gpc-cli/auth as standalone TypeScript SDK packages for programmatic Google Play access.

## When to use

- Building a backend service that interacts with Google Play
- Creating custom dashboards or automation scripts
- Programmatic release management from TypeScript/JavaScript
- Using the typed API client directly (not through the CLI)
- Integrating Google Play operations into a larger application

## Inputs required

- **Node.js 20+** and **TypeScript 5+**
- **@gpc-cli/api** and **@gpc-cli/auth** packages
- **Service account key** — JSON file or raw JSON string

## Procedure

### 0. Install packages

```bash
npm install @gpc-cli/api @gpc-cli/auth
```

These are standalone packages — no need to install the full CLI.

### 1. Authenticate

```typescript
import { resolveAuth } from "@gpc-cli/auth";

// From file path
const auth = await resolveAuth({
  serviceAccountPath: "/path/to/key.json",
});

// From JSON string (e.g., from environment variable)
const auth = await resolveAuth({
  serviceAccountJson: process.env.PLAY_SA_KEY,
});

// From environment (GPC_SERVICE_ACCOUNT or GOOGLE_APPLICATION_CREDENTIALS)
const auth = await resolveAuth();
```

`Read:` `references/auth-patterns.md` for advanced auth patterns and token caching.

### 2. Create API client

```typescript
import { createApiClient } from "@gpc-cli/api";

const client = createApiClient({
  auth,
  maxRetries: 3,
  timeout: 30_000,
});
```

The client provides typed access to all 230 Google Play Developer API endpoints across the Android Publisher v3, Play Developer Reporting v1beta1, Play Custom App Publishing v1 (v0.9.56+), and Games Configuration v1configuration (v0.9.86+) APIs.

### 2a. Create the Enterprise client (v0.9.56+)

For private app publishing via the Play Custom App Publishing API, use a separate factory:

```typescript
import { createEnterpriseClient, type CustomApp } from "@gpc-cli/api";

const enterprise = createEnterpriseClient({ auth });

const app: CustomApp = await enterprise.apps.create(
  "1234567890",              // developer account ID (int64, from Play Console URL)
  "./app.aab",                // bundle path
  {
    title: "My Private App",
    languageCode: "en_US",
    organizations: [{ organizationId: "customer-org-id" }],
  },
);

console.log("Assigned package name:", app.packageName);
// com.google.customapp.A1B2C3D4E5 (Google-assigned, you cannot influence)
```

**Notes:**

- Private apps are permanently private. Once created, they cannot be made public.
- After creation, subsequent operations (version uploads, tracks, listings) go through the regular `createApiClient()` using the returned `packageName`.
- Requires the "create and publish private apps" permission on your service account in Play Console.
- The underlying `HttpClient.uploadCustomApp<T>(path, filePath, metadata, contentType)` method handles a multipart resumable upload where the initial session-initiation POST carries the JSON metadata. See `ResumableUploadOptions.initialMetadata` for reusing this pattern with other Google APIs.

See the `gpc-enterprise` skill for the CLI equivalent and full setup walkthrough.

`Read:` `references/api-reference.md` for the complete client API with all namespaces and methods.

### 3. Edit lifecycle

Most Google Play operations require an edit session:

```typescript
const APP = "com.example.app";

// 1. Create an edit
const edit = await client.edits.insert(APP);

// 2. Make changes within the edit
const tracks = await client.tracks.list(APP, edit.id);
const details = await client.details.get(APP, edit.id);

// 3. Validate before committing
await client.edits.validate(APP, edit.id);

// 4. Commit the edit (applies all changes)
await client.edits.commit(APP, edit.id);

// Optional: commit with options (v0.9.51+)
await client.edits.commit(APP, edit.id, {
  changesNotSentForReview: true,
  changesInReviewBehavior: "HALT_REVIEW",
});
```

**Important:** Only one edit can be open at a time. Always commit or delete edits.

### 4. Common patterns

#### Create a custom closed testing track (v0.9.79+)

```typescript
// Create a custom closed testing track before uploading to it
const track = await client.edits.tracks.create(packageName, "my-custom-track");
```

Custom tracks must be created before any release can be assigned to them. After creation, use the standard `client.tracks.update()` call to push a release to the new track.

#### Upload a release

```typescript
const edit = await client.edits.insert(APP);

// Upload the bundle
const bundle = await client.bundles.upload(APP, edit.id, "app-release.aab");

// Upload with device tier config (v0.9.51+)
const bundle2 = await client.bundles.upload(APP, edit.id, "app-release.aab", {
  deviceTierConfigId: "my-tier-config",
});

// Set the track
await client.tracks.update(APP, edit.id, "beta", {
  track: "beta",
  releases: [{
    versionCodes: [bundle.versionCode],
    status: "completed",
    releaseNotes: [
      { language: "en-US", text: "Bug fixes and improvements" },
    ],
  }],
});

// Commit
await client.edits.validate(APP, edit.id);
await client.edits.commit(APP, edit.id);
```

#### List and respond to reviews

```typescript
// No edit needed for reviews
const reviews = await client.reviews.list(APP, {
  maxResults: 50,
  translationLanguage: "en",
  startIndex: 0,  // pagination offset (v0.9.51+)
});

for (const review of reviews.reviews ?? []) {
  if (review.comments?.[0]?.userComment?.starRating <= 2) {
    await client.reviews.reply(APP, review.reviewId, "Thanks for the feedback!");
  }
}
```

#### Manage subscriptions

```typescript
// No edit needed for subscriptions
const subs = await client.subscriptions.list(APP);

// Get a specific subscription
const sub = await client.subscriptions.get(APP, "premium_monthly");

// Update with mutation options (v0.9.51+)
await client.subscriptions.update(APP, "premium_monthly", data, "price", {
  allowMissing: true,
  latencyTolerance: "PRODUCT_UPDATE_LATENCY_TOLERANCE_LATENCY_TOLERANT",
});

// Activate a base plan
await client.subscriptions.activateBasePlan(APP, "premium_monthly", "monthly");
```

#### Verify purchases

```typescript
// No edit needed for purchases
const purchase = await client.purchases.getProduct(APP, "coins_100", purchaseToken);

if (purchase.purchaseState === 0 && purchase.acknowledgementState === 0) {
  await client.purchases.acknowledgeProduct(APP, "coins_100", purchaseToken);
}
```

#### Upload deobfuscation files (v0.9.51+)

```typescript
// Upload ProGuard mapping
await client.deobfuscation.upload(APP, edit.id, versionCode, "mapping.txt", "proguard");

// Upload native debug symbols
await client.deobfuscation.upload(APP, edit.id, versionCode, "symbols.zip", "nativeCode");
```

#### Manage expansion files (v0.9.51+)

```typescript
// Get expansion file info
const obb = await client.expansionFiles.get(APP, edit.id, versionCode, "main");

// Upload a new expansion file
const uploaded = await client.expansionFiles.upload(APP, edit.id, versionCode, "main", "main.obb");

// Patch expansion file references
await client.expansionFiles.patch(APP, edit.id, versionCode, "main", {
  referencesVersion: 10,
});
```

#### List one-time products with pagination (v0.9.51+)

```typescript
const products = await client.oneTimeProducts.list(APP, {
  pageSize: 25,
  pageToken: nextToken,
});
```

### 5. Pagination

Use the built-in pagination utilities for large result sets:

```typescript
import { paginate, paginateAll } from "@gpc-cli/api";

// Async generator (stream results)
for await (const page of paginate(
  (token) => client.subscriptions.list(APP, { pageToken: token }),
  { limit: 100 },
)) {
  for (const sub of page) {
    console.log(sub.productId);
  }
}

// Collect all results
const all = await paginateAll(
  (token) => client.subscriptions.list(APP, { pageToken: token }),
);
```

> **New in v0.9.83:** `paginateAll` now returns a real continuation token, so `--limit` + `--next-page` correctly resumes across reviews, users, purchases, IAP, and subscriptions. Every CLI list command also shares the unified `{ <key>, nextPageToken, meta.count, message? }` JSON envelope (extended to grants, testers, and tracks in v0.9.87). Scripts reading a bare array from list commands will break — update to destructure the keyed field.

### 6. Rate limiting

Since v0.9.47, `createApiClient()` automatically applies rate limiting to all API calls using Google's 6-bucket model (3,000 queries/min each). No manual configuration needed:

```typescript
// Rate limiting is automatic — all calls are throttled by resource type
const client = createApiClient({ auth });
// Buckets: edits, purchases, reviews, reporting, monetization, default
```

To customize rate limits (e.g., for shared quota across multiple processes):

```typescript
import { createRateLimiter, RATE_LIMIT_BUCKETS } from "@gpc-cli/api";

// Override specific buckets
const limiter = createRateLimiter([
  { ...RATE_LIMIT_BUCKETS.edits, maxTokens: 1500 },     // Half of default
  { ...RATE_LIMIT_BUCKETS.purchases, maxTokens: 1500 },
]);

const client = createApiClient({ auth, rateLimiter: limiter });
```

The `resolveBucket(path)` function maps API paths to buckets automatically:
- `/edits/` paths → `edits` bucket
- `/purchases/`, `/orders` → `purchases` bucket
- `/reviews` → `reviews` bucket
- Reporting API → `reporting` bucket
- `/subscriptions`, `/oneTimeProducts`, `/inappproducts` → `monetization` bucket
- Everything else → `default` bucket

### 7. Error handling

```typescript
import { PlayApiError } from "@gpc-cli/api";
import { AuthError } from "@gpc-cli/auth";

try {
  await client.edits.insert(APP);
} catch (error) {
  if (error instanceof AuthError) {
    console.error(`Auth failed: ${error.code}`);
  } else if (error instanceof PlayApiError) {
    console.error(`API error ${error.status}: ${error.code}`);
    console.error(`Suggestion: ${error.suggestion}`);
  }
}
```

### Changelog generation (v0.9.62+)

The changelog pipeline from `gpc changelog generate` is exposed as standalone `@gpc-cli/core` exports — useful for CI tooling that wants the clustered/linted data structure directly.

```typescript
import {
  generateChangelog,
  resolveLocales,
  renderPlayStore,
  PLAY_STORE_LIMIT,     // 500
  type LocaleBundle,
  type GeneratedChangelog,
} from "@gpc-cli/core";

const generated: GeneratedChangelog = await generateChangelog({
  from: "v0.9.61",
  to: "HEAD",
});

// GitHub target: three renderers exposed as RENDERERS["md" | "json" | "prompt"]
// Play Store target: resolveLocales + renderPlayStore
const locales = await resolveLocales("en-US,fr-FR,de-DE");
const { output, bundle } = renderPlayStore(generated, {
  locales,
  format: "json",
});

for (const entry of bundle.locales) {
  console.log(`${entry.language}: ${entry.chars}/${entry.limit} (${entry.status})`);
}
```

For `--locales auto`, pass `{ client, packageName }` as the second arg to `resolveLocales` — it calls `client.listings.list` to infer the locale set from your live Play Store listing.

### Apply release notes to a draft (v0.9.64+)

```typescript
import {
  applyReleaseNotes,
  validateBundleForApply,
  bundleToReleaseNotes,
  waitForBundleProcessing,
} from "@gpc-cli/core";

// Convert a LocaleBundle to the API shape
const releaseNotes = bundleToReleaseNotes(bundle);

// Validate (returns blocked locale errors, if any)
const errors = validateBundleForApply(bundle);
if (errors.length > 0) throw new Error(errors.join(", "));

// Write into the latest draft on a track
await applyReleaseNotes(client, "com.example.app", "production", releaseNotes);

// waitForBundleProcessing (v0.9.64+, extended v0.9.77): polls bundles.list
// after AAB upload with Fibonacci backoff (2s, 3s, 5s, 8s, 13s, 21s, 34s ~86s)
// until the uploaded versionCode appears. Fixes large-AAB race.
// v0.9.77 also adds multi-retry guard on validate/commit (15s, 30s, 45s).
await waitForBundleProcessing(client, "com.example.app", editId, versionCode);
```

### VitalsThresholds in config types (v0.9.82+)

`VitalsThresholds` is now part of the typed config surface exposed by `@gpc-cli/config`:

```typescript
import type { GpcConfig } from "@gpc-cli/config";

const config: GpcConfig = {
  vitals: {
    thresholds: { crashRate: 2.0 },
  },
};
```

`VitalsThresholds` is also present on `ResolvedConfig` (the fully merged runtime shape). Use it when building tooling that reads or writes GPC config files programmatically.

### OfferPhaseDetails on Orders (v0.9.79+)

The flat `offerPhase` string field on Orders is deprecated. Read from `offerPhaseDetails` instead:

```typescript
const order = await client.purchases.orders.get(APP, orderId);
// Deprecated: order.offerPhase
// Preferred:
const phase = order.offerPhaseDetails; // OfferPhaseDetails — phase type, cycle counts, pricing
```

### download() exponential backoff (v0.9.80+)

`client.download()` (used for APK/AAB binary downloads) now retries automatically with exponential backoff, matching the retry behavior of `request()`. No code changes needed — transient 5xx errors and network timeouts are retried transparently.

### Bulk reports from the Play GCS bucket (v0.9.93+)

Play's monthly bulk reports are CSV objects in a Cloud Storage bucket linked to the developer account, not a Publisher API resource. **Breaking in v0.9.93:** the non-functional `client.reports.list` was removed from `@gpc-cli/api`, and the old `downloadReport` export was removed from `@gpc-cli/core`. Use the three new `@gpc-cli/core` exports instead.

```typescript
import { resolveAuth, DEFAULT_SCOPES, STORAGE_READ_ONLY_SCOPE } from "@gpc-cli/auth";
import {
  listReports,
  downloadStatsReport,
  downloadFinancialReport,
  resolveReportsBucket,
  parseMonth,
  type ListReportsResult,
} from "@gpc-cli/core";

// The reports path needs one extra scope. Request it only here: storage-scoped tokens are
// cached under a separate key, so other commands' tokens never carry storage access.
const auth = await resolveAuth({
  serviceAccountPath: "key.json",
  scopes: [...DEFAULT_SCOPES, STORAGE_READ_ONLY_SCOPE],
});

// pubsite_prod_<developerId>, or an explicit reports.bucket value
const bucket = resolveReportsBucket({ developerId: "1234567890" });
const month = parseMonth("2026-02");

const { reports, nextPageToken }: ListReportsResult = await listReports(
  auth,
  bucket,
  "installs",
  { packageName: "com.example.app", month, maxResults: 50 },
);

// Stats: per app, one CSV per dimension (reviews reports have no dimension)
const stats = await downloadStatsReport(
  auth, bucket, "com.example.app", "installs", month, "country",
);
console.log(stats.objectName, stats.csv); // gunzipped, UTF-16 decoded to UTF-8

// Financial: account-level, ZIP archives
const fin = await downloadFinancialReport(auth, bucket, "earnings", month);
if (fin.kind === "csv") {
  console.log(fin.text);
} else {
  for (const entry of fin.entries) console.log(entry.name, entry.csv.length);
}
```

`auth` only has to satisfy `ReportsAuth` (`{ getAccessToken(): Promise<string> }`), so any token source works. Failures throw `GpcError` with the `REPORT_*` codes (see gpc-troubleshooting); the first-run one is `REPORT_ACCESS_DENIED`, raised until the service account is granted "View app information and download bulk reports (read-only)" in Play Console.

### API correctness history (recent)

- **v0.9.57:** `apprecovery.cancel`/`deploy` URLs now use plural `/appRecoveries/`. `dataSafety.update` is `POST`, not `PUT`. Phantom `dataSafety.get` was removed. `onetimeproducts.offers.activateOffer` / `deactivateOffer` added. New `getVitalsErrorCount` function.
- **v0.9.58 / v0.9.59:** Vitals LMK metric set is `lmkRateMetricSet` with metrics `userPerceivedLmkRate`, `userPerceivedLmkRate7dUserWeighted`, `userPerceivedLmkRate28dUserWeighted`, `distinctUsers`. (v0.9.58 shipped the wrong resource name; v0.9.59 is the corrected build.)

## Verification

- `resolveAuth()` returns a valid auth client
- `createApiClient({ auth })` creates a working client
- `client.edits.insert(APP)` successfully opens an edit
- API calls return typed responses
- Error handling catches `PlayApiError` and `AuthError`

## Failure modes / debugging

| Symptom | Likely Cause | Fix |
|---------|-------------|-----|
| `AUTH_NO_CREDENTIALS` | No auth source found | Pass `serviceAccountPath` or set `GPC_SERVICE_ACCOUNT` |
| `AUTH_INVALID_KEY` | Bad JSON in key file | Re-download from Google Cloud Console |
| Edit insert fails with 403 | Service account lacks API access | Enable Google Play Developer API in GCP |
| Concurrent edit conflict | Another edit is open | Commit or delete the existing edit first |
| `PlayApiError` with status 429 | Rate limited | Use `createRateLimiter()` with appropriate buckets |
| Types not resolving | Wrong TypeScript config | Ensure `moduleResolution: "bundler"` or `"node16"` |
| `client.reports.list` is not a function | Removed in v0.9.93 (it never worked) | Use `listReports` / `downloadStatsReport` / `downloadFinancialReport` from `@gpc-cli/core` |
| `REPORT_ACCESS_DENIED` from a reports call | Service account lacks the bulk-reports grant | Enable "View app information and download bulk reports (read-only)" in Play Console, then retry |
| Reports call returns 403 despite the grant | Token was minted without `devstorage.read_only` | Pass `scopes: [...DEFAULT_SCOPES, STORAGE_READ_ONLY_SCOPE]` to `resolveAuth` |

## Related skills

- **gpc-setup** — service account creation and auth configuration
- **gpc-plugin-development** — building plugins that use the SDK internally
- **gpc-troubleshooting** — interpreting API error codes

