Download the PHP package jcolombo/optmyzr-api-php without Composer
On this page you can find all versions of the php package jcolombo/optmyzr-api-php. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download jcolombo/optmyzr-api-php
More information about jcolombo/optmyzr-api-php
Files in jcolombo/optmyzr-api-php
Package optmyzr-api-php
Short Description PHP SDK / API client for the Optmyzr User Data API (v1) — PPC optimization suggestions, alerts, metrics, workouts and Blueprint tasks
License MIT
Homepage https://github.com/jcolombo/optmyzr-api-php
Informations about the package optmyzr-api-php
optmyzr-api-php
A PHP 8.1+ SDK / API client for the Optmyzr User Data API (v1).
Optmyzr is a PPC-management platform for Google Ads and Microsoft Advertising: optimization suggestions, alerts, quality-score tracking, workouts, and Blueprint task management. This library wraps the full published API surface — all nine documented endpoints — behind a typed entity layer, a fluent query builder, synthetic record identity, client-side rate limiting, optional caching, and raw escape hatches for anything Optmyzr adds later.
- Base URL:
https://dataaccess.optmyzr.com - Auth: a User Token, passed as a query-string parameter (see Authentication)
- Coverage: all 9 operations / 9 paths (see API coverage)
- The API is read-only. Every documented operation is a
GET; there are no write endpoints.
Two things to know before you start
1. The token travels in the query string, not a header. That is Optmyzr's design, not a choice this library made. It means the credential is part of every request URL, so it can reach access logs, proxies, and error trackers that this SDK has no control over. Inside its own boundary the SDK scrubs it everywhere — logs, exception messages, cache keys, cache files,
var_dump()output — see Credential handling. Outside that boundary, treat request URLs as secrets.2. This release is documentation-verified, not live-verified. Every endpoint, field, filter, and enum value here is transcribed from Optmyzr's published Swagger documentation and covered by mocked tests. None of it has been exercised against a live token yet. The synthetic identity keys in particular are inferences that may need recalibration against real data — see
docs/IDENTITY.md. A green test suite here means "internally consistent with the documentation", not "known to work against the live API".tests/validateexists to close that gap in one command the day a token is available.
Table of contents
- Requirements
- Installation
- Authentication
- Quick start
- Synthetic identity
- API coverage
- Filter-support matrix
- Server-side vs client-side filtering
- Account scoping
- Pagination
- Incremental sync
- Read-only enforcement
- Enums are advisory
- Credential handling
- Caching
- Rate limiting
- Errors and logging
- Configuration
- Extending the SDK (classMap)
- Raw escape hatches
- Known API quirks
- Examples
- Development and testing
- License
Requirements
- PHP 8.1+
ext-json- Guzzle
^7.8(installed via Composer) - adbario/php-dot-notation
^3.3 - An Optmyzr User Token
Installation
Authentication
The Optmyzr User Data API authenticates with a User Token sent as a query parameter:
Optmyzr's documentation says only: "You will need an API Token to access this API. Contact your Optmyzr Account Manager for an API token." There is no self-service token generation, no documented expiry, and no documented scope model — so a token cannot be assumed present in any environment, and this SDK never fabricates one.
You supply the token once, to connect(), and never handle it again:
Optmyzr::connect() is a singleton per token + base URL — calling it again with
the same token returns the same connection, with its warmed Guzzle client and
rate-limit state. It makes no network call.
Never commit your token. Load it from an environment variable or an untracked config file. The provided
.gitignorealready excludes.envandoptmyzrapi.config.json.
Quick start
Synthetic identity
The Optmyzr API returns no record identifiers. No endpoint returns one, none
accepts one, and there is no GET /{id} anywhere. That makes ordinary work —
caching a record, recognising it in a later fetch, storing it locally, diffing two
snapshots — impossible without inventing an identity scheme.
This SDK derives three values per record:
| Accessor | Derived from | Stable when a value changes? |
|---|---|---|
naturalKey() |
the key fields, normalised, pipe-joined | yes |
id() |
prefix_ + hash of the natural key |
yes |
fingerprint() |
hash over every field | no — moves when anything moves |
The split is the whole point. Hashing the entire row — the obvious approach —
produces an identifier that changes whenever any value changes, so a suggestion
whose changes count moves from 4 to 7 reads as a delete plus an insert rather
than an update. Hashing only the key fields keeps identity stable while
fingerprint() carries the "did anything move?" signal.
Per-resource keys, and the reasoning for each, are in
docs/IDENTITY.md. Two structural points:
accountIdalone is not unique. The same numeric id can exist on both ad platforms, so the platform is part of the key. That composite is modelled as anAccountKeyvalue object.- Old and current platform names fold together.
AdWordsandGoogle Adsproduce the same identity, as doBing AdsandMicrosoft Advertising. Without that fold, a vendor rename would change the id of every record in every account, and a routine sync would read as a complete delete-and-reinsert.
The keys are configuration, not code, because they are inferred from prose rather than observed in data:
A collision detector reports rows that collapse onto one id, which is the direct empirical check on those inferences:
API coverage
All 9 documented operations.
| Endpoint | Accessor | Resource | Paginated |
|---|---|---|---|
GET /UserData/V1/GetAPIList |
apiList(), supports() |
(bare string[]) |
— |
GET /UserData/V1/Accounts |
accounts() |
Account |
no |
GET /UserData/V1/Metrics |
metrics() |
Metrics |
no |
GET /UserData/V1/ActiveAlerts |
alerts() |
ActiveAlert |
no |
GET /UserData/V1/OptimizationSuggestions |
suggestions() |
OptimizationSuggestion |
no |
GET /UserData/V1/OptimizationHistory |
history() |
OptimizationHistory |
no |
GET /UserData/V1/Workouts |
workouts() |
Workout |
no |
GET /UserData/V1/WorkoutHistory |
workoutHistory() |
WorkoutHistory |
no |
GET /UserData/V1/BlueprintTasks |
blueprintTasks() |
BlueprintTask |
yes |
Full field-by-field reference: docs/API-REFERENCE.md.
Filter-support matrix
Reproduced here rather than only linked, because it is the single fact you most need and least can guess. Optmyzr's filter support is irregular — four endpoints are missing a filter you would expect them to have.
| Endpoint | accountId |
accountType |
emailId |
toolName |
date window | status |
completedStatus |
page |
|---|---|---|---|---|---|---|---|---|
| GetAPIList | — | — | — | — | — | — | — | — |
| Accounts | — | — | ✓ | — | — | — | — | — |
| Metrics | ✓ | ✓ | ✓ | — | — | — | — | — |
| ActiveAlerts | ✓ | ✓ | ✓ | — | — | — | — | — |
| OptimizationSuggestions | ✓ | ✓ | ✓ | ✓ | — | — | — | — |
| OptimizationHistory | ✓ | ✓ | ✓ | ✓ | ✓ startDate/endDate |
— | — | — |
| Workouts | — | — | — | — | — | — | — | — |
| WorkoutHistory | ✓ | — | ✓ | — | — | — | ✓ | — |
| BlueprintTasks | ✓ | — | ✓ | — | ✓ dueDateStart/dueDateEnd |
✓ | — | ✓ |
The bolded gaps are the traps:
Accountshas noaccountIdfilter even though it returns accounts.Workoutsaccepts nothing but the token — workouts are token-scoped, not account-scoped, and carry no account fields at all.WorkoutHistoryandBlueprintTaskshave noaccountTypefilter even thoughWorkoutHistoryreturns one.
The SDK encodes this in its API surface: each collection composes only the filter
methods its endpoint supports, so $optmyzr->accounts()->status('Open') is a call-site
error a static analyser catches, not a parameter the server silently ignores.
Calling param() with an unsupported name throws UnsupportedFilterException.
Server-side vs client-side filtering
Because the matrix is irregular, the SDK is explicit about where each filter runs.
where() routes to a server parameter when one exists and falls back to a
client-side comparison when it does not. explain() always tells you which
happened — reach for it first when a result set is wider or narrower than expected.
Other client-side operations (the API offers no server equivalent for any of them):
Account scoping
The API denormalises accountType + accountId into seven of its nine resources
and declares no relationships at all. AccountContext reconstructs the one join
that matters:
You do not have to remember which endpoints accept accountType — that is the
asymmetry this class exists to absorb. There is deliberately no workouts()
accessor, because workouts have no account dimension.
Grouping works the same way:
An account id with no platform is legitimate — the API accepts accountId alone,
and BlueprintTask rows carry no platform at all:
Pagination
Only BlueprintTasks paginates: 1-based page, a fixed size of 1000, and an
out-of-range page returns 200 with zero entries rather than an error.
fetchAll() stops on the first empty or short page, on a page that adds no new
records (a guard against a server ignoring the page parameter), or at a hard page
cap. Calling it on any other endpoint issues one request and warns, rather than
silently pretending to page.
For the eight unpaginated endpoints, Optmyzr documents nothing about whether they cap their result sets. The SDK exposes the raw row count and warns on a suspiciously round one:
Incremental sync
id() + fingerprint() give a real delta against an API with no identifiers. Your
application persists two short strings per record — nothing else.
$diff->counts() gives ['added' => n, 'changed' => n, 'removed' => n, 'unchanged' => n],
and $diff->upserts() is added + changed together.
Read-only enforcement
The API has no write endpoints, so the write methods exist and throw:
They throw rather than being absent because Call to undefined method save() reads
like a broken SDK, while this message documents the API. create(), update(),
save(), delete(), and fetch($id) all behave this way.
Enums are advisory
Optmyzr describes its enumerated fields in prose and never lists their values. Worse, its documentation names the ad platforms "AdWords" and "Bing Ads" — names Google and Microsoft retired years ago — so the live API may well return something else.
Every enum here is therefore advisory, and the raw string is authoritative:
Nothing throws on an unknown value, and an unrecognised value never blocks
hydration. Enums exist for accountType, associationStatus, alertType, and
workout type.
There is deliberately no enum for BlueprintTask.status or
OptimizationHistory.status: Optmyzr documents neither vocabulary, so an enum
would present a guess as an API contract. Read the real values from live data:
Credential handling
Because the token is a URL parameter, containment is structural rather than incidental:
- The token is merged in only at the transport call. It is not stored on
RequestAbstraction, so the object that gets serialized into the cache, written to logs, and printed in diagnostics never holds it. - A caller-supplied
tokenin a raw-request query is stripped, so it cannot override the real one (Guzzle's shallow option merge would otherwise make that an auth bypass). Redactorscrubs by pattern (token=…in any URL) and by registered value, which is what catches Guzzle exception messages that embed the effective URI in prose.- Cache keys namespace on a hash of the token, so two tokens can never read each other's cached data and the credential never reaches a filename.
connectionName()reportsOptmyzrApi-***bc12— the last four characters only.
This is covered by explicit tests asserting the token appears in no log line, no cache key, no cache file, no serialized request, and no error message.
Caching
Off by default. Enable it by pointing path.cache somewhere writable:
This API serves aggregated dashboard data rather than transactional records, so a 15-minute default window is safe.
Host applications can replace the backend entirely:
Since the API is read-only, nothing the SDK does can stale its own cache. For out-of-band changes — someone applied an optimization in Optmyzr's own UI — invalidate explicitly:
Rate limiting
Optmyzr publishes no rate limits. Its documentation declares only 200 OK for
every operation and says nothing about throttling, quotas, or 429 behaviour.
The shipped defaults — roughly 4 requests/second, 120/minute, 3000/hour — are therefore a deliberately conservative guess, not a documented ceiling. Do not read them as API behaviour. Raise them once you have observed the real limits:
The limiter is nonetheless 429-aware: a Retry-After header (numeric seconds or
HTTP-date) is honoured ahead of exponential backoff and capped by
rateLimit.maxRetryAfterSeconds so a hostile value cannot park the process.
Errors and logging
The SDK does not throw for API-level failures. A failed request yields a
RequestResponse with success === false, and errors dispatch through configurable
handlers:
Because Optmyzr documents no error responses at all, the error parser tries the
shapes an ASP.NET service actually produces (Message, ExceptionMessage,
error, errors, …) before falling back to the status line, and an HTML error page
is detected before JSON decoding so the warning names the real problem instead of
reporting a syntax error at offset 0.
Configuration
Defaults ship in default.optmyzrapi.config.json; override any subset:
| Block | Keys |
|---|---|
connection |
url, pathPrefix, verify, timeout |
request |
dateFormat, epochMillisThreshold, pageSize, maxFetchAllPages |
path |
cache, logs |
enabled |
cache, logging |
cache |
lifespan |
rateLimit |
enabled, minDelayMs, safetyBuffer, maxRetries, retryDelayMs, maxRetryAfterSeconds, perMinute, perHour |
log |
connections, requests |
devMode |
extra warnings, identity-collision reporting |
identity |
algo, normalizeAccountType, keys.<entity> |
error |
enabled, handlers, logFilename, triggerPhpErrors |
classMap |
defaultCollection, entity.<key> |
Note on list values: a list override replaces wholesale rather than merging.
Overriding identity.keys.account with ["accountId"] means exactly that one
field — which is the behaviour you want, since a merge would silently produce a
longer key and change every generated id.
devMode is worth enabling the first time you point this at real data: it turns
on identity-collision reporting, envelope-shape warnings, and unknown-enum warnings.
Extending the SDK (classMap)
Every resource and collection is resolved through the classMap config, so a host
application can substitute its own subclasses without touching the SDK:
Collections work the same way (EntityMap::overload('accounts', MyAccountCollection::class, 'collection')).
Raw escape hatches
For anything Optmyzr adds after this release:
Both flow through the full pipeline — auth, caching, rate limiting, error mapping —
and $path is used verbatim, so it can reach outside UserData/V1.
Known API quirks
Each is handled, and each handler has a test. Full detail with reasoning in
docs/API-REFERENCE.md; design decisions in
OVERRIDES.md.
| # | Quirk | Handling |
|---|---|---|
| G1 | Date format is self-contradictory: prose says mm-dd-yy, the example is 02-17-2018 |
m-d-Y (the example wins), configurable |
| G2 | timestamp is a date-time string on OptimizationHistory, an int64 epoch on WorkoutHistory |
per-resource types, never a global field-name rule |
| G3 | Epoch unit (seconds vs milliseconds) never stated | magnitude sniff at 1e12, configurable |
| G4 | No error responses documented at all | ASP.NET body shapes tried, HTML detected before decoding |
| G5 | No rate limits documented | conservative defaults, flagged as a guess |
| G6 | Enum values described in prose only, using retired platform names | raw string authoritative, enums advisory |
| G7 | Credential travels in the query string | structural containment + redaction |
| G8 | Field casing inconsistent (timeStamp/timestamp, updatetime/updatedTime) |
property aliases |
| G9 | Pagination documented for one endpoint only | fetchAll() only there; raw count exposed elsewhere |
| G10 | GetAPIList values not enumerated |
case- and punctuation-insensitive matching |
| G11 | XML is an advertised response type | Accept: application/json always sent |
| G12 | accountId without accountType unspecified |
sent exactly as given; no platform invented |
Examples
Runnable scripts in examples/:
| File | Shows |
|---|---|
01-basic-connection.php |
connect, apiList(), supports(), disconnect |
02-accounts-and-metrics.php |
account inventory, quality scores, raw vs enum accessors |
03-suggestions-and-alerts.php |
per-account pending work via AccountContext |
04-optimization-history.php |
date windows, attribution by user, totals |
05-blueprint-tasks-pagination.php |
fetchAll(), open(), overdue(), page counts |
06-identity-and-diffing.php |
id()/fingerprint(), snapshots, CollectionDiff |
07-configuration-and-caching.php |
config overlays, cache bridging, explain(), collisions |
Each reads OPTMYZR_API_TOKEN from the environment and exits cleanly when it is
absent.
Development and testing
The suite is fully mocked (Guzzle MockHandler) and passes with zero
credentials configured. It proves the SDK is internally consistent with Optmyzr's
documentation — that the right URL is built, the right parameters are sent, the
documented envelope is parsed, identity is stable, and the token never leaks. It
does not prove the documentation matches reality.
Validating against the live API
Without a token it prints instructions and exits 0 (skipped, not failed), so CI
stays green.
--verbose and --identity are the point: they answer the open questions this
build could not — the real accountType strings, the real status vocabularies,
the epoch unit, and above all whether the inferred identity keys are actually
unique.
License
MIT — see LICENSE.
All versions of optmyzr-api-php with dependencies
ext-json Version *
guzzlehttp/guzzle Version ^7.8
adbario/php-dot-notation Version ^3.3