npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@twentylabs/asa

v0.1.0

Published

Apple Ads from the terminal: read-only report pulls and approved change files, on top of the asc CLI.

Readme

asa

Apple Ads from the terminal, on top of asc (Rork's App Store Connect CLI). No runtime dependencies.

It does three jobs:

  • Profiles. Each Apple Ads ad account + promoted app is one profile, and any project can use any of them.
  • asa pull. Fetches the campaign, keyword and search-term reports for a date range into CSV files. Always read-only.
  • asa apply. Changes the account from a JSON change file of asc ads … commands.
    • By default it prints the exact asc commands and payloads, and the current state from the file's verify reads. Nothing is written.
    • --yes sends the writes, reads the state again, and saves before and after as evidence.

asa never talks to Apple itself. Every call goes through asc, with telemetry off and, outside apply --yes, asc's read-only mode on.

Install

Requires Node 20 or later and asc 5.8 or later (below 6.0).

brew install asc
npm i -g @twentylabs/asa
asa version

Apple Ads API credentials (once per ad account)

Apple Ads credentials are separate from App Store Connect API keys: an AuthKey_*.p8 does not work here.

  1. Create an EC P-256 key pair, outside any git repo:

    mkdir -p ~/.asc && umask 077
    openssl ecparam -genkey -name prime256v1 -noout -out ~/.asc/apple-ads-private-key.pem
    openssl ec -in ~/.asc/apple-ads-private-key.pem -pubout -out ~/.asc/apple-ads-public-key.pem
  2. Invite an API user. In Apple Ads → Account Settings → User Management, invite a new user with an API role:

    | Role | Use it when | | --- | --- | | API Account Read Only | You only pull reports and run doctor. Prefer this. Apple refuses every write, whatever runs with the key. | | API Campaign Manager | You need asa apply --yes. The key can then write, and asa's read-only mode is the only thing keeping pull, plan and doctor from writing. Do not use this key with asc ads by hand. |

  3. Upload the public key. Sign in as that API user → Account Settings → API, and paste the public key. Apple shows the client ID, team ID and key ID.

    • Uploading a new public key replaces the old one; the old key returns invalid_client straight away.
  4. Find the ad account ID, if you do not know it: export the four ASC_ADS_* values below, then run asc ads auth discover.

Profiles

asa auth login --profile acme \
  --client-id SEARCHADS.xxxx --key-id xxxx \
  --private-key ~/.asc/apple-ads-private-key.pem \
  --ad-account 1234567 --app 1234567890
asa doctor
  • --team-id defaults to the client ID, which is what Apple issues in practice.
  • --app is the App Store ID (adamId) of the promoted app. There is no default app or account.
  • --time-zone ORTZ|UTC sets the report time zone. The default, ORTZ, is the ad account's own, so days match the Apple Ads UI.
  • auth login checks the fields and that the key file exists. It does not call Apple; asa doctor does.

Profiles live in ~/.config/asa/config.json (owner-only; ASA_CONFIG or XDG_CONFIG_HOME move it). The file stores the key's path, never the key.

asa profiles              # list (* = default)
asa profiles use acme     # set the default
asa auth logout --profile acme

Choose a profile with --profile NAME, then ASA_PROFILE, then the default. Any field can also come from the environment or a .env file, so CI can run without a config file. Later layers win: profile < environment < --env-file < flags.

| Field | Environment / .env | Flag | | --- | --- | --- | | client ID | ASC_ADS_CLIENT_ID | | | team ID | ASC_ADS_TEAM_ID | | | key ID | ASC_ADS_KEY_ID | | | private key path | ASC_ADS_PRIVATE_KEY_PATH | | | ad account ID | ASC_ADS_AD_ACCOUNT_ID | --ad-account | | app ID | ASA_APP_ID (or ADAM_ID) | --app | | report time zone | ASA_TIME_ZONE | --time-zone |

The credential names are asc's own, so an existing asc .env works with --env-file FILE (or ASA_ENV_FILE). asa passes the resolved credentials to asc and bypasses asc's keychain profiles, so no other stored key can be picked up by accident.

Read

asa doctor                        # asc version, credentials, role, the app's campaigns
asa plan                          # range, output files, request bodies — no API calls
asa pull                          # the last complete Monday–Sunday week
asa pull --end 2026-09-27         # choose the last day
asa pull --days 14                # longer range (1–90 days)
asa pull --out-dir /tmp/x         # write elsewhere (or ASA_DATA_DIR); default ./data
asa pull --force                  # overwrite existing files
  • doctor checks that asc is installed and inside the supported range, validates the credentials, prints the API user's role on the ad account, and lists the campaigns promoting the app.
  • The default end date is the last complete Sunday: run on a Sunday, it takes the Sunday before, because the running week is not finished.

Output files

<out-dir>/YYYY-MM-DD-campaigns.csv, -keywords.csv and -search-terms.csv, named after the last day of the range. One row per campaign × country, keyword × country or search term × country, highest spend first:

| Column | Meaning | | --- | --- | | from, to | the range, inclusive | | campaignId, campaignName, adGroupId, keywordId, keyword, matchType, keywordStatus, bid, searchTermText, countryOrRegion, … | metadata, with the API's own field names; nested fields joined with dots | | impressions, taps, tapInstalls, totalInstalls, localSpend.amount, localSpend.currency | totals for the range | | ttr, cpt, cpi | taps/impressions, spend/taps, spend/totalInstalls; empty when the denominator is 0 |

The columns are not the Apple Ads UI export's. Do not mix the two in one table without renaming.

How the Platform API behaves (learned against a live account), and what pull does about it:

  • The campaign report rejects a campaignId filter, so it runs once for the whole ad account. pull keeps only the rows of the app's campaigns.
  • The keyword and search-term reports require a campaignId filter, so they run once per campaign.
  • The keyword report carries only the keyword's ID. pull adds the text, match type, ad group, bid and status from asc ads targeting-keywords find.
  • A null search term is the UI's (Low volume terms) row, and is written with that label.
  • A campaign with only Search Match has no keywords, so an empty keyword report is correct.

Change

A change file is the exact list of asc commands to send, plus the context a reviewer needs. It is reviewed, approved, applied, and committed together with its result.

{
  "asa": ">=0.1.0",
  "adAccount": "1234567",
  "title": "Pause the Generic campaign",
  "reason": "CPA 3x the account average for two weeks running",
  "steps": [
    { "args": ["ads", "campaigns", "pause", "--campaign", "1000000002"], "confirm": false }
  ],
  "verify": [
    { "args": ["ads", "campaigns", "find"],
      "body": { "filters": [{ "field": "id", "operator": "EQUALS", "value": 1000000002 }] } }
  ]
}
  • args is the asc command without asc, and always starts with ads. asc ads <command> --help shows each command and its payload schema.
  • body is the JSON payload; asa pipes it to asc as --file -.
  • asa adds --ad-account, --output json and, on writes, --confirm. A file that sets them itself is refused.
  • "confirm": false drops --confirm for a command that has no such flag or does not need it (for example campaigns pause).
  • verify runs read-only, before and after. Always include at least one read of the thing being changed.
  • adAccount (optional) must match the active profile, so a change written for one account cannot run against another.
  • asa (optional) is the version range of asa the file needs; an older asa refuses the file instead of misreading it. schemaVersion (default 1) guards the format itself.

Common commands: campaigns update / pause / resume, ad-groups update, targeting-keywords create-bulk / update-bulk, negative-keywords create-bulk / update-bulk. A template is in examples/.

Workflow

  1. Write YYYY-MM-DD-<what>.json.
  2. Dry run: asa apply 2026-10-02-pause.json. It prints each asc command exactly as it will run, the payloads, the file's SHA-256, and the current state from verify. Nothing is written.
  3. Approve that exact file. Approving the idea is not approving the payload; the SHA-256 identifies what was reviewed.
  4. Apply: asa apply 2026-10-02-pause.json --yes.
  5. Check the state after in the output and in the result file; do not trust a ✓ alone. Commit the change file and its result together.

Result files

--yes writes <name>.result.json next to the change file:

{ "change": "2026-10-02-pause.json", "sha256": "…", "adAccount": "1234567",
  "appliedAt": "2026-10-02T09:15:00.000Z",
  "before": [{ "args": [...], "response": {...} }],
  "steps":  [{ "args": [...], "response": {...} }],
  "after":  [{ "args": [...], "response": {...} }] }

If a step fails, apply stops, writes the partial result with an error field, and exits. Read it before running again: re-applying a file that has a result needs --force.

Safety model

| Rule | Why | | --- | --- | | pull, plan and doctor run every asc call with ASC_READ_ONLY=1, set in code and not overridable | asc refuses every write before it leaves the machine (exit 6); find and report queries, which are read-only POSTs, still run. | | plan makes no asc calls at all | Safe to run anywhere, without credentials. | | Only the steps of apply --yes run without read-only mode; its verify reads keep it | One write path, for one reviewed file. | | --read-only or ASA_READ_ONLY=1 refuses apply --yes before any asc call | Work on an account with no risk of changing it. Dry runs still work. | | ASC_TELEMETRY_DISABLED=1 on every call | asc enables telemetry by default. | | asc's keychain profiles are bypassed (ASC_ADS_BYPASS_KEYCHAIN=1) | Only the credentials asa resolved can be used. | | Change files may only run ads … commands, never ads auth, ads api (raw requests, which bypass every per-command guard) or ads v5 | Keeps a change file to campaign-management commands asc validates. | | delete steps are flagged in the dry run | Apple Ads has no undo. | | A change with a result file is refused without --force | Prevents applying the same change twice. | | asc must be inside the supported range (>=5.8.0 <6.0.0, declared in lib/asc.js) | A new major asc may change commands or flags. |

The strongest guard is still the role: with an API Account Read Only user, Apple itself refuses writes.

Exit codes

| Code | Meaning | | --- | --- | | 0 | OK | | 1 | Usage error or refusal | | 2 | asc failed (API error, bad credentials, unexpected output) | | 6 | Refused by read-only mode |

Development

npm test      # node --test; never calls Apple

The tests run the real CLI against a fake asc (test/fixtures/fake-asc), selected with ASA_ASC_BIN. To support a new asc release, test it, then widen SUPPORTED_ASC in lib/asc.js.

Releases: bump package.json, add the CHANGELOG entry, merge, then push a vX.Y.Z tag. The release workflow runs the tests and publishes to npm with trusted publishing (OIDC, no token).

License

MIT. asc is a separate project under its own MIT license; asa calls it as a program and includes none of its code.