Skip to content

Commit 3754328

Browse files
docs: correct verified URL output and apply npm/python review feedback
Fixes found by executing the snippets, plus the review comments from the reference PRs (cloudinary_npm#742, pycloudinary#449) that still applied here. Corrections caught by running the code: - transform-and-deliver-video.md showed a /v1/ segment on a non-nested public_id in four places. The image page documents the rule correctly — /v1/ appears only when the public_id contains a slash — so the two pages contradicted each other. All four now match generated output exactly. - getenv() returns false when unset and fromCloudinaryUrl() requires a string, so the no-credentials path raised a TypeError instead of the SDK's ConfigurationException. troubleshoot-errors.php leaked a PHP signature dump; it now matches the other eleven examples. - Documented URLs omitted the ?_a= analytics parameter the SDK adds by default, so actual output did not match the page. Documented what it is and how to disable it. Review feedback applied: - Exception structure: ApiError is a bare Exception, so getCode() is 0 and there is no structured error body. Added the HTTP status column and a section stating that the exception class is the only machine-readable signal. - 423 (Processing) is unmapped in BaseApiClient and surfaces as GeneralError carrying the raw response body, so it cannot be caught by type. Documented with a message-matching retry. - Moderation is not access control: whether pending, approved, or rejected assets are deliverable is product-environment configurable, and the Media Library and CDN are separate surfaces. - Moderation add-ons need account enablement and sometimes provider terms acceptance — neither of which an agent can do. - Upload size limits are per product environment; raising chunk_size does not raise the ceiling. Added mitigations and an upgrade path. - Prefer asset_id for stored references; links to the search expression and transformation references; concrete one-hour signature lifetime; signed parameters must match what the client posts. Also tagged two illustrative blocks as plain rather than php: conflicting use statements and a bare class name are not runnable snippets and would otherwise be false positives for a snippet parser. Verified: 56 php blocks across docs/ parse clean, examples lint with zero phpcs errors, all twelve exit 1 with a readable message when credentials are absent, unit suite 375/375, all internal links and anchors resolve. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent ddf6548 commit 3754328

13 files changed

Lines changed: 159 additions & 36 deletions

‎docs/configure-cloudinary.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ first and override the property:
9797
```php
9898
use Cloudinary\Configuration\Configuration;
9999

100-
$configuration = Configuration::fromCloudinaryUrl(getenv('CLOUDINARY_URL'));
100+
// `?: ''` matters: getenv() returns false when unset, and fromCloudinaryUrl() requires a
101+
// string, so without it an unset variable raises a TypeError instead of the SDK's
102+
// ConfigurationException.
103+
$configuration = Configuration::fromCloudinaryUrl(getenv('CLOUDINARY_URL') ?: '');
101104
$configuration->logging->enabled = false;
102105

103106
$cloudinary = new Cloudinary($configuration);

‎docs/import-and-call.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ $result->getArrayCopy(); // array, for json_encode() or var_dump()
5858
The classmap autoloader means a file's path is not always its namespace. The signing
5959
helper lives at `src/Api/Utils/ApiUtils.php` but is namespaced `Cloudinary\Api`:
6060

61-
```php
61+
```
6262
use Cloudinary\Api\ApiUtils; // correct
6363
use Cloudinary\Api\Utils\ApiUtils; // Error: Class not found
6464
```

‎docs/moderate-upload.md‎

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,18 @@ the asset.
6060
| `moderation_status` | Same status, flattened. Admin API responses only. |
6161
| `public_id` | Needed to approve or reject later. |
6262

63-
## A pending asset is still delivered
63+
## What `pending` means depends on your product environment
6464

65-
Queuing for moderation does not hide the asset. Until you act on it, its delivery URL
66-
works. If content must not be visible before review, upload it as
65+
`pending` is a moderation state, not a guaranteed access state. Whether a pending asset is
66+
publicly deliverable is governed by a product-environment setting, so it differs between
67+
accounts — on many it **is** deliverable while awaiting review, which surprises people.
68+
69+
Do not rely on moderation as an access-control mechanism in either direction. Verify the
70+
behaviour on your own environment, and if content must not be reachable before review,
71+
enforce that explicitly: upload as
6772
[`'type' => 'private'`](https://cloudinary.com/documentation/upload_images.md) or into a
68-
restricted folder, and publish after approval.
73+
restricted folder, then publish after approval. The same applies to what the asset looks
74+
like in the Media Library versus on the CDN — those are separate surfaces.
6975

7076
## Approving and rejecting
7177

@@ -81,7 +87,9 @@ $cloudinary->adminApi()->update('docs/needs-review', [
8187
]);
8288
```
8389

84-
Rejected assets are moved out of delivery; approved ones stay.
90+
How each state maps to delivery is again environment-configurable — commonly `rejected`
91+
is taken out of delivery and `approved` stays, but confirm it on your environment rather
92+
than assuming it.
8593

8694
## Listing the queue
8795

@@ -98,8 +106,13 @@ foreach ($pending['resources'] as $asset) {
98106
## Automatic moderation is an add-on
99107

100108
`'moderation' => 'manual'` needs no add-on. Automatic kinds — AI-based visual moderation,
101-
perceptual duplicate detection — must be enabled on your account first. Without a
102-
subscription the call fails at request time.
109+
perceptual duplicate detection — must be enabled on your account first, which the account
110+
owner does in the Console; some add-ons also require accepting the provider's terms of
111+
service before the first call will succeed. An agent cannot do either step: if the call
112+
fails for this reason, tell the user what to enable rather than retrying.
113+
114+
The available kinds and their provider-specific response shapes are listed in
115+
[moderation add-ons](https://cloudinary.com/documentation/moderation_addons.md).
103116

104117
Because the verdict is model output, assert on the **shape** of the response — that a
105118
status exists and is one of the expected values — not on a specific verdict for a given
@@ -119,3 +132,5 @@ image.
119132
- [Upload an image](upload-image.md)
120133
- [Search and manage assets](search-and-manage-assets.md)
121134
- [What this SDK does and does not do](platform-capabilities.md)
135+
- [Moderation add-ons](https://cloudinary.com/documentation/moderation_addons.md) — the
136+
kinds beyond `manual`, and what each returns.

‎docs/platform-capabilities.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Any documentation URL returns Markdown if you append `.md`.
4848
| Search by expression | `$cloudinary->searchApi()` | [Search and manage assets](search-and-manage-assets.md) |
4949
| List, rename, delete, tag | `$cloudinary->adminApi()`, `$cloudinary->uploadApi()` | [Search and manage assets](search-and-manage-assets.md) |
5050
| Find visually similar assets | `$cloudinary->adminApi()->visualSearch()` | [Search and manage assets](search-and-manage-assets.md) |
51-
| Attach structured fields | `$cloudinary->adminApi()->addMetadataField()` | [Use structured metadata](use-structured-metadata.md) |
51+
| Track typed, validated business data per asset — owner, campaign, licence expiry — and search on it | `$cloudinary->adminApi()->addMetadataField()` | [Use structured metadata](use-structured-metadata.md) |
5252
| Bundle assets into an archive | `$cloudinary->uploadApi()->createArchive()` | [Search and manage assets](search-and-manage-assets.md) |
5353

5454
### Analyze and moderate

‎docs/search-and-manage-assets.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ Runnable version: [`examples/search-and-manage-assets.php`](../examples/search-a
5151
| `created_at>2026-01-01` | Uploaded since that date |
5252
| `context.alt:shirt*` | Context field prefix match |
5353

54-
Combine with `AND`, `OR`, `NOT`.
54+
Combine with `AND`, `OR`, `NOT`. Full grammar:
55+
[search expressions reference](https://cloudinary.com/documentation/search_expressions.md).
5556

5657
### Leading wildcards are rejected
5758

‎docs/sign-browser-upload.md‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
Letting a browser or mobile app upload straight to Cloudinary without routing the bytes
66
through your server — and without ever exposing your `api_secret`.
77

8-
Your server signs a short-lived set of parameters; the client posts the file plus that
9-
signature directly to Cloudinary.
8+
Your server signs a set of parameters; the client posts the file plus that signature
9+
directly to Cloudinary. The signature covers a `timestamp` and is accepted for
10+
**one hour** from it — generate one per upload attempt rather than caching.
1011

1112
## Complete flow
1213

@@ -23,7 +24,10 @@ use Cloudinary\Cloudinary;
2324
$cloudinary = new Cloudinary();
2425
$cloud = $cloudinary->configuration->cloud;
2526

26-
// Only the parameters the client is allowed to send. Every one of these is signed.
27+
// Only the parameters the client is allowed to send. Every entry in this array is
28+
// covered by the signature, and every signed entry must also be POSTed by the client —
29+
// the two sets have to match exactly. To let the client set something (a tag, a folder),
30+
// add it here; anything absent here cannot be sent.
2731
$params = [
2832
'timestamp' => time(),
2933
'folder' => 'user-uploads',
@@ -91,7 +95,7 @@ Invalid Signature <hash>. String to sign - 'folder=user-uploads&timestamp=178758
9195

9296
## Signatures are short-lived
9397

94-
`timestamp` is part of the signature and Cloudinary rejects stale ones (about an hour).
98+
`timestamp` is part of the signature and Cloudinary rejects one older than an hour.
9599
Generate a signature per upload attempt; do not cache or reuse them.
96100

97101
## Alternative: unsigned uploads

‎docs/transform-and-deliver-image.md‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,21 @@ echo $url, PHP_EOL;
3333
Output:
3434

3535
```
36-
https://res.cloudinary.com/<cloud_name>/image/upload/c_fill,g_auto,h_400,w_400/f_auto/q_auto/sample
36+
https://res.cloudinary.com/<cloud_name>/image/upload/c_fill,g_auto,h_400,w_400/f_auto/q_auto/sample?_a=BAAHWXGY
3737
```
3838

3939
Runnable version: [`examples/transform-and-deliver-image.php`](../examples/transform-and-deliver-image.php).
4040

41+
### The `?_a=` suffix
42+
43+
Generated URLs carry an `_a` query parameter — anonymous SDK-version telemetry, no
44+
account or asset data. It does not affect delivery or caching. The examples below omit it
45+
for readability; real output always includes it. To turn it off:
46+
47+
```php
48+
$cloudinary = new Cloudinary('cloudinary://key:secret@cloud?analytics=false');
49+
```
50+
4151
## Result fields to keep
4252

4353
The builder is not a response object — it produces a string. Cast it explicitly when you
@@ -128,3 +138,8 @@ This is expected and the URL resolves correctly. To emit a real version, pass th
128138
- [Transform and deliver a video](transform-and-deliver-video.md)
129139
- [Upload an image](upload-image.md)
130140
- [Troubleshoot errors](troubleshoot-errors.md)
141+
- [Transformation reference](https://cloudinary.com/documentation/transformation_reference.md)
142+
— every parameter, with the URL syntax each one produces.
143+
- [Transformation builder skill](https://cloudinary.com/documentation/cloudinary_llm_mcp.md)
144+
— install it (`npx skills add cloudinary-devs/skills`) rather than guessing at
145+
transformation chains.

‎docs/transform-and-deliver-video.md‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,15 @@ echo $url, PHP_EOL;
3131
Output:
3232

3333
```
34-
https://res.cloudinary.com/<cloud_name>/video/upload/c_fill,h_360,w_640/f_auto/q_auto/v1/my-video
34+
https://res.cloudinary.com/<cloud_name>/video/upload/c_fill,h_360,w_640/f_auto/q_auto/my-video?_a=BAAHWXGY
3535
```
3636

3737
Runnable version: [`examples/transform-and-deliver-video.php`](../examples/transform-and-deliver-video.php).
3838

39+
The trailing `?_a=` is anonymous SDK-version telemetry, present on every generated URL.
40+
See [Transform and deliver an image](transform-and-deliver-image.md#the-_a-suffix) for
41+
how to disable it. The path examples below omit it for readability.
42+
3943
## Result fields to keep
4044

4145
`$cloudinary->video()` builds a string, like the image builder. Cast explicitly outside
@@ -58,15 +62,15 @@ use Cloudinary\Transformation\VideoEdit;
5862

5963
// First five seconds.
6064
$cloudinary->video('my-video')->videoEdit(VideoEdit::trim()->startOffset(0)->endOffset(5));
61-
// -> /video/upload/eo_5,so_0/v1/my-video
65+
// -> /video/upload/eo_5,so_0/my-video
6266

6367
// Quieter audio.
6468
$cloudinary->video('my-video')->videoEdit(VideoEdit::volume(-20));
65-
// -> /video/upload/e_volume:-20/v1/my-video
69+
// -> /video/upload/e_volume:-20/my-video
6670

6771
// Auto-generated highlight preview.
6872
$cloudinary->video('my-video')->videoEdit(VideoEdit::preview(5));
69-
// -> /video/upload/e_preview:duration_5/v1/my-video
73+
// -> /video/upload/e_preview:duration_5/my-video
7074
```
7175

7276
## Generating a `<video>` tag

‎docs/troubleshoot-errors.md‎

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,69 @@ account, or the input.
99

1010
All API failures extend `Cloudinary\Api\Exception\ApiError`:
1111

12-
| Exception | Typical cause |
13-
|---|---|
14-
| `BadRequest` | Malformed input — wrong `resource_type`, bad search expression, undefined metadata key. |
15-
| `AuthorizationRequired` | Bad or mismatched credentials; a feature not enabled for the account. |
16-
| `NotFound` | No asset with that `public_id` and `resource_type`. |
17-
| `AlreadyExists` | The identifier is taken. |
18-
| `NotAllowed` | The operation is forbidden on this account or asset. |
19-
| `RateLimited` | Too many requests — or an unsubscribed add-on. |
20-
| `GeneralError` | Server-side failure with no more specific type. |
12+
| Exception | HTTP | Typical cause |
13+
|---|---|---|
14+
| `BadRequest` | 400 | Malformed input — wrong `resource_type`, bad search expression, undefined metadata key. |
15+
| `AuthorizationRequired` | 401 | Bad or mismatched credentials; a feature not enabled for the account. |
16+
| `NotAllowed` | 403 | The operation is forbidden on this account or asset. |
17+
| `NotFound` | 404 | No asset with that `public_id` and `resource_type`. |
18+
| `AlreadyExists` | 409 | The identifier is taken. |
19+
| `RateLimited` | 420, 429 | Too many requests — or an unsubscribed add-on. |
20+
| `GeneralError` | 500, anything unmapped | Server-side failure, or a status this SDK has no specific class for. |
2121

2222
Configuration problems throw from a **different namespace**:
2323

24-
```php
24+
```
2525
Cloudinary\Exception\ConfigurationException // not under Api\Exception
2626
```
2727

28+
### Only the message is preserved
29+
30+
This SDK constructs exceptions with the server's error message and nothing else. There is
31+
no status-code getter and no structured error body — `getCode()` returns `0`:
32+
33+
```php
34+
try {
35+
$cloudinary->adminApi()->asset('missing');
36+
} catch (ApiError $e) {
37+
$e->getMessage(); // 'Resource not found - missing'
38+
$e->getCode(); // 0 — not the HTTP status
39+
get_class($e); // Cloudinary\Api\Exception\NotFound — this is the signal
40+
}
41+
```
42+
43+
Branch on the **exception class**, not on a code parsed out of the message. The class is
44+
the only reliable machine-readable part.
45+
46+
### 423 and other unmapped statuses
47+
48+
Only the statuses in the table have their own class. Anything else — notably **423
49+
(asset still processing)** — becomes a `GeneralError` whose message is the raw response
50+
body rather than the parsed error text:
51+
52+
```
53+
GeneralError: Server returned unexpected status code - 423 - {"error":{"message":"..."}}
54+
```
55+
56+
So a 423 cannot be distinguished by type. If you need to retry on it, match the status in
57+
the message:
58+
59+
```php
60+
use Cloudinary\Api\Exception\GeneralError;
61+
62+
try {
63+
$cloudinary->adminApi()->asset('still-processing');
64+
} catch (GeneralError $e) {
65+
if (str_contains($e->getMessage(), 'status code - 423')) {
66+
// Asset is still being processed — back off and retry.
67+
}
68+
69+
throw $e;
70+
}
71+
```
72+
73+
Cloudinary platform status: [status.cloudinary.com](https://status.cloudinary.com).
74+
2875
## Catching them
2976

3077
```php
@@ -108,7 +155,9 @@ handler, so one failure can look like three. To handle errors yourself without t
108155
```php
109156
use Cloudinary\Configuration\Configuration;
110157

111-
$configuration = Configuration::fromCloudinaryUrl(getenv('CLOUDINARY_URL'));
158+
// getenv() returns false when unset; fromCloudinaryUrl() requires a string, so `?: ''`
159+
// yields the SDK's ConfigurationException instead of a raw PHP TypeError.
160+
$configuration = Configuration::fromCloudinaryUrl(getenv('CLOUDINARY_URL') ?: '');
112161
$configuration->logging->enabled = false;
113162

114163
$cloudinary = new Cloudinary($configuration);

‎docs/upload-image.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Runnable version: [`examples/upload-image.php`](../examples/upload-image.php).
4343
| Field | Why it matters |
4444
|---|---|
4545
| `public_id` | The delivery handle. Every transformation URL is built from it. |
46-
| `asset_id` | Immutable identifier. Survives renames — prefer it for lookups you store. |
46+
| `asset_id` | Immutable identifier. **Store this one.** Survives renames and moves; `public_id` does not. |
4747
| `secure_url` | Ready-to-use HTTPS delivery URL of the original. |
4848
| `version` | Cache-busting number; changes on every re-upload to the same `public_id`. |
4949
| `format`, `width`, `height`, `bytes` | What the server actually stored, after any incoming transformation. |

0 commit comments

Comments
 (0)