cloudinary 2.1.1 copy "cloudinary: ^2.1.1" to clipboard
cloudinary: ^2.1.1 copied to clipboard

[pending analysis]

Complete Cloudinary SDK for Dart and Flutter - upload, admin, search and signed delivery URLs, with no Flutter dependency.

cloudinary for Dart

cloudinary

Every Cloudinary endpoint, in one pure-Dart package that never asks your app to hold a secret it shouldn't.

pub package CI pub likes pub points contributors licence

93 API methods across Upload, Admin and Search  ·  430 tests  ·  2 runtime dependencies  ·  0 Flutter dependencies

Every signature, auth token and delivery URL is pinned by golden vectors derived from Cloudinary's own algorithm, so correctness here is measured rather than asserted. There is no benchmark: a client's speed is the network's. The method and test counts are checked by test/readme_test.dart.

Live demo  ·  Quick start  ·  Uploading  ·  Delivery URLs  ·  Admin  ·  Search  ·  Is this for you  ·  Migrating from 1.x  ·  API reference

Table of contents #

Overview #

This package talks to Cloudinary's Upload, Admin and Search APIs and builds signed delivery URLs. It is written in pure Dart with no Flutter dependency, so the same code runs in a Flutter app, a Dart backend, a CLI, and on the web.

Responses come back as typed models, failures throw typed exceptions, and every model also exposes the raw decoded map, so a field Cloudinary added last week is reachable today rather than after a release here.

Demo #

Try every feature in the live web demo: a delivery URL playground on Cloudinary's public demo cloud, a real unsigned upload to your own cloud, signatures, signed URLs, auth tokens and webhook verification computed with a throwaway secret, a search query builder, a tour of the Admin API, CLOUDINARY_URL parsing and the exception family. It is the example app built for the web; the same app runs on Android, iOS, macOS, Windows and Linux.

Quick start #

Prerequisites #

  • Dart SDK ^3.13.0 (Flutter 3.47 or newer bundles a compatible SDK).
  • A Cloudinary account. Your cloud name, API key and API secret are on the dashboard.
  • For client-side uploads, an unsigned upload preset, so the app needs no secret.

Install #

dependencies:
  cloudinary: ^2.0.0

Then dart pub get or flutter pub get, and import it:

import 'package:cloudinary/cloudinary.dart';

Create a client #

On a server, where the secret is safe:

final cloudinary = Cloudinary.signed(
  cloudName: 'your-cloud',
  apiKey: 'your-key',
  apiSecret: 'your-secret',
);

In a Flutter or web app, where it is not:

final cloudinary = Cloudinary.unsigned(cloudName: 'your-cloud');

You can also read the standard CLOUDINARY_URL environment variable:

final cloudinary = Cloudinary.fromEnvironment();

Call cloudinary.close() when you are done, unless you passed your own http.Client, in which case closing it is yours to do.

Uploading #

final result = await cloudinary.upload.upload(
  file: const CloudinaryFileSource.path('/path/to/photo.jpg'),
  folder: 'trips/2026',
  tags: ['holiday'],
  onProgress: (sent, total) => print('$sent / $total'),
);

print(result.secureUrl);

From a client app with an unsigned preset, and no credentials anywhere:

final result = await cloudinary.upload.unsignedUpload(
  file: CloudinaryFileSource.bytes(bytes, filename: 'photo.jpg'),
  uploadPreset: 'my_unsigned_preset',
);

A file can come from a path, from bytes, or from a URL Cloudinary fetches itself:

const CloudinaryFileSource.path('/path/to/photo.jpg');   // not available on the web
CloudinaryFileSource.bytes(bytes, filename: 'a.png');
const CloudinaryFileSource.url('https://example.com/a.png');

The rest of the Upload API is there too: explicit, rename, destroy, destroyByAssetId, addTag, removeTag, replaceTag, removeAllTags, addContext, removeAllContext, updateMetadata, explode, multi, generateSprite, text, createArchive, createZip and deleteByToken.

Delivery URLs #

URL building is pure and synchronous, so it is safe to call inside a widget build.

final url = cloudinary.url.image('trips/2026/photo.jpg')
    .transform(Transformation()
      ..width(600)
      ..height(400)
      ..crop(CropMode.fill)
      ..gravity(Gravity.auto)
      ..quality(Quality.auto)
      ..format(DeliveryFormat.auto))
    .build();

Chain several transformations when one stage feeds the next:

final url = cloudinary.url.image('photo.jpg')
    .transformChain(TransformationChain([
      Transformation()..width(600)..crop(CropMode.fill),
      Transformation()..effect(Effect.sepia),
    ]))
    .build();

Anything without a typed setter goes through raw, so you are never blocked:

Transformation()..raw('e_custom:42');

Sign a URL so nobody can edit the transformation, or attach a time-limited token:

cloudinary.url.image('private.jpg').signed().build();

cloudinary.url.image('private.jpg').authToken(
  const AuthToken(key: 'your-token-key', acl: '/image/*', duration: 3600),
).build();

Private CDN distributions, CNAMEs, CDN subdomain sharding, SEO suffixes, short URLs and version pinning are all configured once through UrlConfig.

Admin API #

Grouped the way Cloudinary's own documentation is, so things are where you expect:

await cloudinary.admin.account.ping();
await cloudinary.admin.account.usage();

final page = await cloudinary.admin.resources.list(maxResults: 50);
final more = await cloudinary.admin.resources.list(nextCursor: page.nextCursor);

await cloudinary.admin.folders.create('trips/2026');
await cloudinary.admin.tags.list(prefix: 'hol');
await cloudinary.admin.transformations.create(name: 'thumb', transformation: 'w_150,h_150,c_fill');
await cloudinary.admin.uploadPresets.create(name: 'mobile', unsigned: true);
await cloudinary.admin.metadataFields.create(
  externalId: 'photographer',
  label: 'Photographer',
  type: MetadataFieldType.string,
);

The ten groups are account, resources, folders, tags, transformations, uploadPresets, uploadMappings, streamingProfiles, metadataFields and metadataRules.

resources.deleteAll requires confirm: true. Cloudinary has no undo for it.

Search API #

final results = await cloudinary.search
    .expression('resource_type:image AND tags=holiday')
    .sortBy('created_at', SortDirection.desc)
    .aggregate('format')
    .maxResults(50)
    .execute();

for (final asset in results.resources) {
  print('${asset.publicId} ${asset.bytes}');
}

For a query you run repeatedly from clients, build a signed, cacheable search URL instead:

final url = cloudinary.search.expression('tags=holiday').toUrl(ttl: 300);

Keeping your API secret out of your app #

An API secret shipped in a mobile or web build is readable by anyone who downloads it. This package makes that hard to do by accident:

  • Cloudinary.signed throws on a web runtime unless you pass allowSecretOnWeb: true.
  • Guards are thrown exceptions, never assert, because Dart strips asserts from release builds. A guard that vanishes in production is not a guard.
  • The API secret never appears in a URL, and toString() redacts it.

When a client genuinely needs a signed operation, sign it on your server and hand the signature back:

class MySigner implements SignatureProvider {
  @override
  Future<RemoteSignature> sign(Map<String, dynamic> params) async {
    final res = await myBackend.post('/cloudinary/sign', params);
    return RemoteSignature(
      signature: res['signature'],
      timestamp: res['timestamp'],
      apiKey: res['api_key'],
    );
  }
}

final cloudinary = Cloudinary.unsigned(
  cloudName: 'your-cloud',
  signatureProvider: MySigner(),
);

Handling errors #

Failures throw. The hierarchy is sealed, so a switch over it is exhaustive and the analyzer tells you when a new case appears.

try {
  await cloudinary.upload.upload(file: source);
} on CloudinaryRateLimitException catch (e) {
  print('Rate limited, resets at ${e.resetAt}');
} on CloudinaryAuthException {
  print('Check your credentials');
} on CloudinaryApiException catch (e) {
  print('Cloudinary said ${e.statusCode}: ${e.message}');
} on CloudinaryTransportException catch (e) {
  print('Never reached Cloudinary: ${e.cause}');
}

Responses with status 420, 429, 502, 503 or 504 are retried automatically, honouring Retry-After and Cloudinary's X-FeatureRateLimit-Reset. A 500 is never retried, because the request may have partly succeeded, and for the same reason a POST or DELETE retries only on 420 and 429, where the server states it did not act. Configure or disable it with RetryPolicy.

Verifying webhooks #

final ok = verifyNotificationSignature(
  body: rawRequestBody,          // the raw body as received, not re-encoded
  timestamp: int.parse(headers['x-cld-timestamp']!),
  signature: headers['x-cld-signature']!,
  apiSecret: 'your-secret',
);

The comparison is constant-time and payloads older than two hours are rejected, which limits replay.

Before and after #

Version 1 returned a response object whether or not the call worked, so a failure looked like a success until you checked the right getter:

// v1
final response = await cloudinary.upload(file: path);
if (response.isSuccessful) {
  print(response.secureUrl);
} else {
  print(response.error);   // easy to forget, and silent when you do
}

Version 2 throws, and returns a typed model:

// v2
final result = await cloudinary.upload.upload(
  file: CloudinaryFileSource.path(path),
);
print(result.secureUrl);

MIGRATION.md maps every v1 call to its v2 equivalent.

Is this for you #

Use this package if you want one dependency that covers uploading, administration, search and delivery URLs, with typed errors and no Flutter dependency so your backend and your app can share it.

It fits particularly well if you are uploading from a client app, because the signature provider and the web guard are built for exactly that.

Skip it if you only need to build delivery URLs and never call an API. Cloudinary's own cloudinary_url_gen is purpose-built for that and has a richer typed transformation DSL. This package covers the common transformation parameters and gives you raw for the rest, which is the right trade when URL building is not the main thing you are doing.

Compared to #

cloudinary_url_gen is Cloudinary's official URL builder. Its transformation DSL is more expressive than this one: a named constructor per effect, per gravity mode, per qualifier. It does not call the Upload, Admin or Search APIs.

cloudinary_api is Cloudinary's official API client, and pairs with cloudinary_url_gen. If you prefer first-party packages and are happy taking two of them, that combination is a reasonable choice.

cloudinary_flutter adds Flutter widgets on top of the official packages. This package has no widgets and no Flutter dependency, which is what lets it run on a server.

This package is one dependency covering all three APIs plus URL building, with a sealed error hierarchy, a raw-map escape hatch on every model, and client-side secret handling as a first-class concern.

FAQ #

Does it work on the web? Yes. The package compiles to JavaScript, and that is verified rather than assumed. The one exception is CloudinaryFileSource.path, which needs a filesystem; use .bytes there and you get a clear exception rather than a mystery if you forget.

Why does Cloudinary.signed throw in my Flutter web build? Because an API secret in a browser bundle is readable by anyone who opens devtools. Use Cloudinary.unsigned with an upload preset, or a SignatureProvider. If you are certain the code never reaches a browser, pass allowSecretOnWeb: true.

Can I use my own HTTP client? Yes, pass any http.Client to the constructor. That is also how the test suite runs with no network. The package does not re-export package:http, so its types are not frozen into this API.

Cloudinary added a response field. Do I have to wait for a release? No. Every model exposes raw, the complete decoded response, so the new field is available immediately as result.raw['new_field'].

What do the numbers in the header mean? They are the things this package controls and can prove: how much of Cloudinary's API is covered, and how much of that is pinned by tests. Request time belongs to Cloudinary and the network, so coverage and correctness are what gets measured here.

Scope #

This package covers the Upload, Admin and Search APIs in full, plus delivery URL construction. The Provisioning API (sub-accounts, users, user groups and access keys) and the v2 Analysis API are not covered.

About Cloudinary #

Cloudinary is a media API for websites and mobile apps: it stores, transforms, optimises and delivers images and video through multiple CDNs.

Migrating from 1.x #

2.0 is a rewrite: failures throw typed exceptions instead of returning an error string, package:http replaces dio, the API is grouped under cloudinary.upload, admin, search and url, and CloudinaryFileSource replaces file and fileBytes. MIGRATION.md maps every 1.x call to its 2.x equivalent.

Contributing #

Contributions are welcome. Fork, branch and open a pull request - see CONTRIBUTING.md for the checks a PR has to pass, and note that every PR must bump the version in pubspec.yaml and add a matching CHANGELOG.md entry. Bugs and ideas go to Issues, questions to Discussions, and vulnerabilities follow SECURITY.md.

Contributors #

Thanks to everyone who has contributed to cloudinary.

Contributors

License #

MIT. See LICENSE.

Support the project #

cloudinary is MIT licensed and free to use, always. If it saves you writing the signing code twice, sponsorship is welcome.


GitHub Sponsors Ko-fi Buy Me a Coffee

Connect #

Nikhil Rajput

GitHub LinkedIn X Instagram Telegram Email

33
likes
0
points
743
downloads

Publisher

verified publishernixrajput.com

Weekly Downloads

Complete Cloudinary SDK for Dart and Flutter - upload, admin, search and signed delivery URLs, with no Flutter dependency.

Homepage
Repository (GitHub)
View/report issues

Topics

#cloudinary #cloudinary-sdk #cloudinary-api #media #cdn

Funding

Consider supporting this project:

ko-fi.com
www.buymeacoffee.com
github.com

License

(pending) (license)

Dependencies

crypto, http

More

Packages that depend on cloudinary