plycdn Storage — S3 and Azure compatibility

Audience: developers and administrators who want to use the tools they already have — the AWS CLI, boto3 and the other AWS SDKs, rclone, s3cmd, Cyberduck, the Azure CLI and the Azure Storage SDKs — with plycdn buckets.

plycdn speaks two storage protocols beside its own API: the S3 API and the Azure Blob API. They reach the same buckets and the same files as the Storage guide describes. A file written with the AWS CLI is an ordinary plycdn object: it counts towards your storage, follows the bucket's upload rules, appears in the dashboard and in GET /buckets/{bucketId}/objects, and can be delivered through a signed link like any other. A file uploaded through the dashboard can be downloaded with rclone, and its ETag verifies.

Use these protocols to move files in and out: migrations, backups, sync jobs, build pipelines, editors' desktop tools. To show files to viewers, use delivery links on plycdn.net (Storage guide, Delivering); the storage protocols are API access, not delivery (see "Usage and pricing").

Only a subset of each protocol is supported. Everything supported is listed below; everything else is refused with a clear NotImplemented answer rather than half-working.

1Endpoints

Every host is on plycdn.net, and every one of them is served in a region:

Protocol Address Use
S3, path style https://s3.{label}.plycdn.net/{bucket}/{key} Any S3 tool
S3, virtual-host style https://{bucket}.s3.{label}.plycdn.net/{key} The AWS SDKs' default, and what rclone, s3cmd and Cyberduck prefer
Azure Blob, regional https://{account}.blob.{label}.plycdn.net Direct to the region. Recommended
Azure Blob, global https://{account}.blob.plycdn.net Your storage account's home region; requests for a container in another region are forwarded there

{label} is the region's short host label; each region has its own (in-mum for mumbai). It is not the region code (mumbai), which you still use in the API, in billing and as the signing region below; only the host names carry the label, and a host built from the code does not exist. Regions are added over time, so don't keep a list of them in your code or configuration: read the current list, with each region's hosts, from GET /regions. Each item carries an endpoints object with the hosts as patterns:

GET /api/content/v1/regions
{ "items": [
    { "code": "mumbai", "name": "Mumbai, India", "strictCapable": true, "country": "IN",
      "endpoints": {
        "s3": "https://s3.in-mum.plycdn.net",
        "s3VirtualHost": "https://{bucket}.s3.in-mum.plycdn.net",
        "blob": "https://{account}.blob.in-mum.plycdn.net",
        "blobGlobal": "https://{account}.blob.plycdn.net",
        "upload": "https://in-mum.upload.plycdn.com" } }
  ] }

The response lists the regions currently open for new buckets, the same for everyone; only the first is shown here. Replace {bucket} or {account} with your bucket name or storage account name. The dashboard shows the same hosts, filled in, on each bucket's Connect with S3 tools and Connect with Azure tools panels.

A bucket lives in one region, and its S3 and Azure hosts are that region's. Every example in this guide uses mumbai; use your bucket's region instead.

S3: the region your tool signs with

The signing region is the region code (mumbai). Set it wherever your tool asks for a "region". Some tools sign their first call (usually the bucket list) with us-east-1; that is accepted too, as is auto. It changes nothing about security: the signature still needs your secret, and it covers the host name, so a request signed for one region's host cannot be replayed at another.

Any other signing region is refused with AuthorizationHeaderMalformed.

S3: a bucket in another region

A request for a bucket at another region's host is answered 301 PermanentRedirect, with the bucket's region in the x-amz-bucket-region header and the right endpoint in the XML body, as S3 does. It is not forwarded. The AWS SDKs, the AWS CLI and rclone either follow it or tell you which region to use; the fix is to point the tool at the bucket's region.

ListBuckets is the exception: at any region's host it lists every bucket your credential can reach, in every region, each with its BucketRegion.

Azure: storage account, containers and the two hosts

An Azure client addresses a storage account. On plycdn, the storage account is your organization, under a name you choose once:

  • 3 to 24 lower-case letters and digits (Azure's own rule, so every tool accepts it), unique across plycdn, and fixed once chosen. A few names are reserved (our own service names and every region code and host label).
  • It has a home region, chosen with the name, which you can change later.
  • Each sub-organization is its own organization, so it has its own storage account, name and keys.

Choose the name in the dashboard (Developers → Storage credentials → Enable Azure access), or from your backend with POST /storage-account (API reference). Creating the account or changing its home region from code needs an admin API key (a standard key is refused with admin_key_required); reading it works with either. A container is a bucket: its container name is the bucket name.

  • {account}.blob.{label}.plycdn.net reaches the containers in that region directly. Use it: it is the one the dashboard shows for each bucket.
  • {account}.blob.plycdn.net goes to your home region. A request for a container in another region is forwarded to that region for you, because Azure has no redirect a client follows for writes. It works for every operation, but takes one more hop than the regional host. A request the global host cannot forward unchanged — a blob name with . or .. path segments or raw (not percent-encoded) non-ASCII bytes, or a query that would be re-encoded on the way (even when only the query differs) — is refused there with InvalidResourceName, naming the account's regional host; send it to the regional host, which takes it as received.

Path-style Azure addressing (https://host/{account}/{container}, as local emulators use) is not supported.

Only HTTPS

Plain HTTP is refused. Every response that carries a file also carries Content-Security-Policy: sandbox and X-Content-Type-Options: nosniff, so an HTML file opened from a storage address runs no script. The addresses are on plycdn.net, never on plycdn.com, where the dashboard lives.

2Credentials

S3 and Azure tools sign their requests with a storage credential. It is a separate kind of key from your API key, and it only works on the storage protocols.

Storage credentials are managed by people, in the dashboard only: Developers → Storage credentials. There is no API and no SDK method that creates, lists, rotates or revokes one, and an API key — admin keys included — is refused on those routes with dashboard_session_required.

Kind What you get Signs with
S3 An access key ID (PLY and 17 more characters) and a 40-character secret access key AWS Signature Version 4: headers, presigned URLs, and signed or unsigned streaming uploads with trailers
Azure An account key (88 characters, base64) in slot key1 or key2 of your storage account Shared Key, and SAS tokens signed with that key

An Azure credential needs the storage account to exist first (see Endpoints).

Permissions and scope

Permission Allows
read Listing, downloading and reading properties: GetObject, HeadObject, the listings, ListParts, ListMultipartUploads, Get Block List, and reading the source of a copy
write Uploading: PutObject, multipart uploads, Put Blob, Put Block, Put Block List, the destination of a copy, and aborting an upload
delete DeleteObject, DeleteObjects, Delete Blob
manage Creating and deleting buckets from your tools (CreateBucket, DeleteBucket, Create Container, Delete Container). Off by default
  • A credential reaches all buckets of its organization, including buckets created later, or only the buckets you choose. manage can be given only to an all-buckets credential.
  • A credential belongs to one organization and reaches only that organization's buckets. Unlike an API key, a parent organization's credential does not reach its sub-organizations: storage requests carry no organization header. Create the credential in the sub-organization.
  • A request needs every permission its operation needs. A missing one is AccessDenied (S3) or AuthorizationPermissionMismatch (Azure).
  • A credential can have an expiry date (expiresAt, a time with its timezone offset, such as 2027-01-31T00:00:00Z or 2027-01-31T00:00:00+05:30; a time without an offset is refused). From that moment every request signed with it is refused (InvalidAccessKeyId / AuthenticationFailed), and it cannot be rotated: create a new one.
  • At most 20 credentials can be active per organization (credential_limit_reached). Revoked and expired credentials do not count.

The secret is shown once

When you create a credential, the dashboard shows its secret once, with copy buttons and a download as a .env or AWS credentials file. It cannot be shown again, by you or by us. The list shows the access key ID (S3) or slot (Azure), the last four characters of the secret, its permissions and buckets, who created it and when it was last used (updated at most every 5 minutes).

Rotating and revoking

Rotation works like API keys: Rotate creates a second credential with the same name, permissions and buckets, and shows its secret. The old one keeps working until you revoke it:

Only a credential that still works can be rotated: rotating a revoked or expired one is credential_not_found.

  1. Rotate, and copy the new secret.
  2. Deploy it to the tools and servers that use the old one.
  3. When the old credential's Last used stops moving, revoke it.

An Azure account has two key slots, key1 and key2, as in Azure. Rotating fills the other slot, so your application can move to the new key while the old one still works. With both slots in use, revoke one before rotating again.

Revoking takes effect within 30 seconds for every new request, including presigned URLs and SAS tokens signed with the credential. A transfer already streaming finishes. A credential you create or rotate in works at once.

Presigned URLs and SAS

  • S3 presigned URLs (SigV4 query signing) work for every operation, with X-Amz-Expires from 1 to 604 800 seconds (7 days), as in S3.
  • Azure SAS: service SAS (sr=c or sr=b) and account SAS (ss including b; srt from s, c, o), signed with an account key, on every Azure operation listed here. sv versions from 2020-12-06 up to 2026-06-06 (a newer sv is refused with NotImplemented); st, se, sip and spr are honoured. A SAS lasts at most 7 days, counted from its st, or from the moment of the request when it has no st. Its permissions are limited by the key's: a SAS can never do more than the credential that signed it.
  • Not supported: user delegation SAS, stored access policies (si), directory SAS (sr=d), SAS parameters for snapshots, versions or encryption scopes, and the response overrides rscc, rscd, rsce, rscl and rsct: a SAS carrying any of them is refused with NotImplemented, never served with the stored headers instead. Set a file's content type, disposition and other headers when you upload it.

A SAS's permissions (sp) may use only the letters r, l, a, c, w and d. A SAS carrying any other letter — for example racwdlup, which some tools generate by default — is refused with NotImplemented; generate it with only the letters you need. Each letter grants exactly its Azure meaning, and no letter implies another:

Letter Grants
r Read: Get Blob, Get Blob Properties, Get Block List, reading the source of a copy, and Get Container Properties
l List: List Blobs, and List Containers (an account SAS with s in srt)
w Write: Put Blob, Put Block, Put Block List and the destination of a copy, creating or overwriting; and Create Container (an account SAS with c in srt)
c Create: Create Container (an account SAS with c in srt); and Put Blob, Put Block, Put Block List and the destination of a copy only while the blob does not exist yet. It never overwrites: a write to an existing blob is refused (AuthorizationPermissionMismatch)
a Append: append operations only. Append blobs are not supported, so a grants nothing
d Delete: Delete Blob, and Delete Container (an account SAS with c in srt)

Creating a container with a SAS (c or w) or deleting one (d) also needs a signing key whose credential has the manage permission. Blob metadata and property writes (Set Blob Metadata, Set Blob Properties) are not supported with any letter (see "Unsupported").

Which resources a SAS reaches: a blob SAS (sr=b) reaches only that blob's operations; a container SAS (sr=c) reaches its blobs, List Blobs and Get Container Properties, never List Containers or creating and deleting containers; an account SAS reaches the levels in its srt — s for List Containers, c for container operations (creating, deleting, reading properties, listing blobs), o for blob operations.

The SAS is then limited to what its signing key allows: a letter the key's permissions do not cover grants nothing.

Presigned URLs and SAS links work in a browser, but they are not a way to show files to viewers: see "Usage and pricing".

Unsigned requests are refused

Every request must be signed, even to a public bucket (AccessDenied in S3; Azure answers ResourceNotFound to an unsigned request for a supported operation, as it does for anonymous reads of private data; an operation we do not support is answered NotImplemented whether signed or not). A public bucket's files are served at its own address on plycdn.net, not through the storage protocols.

Header-signed requests must be within 15 minutes of our clock (RequestTimeTooSkewed / AuthenticationFailed); keep your machine's clock synchronized.

3Connecting your tools

The examples use the bucket course-media in mumbai, an S3 credential PLYEXAMPLEKEYID23456, and the storage account acmelearning. Keep secrets in your secret store or the tool's own credentials file, never in source code.

AWS CLI

~/.aws/config:

[profile plycdn]
region = mumbai
endpoint_url = https://s3.in-mum.plycdn.net
s3 =
  addressing_style = virtual

~/.aws/credentials:

[plycdn]
aws_access_key_id = PLYEXAMPLEKEYID23456
aws_secret_access_key = YOUR_SECRET_ACCESS_KEY
aws --profile plycdn s3 cp ./intro.mp4 s3://course-media/week-1/intro.mp4
aws --profile plycdn s3 sync ./week-2 s3://course-media/week-2/
aws --profile plycdn s3 ls s3://course-media/week-1/
aws --profile plycdn s3 presign s3://course-media/week-1/intro.mp4 --expires-in 3600

endpoint_url in a profile needs AWS CLI 2.13 or later; with an older CLI, pass --endpoint-url https://s3.in-mum.plycdn.net on each command. The CLI's default checksums (CRC32 on a single upload, a full-object CRC64NVME on a multipart upload) are verified; there is no need to turn them off.

boto3 (Python)

import boto3
from botocore.config import Config

s3 = boto3.client(
    "s3",
    endpoint_url="https://s3.in-mum.plycdn.net",
    region_name="mumbai",
    aws_access_key_id="PLYEXAMPLEKEYID23456",
    aws_secret_access_key=secret_from_your_store,
    config=Config(signature_version="s3v4", s3={"addressing_style": "virtual"}),
)
s3.upload_file("intro.mp4", "course-media", "week-1/intro.mp4")   # multipart above 8 MB
url = s3.generate_presigned_url("get_object",
                                Params={"Bucket": "course-media", "Key": "week-1/intro.mp4"},
                                ExpiresIn=3600)

.NET

Plycdn.AspNetCore.Storage.S3 builds an AmazonS3Client (from the AWS SDK for .NET) configured for a region:

dotnet add package Plycdn.AspNetCore.Storage.S3 --version 1.4.0 --source YOUR_PACKAGE_FEED_OR_FOLDER
using Amazon.S3.Model;
using Plycdn.AspNetCore.Storage.S3;

// Recommended: the host label (in-mum) is read from GET /regions, so a new region needs no new package.
// api is your PlycdnStorageApi (server scope); regions are data, never constants.
using var s3 = await PlycdnS3.CreateClientAsync(api, organizationId, "mumbai", accessKeyId, secretAccessKey);
await s3.PutObjectAsync(new PutObjectRequest
{
    BucketName = "course-media", Key = "week-1/intro.mp4", FilePath = "intro.mp4",
});

services.AddPlycdnS3(...) registers the same client (IAmazonS3) for dependency injection, reading the region, host label and credential from your configuration: Region, HostLabel, AccessKeyId and SecretAccessKey in the Plycdn:S3 section (keep the secret in your secret store), or set in code with services.AddPlycdnS3(s => { ... }). The region is a string: pass the code from GET /regions, not a constant. The client connects to the region's host (s3.in-mum.plycdn.net for mumbai) and signs with the region code. The short host label is not built in:

  • PlycdnS3.CreateClientAsync reads it from the region's endpoints in GET /regions, so you set nothing. Setting HostLabel (PlycdnCompatOptions.HostLabel) skips the lookup.
  • PlycdnS3.CreateClient and AddPlycdnS3 build the client without calling the API, so they need the label from you: copy it from the region's endpoints.s3 in GET /regions (in-mum in https://s3.in-mum.plycdn.net) into Plycdn:S3:HostLabel, the hostLabel argument or PlycdnCompatOptions.HostLabel. Without it they refuse to start with a message that names the setting. The label is not the region code (a host built from mumbai does not exist).

By default the bucket is in the host name (virtual-host style); Plycdn:S3:PathStyle (true) puts it in the path instead. PlycdnStorageEndpoints in Plycdn.AspNetCore.Storage gives you the hosts (S3ServiceUrl(region, hostLabel), S3VirtualHost(bucket, region, hostLabel), BlobServiceUrl(account, region, hostLabel)) if you configure a client yourself.

rclone

rclone is a good way to move a whole library in: from your machine, from another S3-compatible or Azure store, or from anything else rclone reads. Use a current stable rclone.

1. Configure the remote with an S3 credential from the dashboard (Developers → Storage credentials; give it read and write, and delete if you will run rclone sync). In rclone.conf (rclone config file prints where it is):

[plycdn]
type = s3
provider = Other
access_key_id = PLYEXAMPLEKEYID23456
secret_access_key = YOUR_SECRET_ACCESS_KEY
endpoint = https://s3.in-mum.plycdn.net
region = mumbai
  • endpoint is the S3 endpoint of the bucket's region, exactly as the dashboard shows it on the bucket's Connect with S3 tools panel (or endpoints.s3 from GET /regions). A bucket in another region needs its own remote with that region's endpoint and code.
  • rclone uses path-style addresses by default (force_path_style = true). Both styles are served; add force_path_style = false if you prefer virtual-host style.
  • Always point rclone at the plycdn endpoint above, never at any storage address you may have seen elsewhere: only the plycdn endpoint checks your credential, applies the bucket's rules and records the file as a plycdn object.
  • rclone's default bucket check (it looks for the bucket before its first upload) works as it is, so there is no need for no_check_bucket. If the bucket does not exist, rclone tries to create it, which needs the manage permission; create buckets in the dashboard instead. A credential without read cannot look, so set no_check_bucket = true for it.

2. Copy, comparing files by checksum:

rclone copy SOURCE:path plycdn:course-media/path \
  --transfers 4 --s3-upload-concurrency 4 --s3-chunk-size 64M \
  --checksum --fast-list --progress
  • SOURCE:path is a local folder or any rclone remote.
  • --transfers 4 × --s3-upload-concurrency 4 keeps at most 16 transfers open at once, the limit for one client address (see "Limits"); more only earns SlowDown answers, which rclone retries. To go faster, run more machines rather than raising these.
  • --s3-chunk-size 64M sends files above 200 MiB (rclone's --s3-upload-cutoff) as multipart uploads in 64 MiB parts. rclone holds transfers × concurrency × chunk size in memory (1 GiB here); lower the chunk size on a small machine, never below 5M.
  • --checksum compares size and MD5 to decide what to copy, rather than modification times. rclone records the MD5 of each file it uploads in several parts as metadata, so every file it copied can be checked. Don't pass --s3-disable-checksum.
  • An interrupted run can simply be started again: files already there and matching are skipped.

3. Verify:

rclone check SOURCE:path plycdn:course-media/path --fast-list --one-way

rclone check compares every file's size and MD5 on both sides and reports differences; 0 differences found is the result to look for. --one-way checks only that everything in the source arrived. Files uploaded in several parts by another tool have no whole-file MD5 that rclone can read, and rclone check counts them as hashes it could not check rather than as differences.

s3cmd

~/.s3cfg:

[default]
access_key = PLYEXAMPLEKEYID23456
secret_key = YOUR_SECRET_ACCESS_KEY
host_base = s3.in-mum.plycdn.net
host_bucket = %(bucket)s.s3.in-mum.plycdn.net
bucket_location = mumbai
use_https = True
signature_v2 = False

Cyberduck

Open a new connection, choose Amazon S3 (or an S3 profile with a custom server), and set:

  • Server: s3.in-mum.plycdn.net, port 443;
  • Access Key ID and Secret Access Key: your S3 credential.

Buckets, folders, uploads, downloads, renaming (a copy and a delete) and New Folder all work. Cyberduck's first request is signed for us-east-1, which is accepted.

Any SDK with an S3-compatible mode

Any AWS SDK, or any other SDK or tool with an S3-compatible mode, works with these settings:

Setting Value
Endpoint https://s3.{label}.plycdn.net
Region The region code, such as mumbai (not the host label)
Addressing Path style or virtual-host style; both are served
Signature Version 4 (SigV4)
Credentials Your S3 access key ID and secret access key

For example, with the AWS SDK for JavaScript:

import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";

const s3 = new S3Client({
  endpoint: "https://s3.in-mum.plycdn.net",
  region: "mumbai",
  forcePathStyle: true,
  credentials: { accessKeyId: "PLYEXAMPLEKEYID23456", secretAccessKey: secretFromYourStore },
});
await s3.send(new PutObjectCommand({ Bucket: "course-media", Key: "notes.pdf", Body: pdfBytes }));

Azure CLI

export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=https;AccountName=acmelearning;AccountKey=YOUR_ACCOUNT_KEY;BlobEndpoint=https://acmelearning.blob.in-mum.plycdn.net"

az storage blob upload --connection-string "$AZURE_STORAGE_CONNECTION_STRING" \
  --container-name course-media --name week-1/intro.mp4 --file ./intro.mp4
az storage blob list --connection-string "$AZURE_STORAGE_CONNECTION_STRING" \
  --container-name course-media --prefix week-1/ --output table

BlobEndpoint is what points the CLI at plycdn; without it the CLI goes to Azure. A connection string with a SharedAccessSignature instead of AccountKey works the same way.

To copy a blob, give the source as its plycdn URL with --source-uri. --source-container and --source-blob do not work: the CLI then builds an Azure URL (…blob.core.windows.net) whatever the connection string says, and a copy source that is not a plycdn blob is refused (CannotVerifyCopySource).

az storage blob copy start --connection-string "$AZURE_STORAGE_CONNECTION_STRING" \
  --destination-container course-media --destination-blob week-1/intro-copy.mp4 \
  --source-uri "https://acmelearning.blob.in-mum.plycdn.net/course-media/week-1/intro.mp4"

azure-storage-blob (Python)

from azure.storage.blob import BlobServiceClient

service = BlobServiceClient(
    account_url="https://acmelearning.blob.in-mum.plycdn.net",
    credential={"account_name": "acmelearning", "account_key": key_from_your_store},
    max_single_put_size=64 * 1024 * 1024,   # above this, upload in blocks
    read_timeout=600,                       # committing a large block list takes time
)
blob = service.get_blob_client("course-media", "week-1/intro.mp4")
with open("intro.mp4", "rb") as f:
    blob.upload_blob(f, overwrite=True)

Large uploads go as blocks and end with Put Block List, which assembles the blocks into the file inside the region before it answers — roughly a second for every 250 MB. Keep max_single_put_size at 64 MiB or below, so a large file is sent as blocks (a single Put Blob is limited to 5000 MiB), and raise read_timeout to at least 600 seconds for files of tens of gigabytes, so the client waits for the commit rather than giving up and retrying it. Other Azure SDKs have the same two settings under their own names.

.NET (Azure)

Plycdn.AspNetCore.Storage.AzureBlob builds a BlobServiceClient (from the Azure Storage SDK for .NET):

dotnet add package Plycdn.AspNetCore.Storage.AzureBlob --version 1.4.0 --source YOUR_PACKAGE_FEED_OR_FOLDER
using Plycdn.AspNetCore.Storage.AzureBlob;

var service = await PlycdnBlob.CreateServiceClientAsync(api, organizationId, "acmelearning", accountKey, "mumbai");
var blob = service.GetBlobContainerClient("course-media").GetBlobClient("week-1/intro.mp4");
await blob.UploadAsync("intro.mp4", overwrite: true);

The region code (mumbai) and the label from its listed endpoints give the host acmelearning.blob.in-mum.plycdn.net. Leave out the region to use the global host (acmelearning.blob.plycdn.net), which needs no label. CreateServiceClientAsync reads the label from GET /regions. PlycdnBlob.CreateServiceClient and services.AddPlycdnBlob(...) do not call the API, so with a region they need the label from you: copy it from the region's endpoints in GET /regions (in-mum in https://acmelearning.blob.in-mum.plycdn.net) into HostLabel. AddPlycdnBlob reads Account, AccountKey and, optionally, Region and HostLabel from the Plycdn:Blob section; without a HostLabel next to a Region it refuses to start, naming the setting. The client waits up to 10 minutes for a response, so the commit of a very large file is not sent twice.

4S3 operations

Operation Permission Notes
ListBuckets read Lists the buckets the credential reaches, in every region, with BucketRegion. max-buckets, continuation-token, prefix, bucket-region
CreateBucket manage An ordinary bucket creation: the name is unique across plycdn, the plan's bucket limit applies, and it is audited. LocationConstraint must be the endpoint's region or absent. Created private with the default upload rules; change them in the dashboard or with PATCH /buckets/{bucketId}. Answers as soon as the name is yours, waiting up to 20 seconds for storage to be ready (see below)
HeadBucket read With x-amz-bucket-region
GetBucketLocation read The region code
DeleteBucket manage The bucket must be empty (BucketNotEmpty). Then an ordinary delete: restorable for 7 days in the dashboard
ListObjectsV2 read UTF-8 binary order; prefix, delimiter, max-keys up to 1000, continuation-token, start-after, encoding-type=url, fetch-owner
ListObjects read The older listing: prefix, delimiter, marker, max-keys, encoding-type=url
GetObject read One Range; If-Match, If-None-Match, If-Modified-Since, If-Unmodified-Since; response-* overrides on signed requests. partNumber on an object written in one piece: part 1 is the whole object, a higher part is InvalidRange. partNumber on an object written in several parts is refused (NotImplemented); use a Range instead. Range and partNumber together are refused
HeadObject read As GetObject, without the body. A Range is honoured as GetObject honours it: 206 with Content-Range, or 416
PutObject write Up to 5 GiB. Content-MD5; x-amz-checksum-crc32, -crc32c, -crc64nvme, -sha1, -sha256 (header or trailer); the signed payload hash; aws-chunked bodies; x-amz-meta-* (2 KiB); Content-Type, Content-Encoding, Content-Language, Content-Disposition, Cache-Control, Expires; conditional writes If-None-Match: * and If-Match
DeleteObject delete A missing key succeeds, as in S3. If-Match is honoured (412 PreconditionFailed when it does not hold, NoSuchKey when the key is missing); any other conditional header on a delete is refused (NotImplemented)
DeleteObjects delete Up to 1000 keys; Quiet. The request must carry Content-MD5 or an x-amz-checksum-* header, as S3 requires (the AWS tools send one); without either it is refused (InvalidRequest), even with a signed payload hash. A signed payload hash is checked as well. See "Security notes"
CreateMultipartUpload write Takes the same headers as PutObject. x-amz-checksum-algorithm may be CRC32, CRC32C, SHA1 or SHA256 with the COMPOSITE checksum type (their default), CRC64NVME (always FULL_OBJECT: the AWS CLI's default), or CRC32 or CRC32C with x-amz-checksum-type: FULL_OBJECT. SHA1 or SHA256 as FULL_OBJECT, and CRC64NVME as COMPOSITE, are refused (InvalidRequest), as in S3. The answer names the upload's algorithm and checksum type (x-amz-checksum-algorithm, x-amz-checksum-type)
UploadPart write Parts 1 to 10 000, up to 5 GiB each
CompleteMultipartUpload write Parts in ascending order, ETags as returned; every part but the last at least 5 MiB (EntityTooSmall). A signed x-amz-checksum-* for the whole object must equal the upload's checksum, otherwise BadDigest: for COMPOSITE, the checksum of the parts' checksums, with -N; for FULL_OBJECT, the checksum of the whole object, which we combine from the parts' checksums. The answer carries the object's checksum and its ChecksumType. Answers normally (an ordinary status, errors included) when it finishes within 10 seconds. Longer than that it answers 200 and keeps the connection alive with spaces until the result, which can then be an error inside the 200, as in S3
AbortMultipartUpload write Not charged. Uploads left open end on their own after 7 days
ListParts read
ListMultipartUploads read
CopyObject write Within your organization and region, up to 5 GiB; x-amz-copy-source-if-*; x-amz-metadata-directive COPY or REPLACE. With COPY (the default) the copy keeps the source's metadata and headers, and any x-amz-meta-*, Content-Type, object headers or x-amz-storage-class: STANDARD the request also sends are ignored, as S3 ignores them: send REPLACE to change them. A copy onto itself must use REPLACE. x-amz-object-annotation-directive: EXCLUDE (which the AWS CLI sends) is accepted; we keep no annotations. The copy source is a bucket/key name, never a URL. Also used to rename (copy, then delete)
UploadPartCopy write A part copied from another object, with x-amz-copy-source-range; the way to copy objects over 5 GiB. x-amz-object-annotation-directive: EXCLUDE is accepted
GetObjectAttributes read ETag, ObjectSize, StorageClass (always STANDARD) and ObjectParts (the number of parts only). Checksum is accepted but never returned

A copy also needs read on the source bucket. Copies never cross regions: a source bucket in another region is refused (InvalidArgument); copy through your machine, or download and upload, instead.

A new bucket is usable at once from your tools, but its storage may take a few seconds more to be ready. Until it is, data calls on it answer 503 SlowDown with Retry-After, which every S3 SDK and tool retries on its own.

Headers accepted because they state what is already true: x-amz-acl: private and bucket-owner-full-control, x-amz-storage-class: STANDARD, x-amz-server-side-encryption: AES256 (everything is encrypted at rest), and x-amz-expected-bucket-owner equal to your organization's ID. Any other value of these headers is refused with NotImplemented. Also accepted, and ignored: x-amz-checksum-mode on GetObject and HeadObject, and x-amz-bucket-object-lock-enabled: false on CreateBucket (true is refused).

Configuration reads answered truthfully. Many tools read a bucket's configuration before they start and treat an error as "broken", so these are answered with what is true on plycdn:

Operation Permission Answer
GetBucketVersioning read Versioning has never been enabled (an empty configuration)
GetBucketAcl read The owner, with FULL_CONTROL only
GetObjectAcl read The owner, with FULL_CONTROL only
GetBucketTagging read NoSuchTagSet
GetObjectTagging read An empty tag set
GetBucketLifecycleConfiguration read NoSuchLifecycleConfiguration
GetBucketPolicy read NoSuchBucketPolicy
GetBucketCors read NoSuchCORSConfiguration
GetBucketEncryption read AES256
GetBucketLogging read Empty: logging is not enabled
GetPublicAccessBlock read NoSuchPublicAccessBlockConfiguration

5Azure operations

Block blobs only.

Operation Permission Notes
List Containers read The containers the key reaches; prefix, marker, maxresults up to 5000, include=metadata and include=deleted (rclone sends it; we keep no deleted containers, so none are listed)
Create Container manage An ordinary bucket creation, in the host's region (the home region on the global host). Unique across plycdn, within the plan's bucket limit, private with the default upload rules. Container metadata is not stored, so a Create Container with x-ms-meta-* is refused
Delete Container manage Removes the container even if it is not empty, as Azure does. Restorable for 7 days in the dashboard
Get Container Properties read
List Blobs read Flat and hierarchical (delimiter); prefix, marker, maxresults up to 5000, include=metadata
Put Blob write Up to 5000 MiB, x-ms-blob-type: BlockBlob only. x-ms-blob-content-*, Content-MD5, x-ms-blob-content-md5, x-ms-meta-* (8 KiB), conditional headers
Put Block write Up to 4000 MiB per block; up to 50 000 uncommitted blocks per blob, kept for 7 days
Put Block List write Up to 50 000 blocks; Latest and Uncommitted items. Committed items are refused (NotImplemented): a committed blob keeps no block list, so it cannot be amended block by block; upload its blocks again. Assembles the blob in the region before answering: see the timeout advice under "Connecting your tools"
Get Block List read The uncommitted blocks. The committed list is always empty, since committed blobs keep no block list
Get Blob read Range or x-ms-range; x-ms-range-get-content-md5 (ranges up to 4 MiB); conditional headers
Get Blob Properties read Content-MD5 for blobs written in one piece
Delete Blob delete If-Match is honoured (412 ConditionNotMet when it does not hold, BlobNotFound when the blob is missing); any other conditional header on a delete is refused (NotImplemented). x-ms-delete-snapshots is refused (there are no snapshots)
Copy Blob write Completed before it answers (x-ms-copy-status: success on the 202). The source must be a blob URL of your own account on plycdn, in the same region (copies never cross regions: CannotVerifyCopySource); it is authorized by your key (read on the source) or by a SAS on the source URL. The source URL is matched against our own addresses and never fetched
Get Account Information none Standard_LRS, StorageV2, hierarchical namespace off. The SDKs and the CLI ask for it. Answered for any valid account key or SAS, whatever its permissions or letters, since it says nothing about your data

Service and account SAS work on every operation above (see Credentials), with the same headers and conditions as Shared Key, whatever the request is signed with: a header or condition an operation does not support (for example x-ms-delete-snapshots, a lease, or If-Unmodified-Since on Delete Blob) is refused with NotImplemented, never ignored.

A new container is usable at once; until its storage is ready, data calls on it answer 503 ServerBusy with Retry-After, which the Azure SDKs retry on their own.

6Unsupported

Everything below is answered 501 NotImplemented, in each protocol's own error format, with the operation named in the message and x-plycdn-code: operation_not_supported. Nothing is silently ignored.

S3: ACL writes; bucket policies; versioning and versioned requests (versionId); object lock and legal hold; lifecycle writes; tagging writes and x-amz-tagging; replication; website hosting; CORS writes; logging writes; event notifications; inventory; analytics; metrics configuration; intelligent tiering; transfer accelerate; requester pays; ownership controls; public access block writes; SSE-KMS and SSE-C; S3 Select; RestoreObject; POST Object (browser form uploads); SigV2 and SigV4a signatures; WriteGetObjectResponse; directory buckets and session authentication.

Azure: page blobs and append blobs; leases; snapshots; versions; soft delete and undelete; Set Blob Properties, Set Blob Metadata and Set Blob Tier; Set Container Metadata and Set Container ACL; blob and container tags, Find Blobs by Tags; Put Block From URL, Put Blob From URL and Copy Blob From URL; Abort Copy Blob; Query Blob Contents; user delegation keys and user delegation SAS; stored access policies; service properties writes; static website; batch; immutability policies and legal hold; encryption scopes; SharedKeyLite signatures; access tiers other than the default; SAS response overrides (rscc, rscd, rsce, rscl, rsct); and x-ms-content-crc64 (transactional CRC64) on Put Block, Put Blob and Put Block List: send Content-MD5 instead. A SAS whose sv is newer than 2026-06-06 is refused too.

Some of these have a plycdn equivalent: bucket settings, upload rules and delivery restrictions are set in the dashboard or with PATCH /buckets/{bucketId}, and object metadata with PATCH /objects/{objectId} (Storage guide).

7Errors

Each protocol answers in its own format: S3's XML <Error> with Code, Message and RequestId; Azure's XML error with x-ms-error-code. HEAD requests carry no body. Every error also carries our own code in the x-plycdn-code header: match on that, or on the protocol's code, never on the message. Request IDs (x-amz-request-id, x-ms-request-id) are ours; quote one when you contact us.

Code Answered as What to do
credential_invalid S3: 403 InvalidAccessKeyId
Azure: 403 AuthenticationFailed
The access key ID or account is unknown, revoked or expired. Check it in the dashboard
signature_invalid S3: 403 SignatureDoesNotMatch
Azure: 403 AuthenticationFailed
Wrong secret, or the request changed after signing (a proxy rewriting a header, or the wrong host)
clock_skew S3: 403 RequestTimeTooSkewed
Azure: 403 AuthenticationFailed
Your clock is more than 15 minutes off; synchronize it
request_expired S3: 403 AccessDenied
Azure: 403 AuthenticationFailed
The presigned URL or SAS has expired; make a new one
signing_region_invalid S3: 400 AuthorizationHeaderMalformed Sign with the endpoint's region code (or us-east-1)
permission_denied S3: 403 AccessDenied
Azure: 403 AuthorizationPermissionMismatch
The credential (or SAS) lacks the permission, or the bucket is outside its scope
anonymous_refused S3: 403 AccessDenied
Azure: 404 ResourceNotFound
Sign the request. Public files are served at the bucket's own address
bucket_not_found S3: 404 NoSuchBucket
Azure: 404 ContainerNotFound
No bucket of that name in your organization
object_not_found S3: 404 NoSuchKey
Azure: 404 BlobNotFound
No such key, or it was deleted or disabled
bucket_wrong_region S3: 301 PermanentRedirect
Azure: forwarded, never shown
Use the region in x-amz-bucket-region
bucket_name_taken S3: 409 BucketAlreadyExists
Azure: 409 ContainerAlreadyExists
Names are unique across plycdn; choose another
bucket_already_yours S3: 409 BucketAlreadyOwnedByYou
Azure: 409 ContainerAlreadyExists
You already have it; carry on
bucket_not_empty S3: 409 BucketNotEmpty Delete the objects first
bucket_not_active S3: 503 SlowDown + Retry-After
Azure: 503 ServerBusy + Retry-After
The bucket's storage is still being prepared; tools retry. If it persists, check the bucket's state
object_exists S3: 403 AccessDenied
Azure: 409 BlobAlreadyExists
The bucket's upload rules do not allow overwriting
precondition_failed S3: 412 PreconditionFailed
Azure: 412 ConditionNotMet
A conditional header did not hold (another writer got there first, or the object changed)
not_modified S3: 304
Azure: 304
Your copy is current
range_not_satisfiable S3: 416 InvalidRange
Azure: 416 InvalidRange
The range starts beyond the end of the object
file_type_not_allowed S3: 403 AccessDenied
Azure: 403 AuthorizationFailure
Not in the bucket's allowed types
file_too_large S3: 400 EntityTooLarge
Azure: 413 RequestBodyTooLarge
Beyond a limit below, or the bucket's largest object; use multipart or blocks
plan_limit_reached S3: 403 QuotaExceeded
Azure: 403 QuotaExceeded
A plan limit is reached: your plan's storage is full, or its number of buckets is reached (a CreateBucket or Create Container), or the organization holds 100 000 folder markers. Free space or delete a bucket, or change plan
bucket_suspended S3: 403 AccessDenied
Azure: 403 AuthorizationFailure
plycdn has taken the bucket down: no uploads until it is reinstated; listing, reading and deleting still work. Contact [email protected]
organization_suspended S3: 403 AccessDenied
Azure: 403 AuthorizationFailure
plycdn has suspended your organization: every request with its access keys is refused until it is reactivated. Links you already issued keep working until they expire. Contact plycdn support
part_too_small S3: 400 EntityTooSmall Every part but the last must be at least 5 MiB
part_invalid S3: 400 InvalidPart
Azure: 400 InvalidBlockList
A listed part or block was never uploaded, or its ETag differs
part_order_invalid S3: 400 InvalidPartOrder List the parts in ascending order
multipart_not_found S3: 404 NoSuchUpload The upload was completed, aborted or expired (7 days)
upload_expired S3: 404 NoSuchUpload
Azure: 404 BlobNotFound
The write stayed open past its time limit and has expired; send it again
block_list_invalid Azure: 400 InvalidBlockList The block list names a block that is not staged, or lists more than 50 000 blocks
digest_mismatch S3: 400 BadDigest
Azure: 400 Md5Mismatch
The body does not match its Content-MD5; send it again
checksum_mismatch S3: 400 XAmzContentChecksumMismatch
Azure: 400 Md5Mismatch
The body does not match a declared checksum; send it again
incomplete_body S3: 400 IncompleteBody
Azure: 400 InvalidInput
The body was shorter than its declared length
size_mismatch S3: 400 IncompleteBody
Azure: 400 InvalidInput
The body was shorter or longer than declared
missing_content_length S3: 411 MissingContentLength
Azure: 411 MissingContentLengthHeader
Send Content-Length (or x-amz-decoded-content-length with aws-chunked)
metadata_too_large S3: 400 MetadataTooLarge
Azure: 400 InvalidMetadata
User metadata is limited to 2 KiB (S3) or 8 KiB (Azure)
invalid_key S3: 400 InvalidArgument
Azure: 400 InvalidResourceName
The key breaks a rule under "Keys and folders"
copy_source_invalid S3: 400 InvalidArgument
Azure: 400 CannotVerifyCopySource
The source is not a readable object of your organization in this region
copy_too_large S3: 400 InvalidRequest
Azure: 413 RequestBodyTooLarge
Copies over 5 GiB go through UploadPartCopy
operation_not_supported S3: 501 NotImplemented
Azure: 501 NotImplemented
See "Unsupported"
rate_limited S3: 503 SlowDown + Retry-After
Azure: 503 ServerBusy + Retry-After
Too many requests or transfers at once; tools back off on their own
object_busy S3: 503 SlowDown + Retry-After
Azure: 503 ServerBusy + Retry-After
Another write to this key is landing; retry shortly
upload_not_open S3: 503 SlowDown + Retry-After
Azure: 503 ServerBusy + Retry-After
The upload is being completed; retry shortly
upload_too_slow S3: 400 RequestTimeout
Azure: 408 OperationTimedOut
The body arrived too slowly and was cut; send it again
provider_unavailable S3: 503 ServiceUnavailable
Azure: 503 ServerBusy
Transient; retry with backoff
validation_error S3: 400 InvalidArgument
Azure: 400 InvalidInput
A value is not valid (a bucket name breaking the name rules, for example)
malformed_request S3: 400 InvalidRequest
Azure: 400 InvalidInput
The XML body or a header could not be read

The codes shared with our own API (bucket_not_found, object_not_found, bucket_name_taken, bucket_not_active, object_exists, object_busy, file_type_not_allowed, file_too_large, plan_limit_reached, checksum_mismatch, size_mismatch, upload_not_open, upload_too_slow, provider_unavailable, bucket_hostname_unavailable, validation_error) mean the same as in the API reference. bucket_hostname_unavailable (S3: 503 ServiceUnavailable; Azure: 503 ServerBusy) means creating the bucket could not finish; retry the create.

8Limits

Limit Value
PutObject / Put Blob 5 GiB / 5000 MiB
UploadPart / Put Block 5 GiB / 4000 MiB
Parts in a multipart upload / uncommitted blocks per blob 10 000 / 50 000
Blocks in one Put Block List 50 000
Smallest part (all but the last) 5 MiB
Put Block List assembly 50 GiB per blob; larger files go through S3 multipart
CopyObject / Copy Blob 5 GiB; larger S3 copies through UploadPartCopy
Largest object 1 342 177 280 000 bytes (the storage limit; your plan's or bucket's limit may be lower)
Keys per DeleteObjects 1000
Listing page 1000 keys (S3), 5000 blobs (Azure)
User metadata 2 KiB per S3 request, 8 KiB per Azure request
XML request bodies 2 MiB (S3 DeleteObjects and CompleteMultipartUpload); 8 MiB for an Azure Put Block List
Request headers 16 KiB in all
Requests per client address 400 a second, with bursts up to twice that (SlowDown / ServerBusy with Retry-After beyond it)
Requests per credential your plan's, across every region and endpoint: 36,000 a minute on starter, 120,000 on growth, 300,000 on enterprise (GET /plan names yours); ask us to raise them
Transfers at once 16 per client address (IPv6: per /64), 64 per credential, 128 per organization
Open connections per client address 32
Slowest accepted upload 16 KiB/s, after the first 30 seconds (RequestTimeout / OperationTimedOut)
A download the client stops reading closed after 60 seconds
Clock difference for header-signed requests 15 minutes
Presigned URL / SAS lifetime 7 days (a SAS: from its st, or from the request when it has none)
Abandoned multipart uploads and staged blocks removed after 7 days
Folder markers 100 000 per organization

Every write (PutObject / Put Blob, CreateMultipartUpload, Put Block List, and a copy that replaces the type) refuses a Content-Type that is not exactly one media type (type/subtype with optional parameters, e.g. not video/mp4,text/html) with InvalidArgument (S3) or InvalidInput (Azure); the type part is stored in lower case. Script-capable types such as HTML, XML and SVG are always stored as attachments, even when Content-Disposition: inline is sent.

Bytes of uploads in progress count toward your storage quota until the upload completes or is aborted: a multipart upload's parts and a blob's uncommitted blocks count from the moment each is sent (and stop counting when they expire after 7 days), and completing the upload counts the object once. A part or block that would pass the quota is refused with QuotaExceeded.

Beyond a rate or transfer limit, the answer is SlowDown / ServerBusy with Retry-After, which every SDK backs off on. Leave your tool's retries on. Expect: 100-continue is answered only after your request is authorized and admitted, so a refused upload never sends its body.

9ETags, checksums and consistency

ETags are what S3 tools expect:

  • For a file written in one piece (PutObject, Put Blob, a one-part upload through our API, or a copy of one), the ETag is the MD5 of its content, in quotes.
  • For a file written in N parts, it is the MD5 of the parts' MD5s followed by -N, as in S3. An Azure blob committed from a block list is such a file, with N the number of blocks in the list (even one), so it has no Content-MD5; an x-ms-blob-content-md5 sent with the list is still checked.
  • A file uploaded through the dashboard or our upload API gets the same ETag, so aws s3 sync and rclone check verify it. Azure clients see the same value as the blob's ETag.
  • The etag field in our own API's object response is unchanged and can differ from this value; compare S3 and Azure ETags with each other, not with it.

Checksums. Every body's MD5 is computed as it arrives. Content-MD5, the x-amz-checksum-* headers and trailers, the SigV4 payload hash and x-ms-blob-content-md5 are all checked before the file is stored; a mismatch stores nothing (BadDigest, XAmzContentChecksumMismatch, Md5Mismatch). A file written in one piece also gets its SHA-256 recorded as verified.

A request body the signature does not cover is accepted, as S3 accepts it: an UNSIGNED-PAYLOAD or STREAMING-UNSIGNED-PAYLOAD-TRAILER request, or one that carries only a CRC checksum (x-amz-checksum-crc32, -crc32c or -crc64nvme). Every checksum it declares is still checked. What that means for a few small request bodies is under "Security notes".

Consistency. Reads, listings and deletes are strongly consistent, whichever protocol or API wrote the file: a file is listed and readable as soon as its write succeeds, and gone as soon as its delete succeeds. When two writers race on the same key, exactly one wins; a conditional write (If-None-Match: *, If-Match, or Azure's conditional headers) is checked at the moment the file lands, so two conditional writers cannot both succeed. Bucket creation is the one exception (see the note under S3 operations). Delivery links on plycdn.net are cached separately, and refreshed on overwrite and delete as the Storage guide describes.

Metadata and headers. x-amz-meta-* and x-ms-meta-* are the object's metadata (lower-case names, string values), the same field our API reads and writes. A name may use any character an HTTP header name allows, _ and . included (x-amz-meta-my_key, x-amz-meta-a.b). A metadata entry that is not a string cannot be sent as a header; S3 reports how many were left out in x-amz-missing-meta. Content-Encoding, Content-Language, Content-Disposition and Cache-Control are kept with the file and served by delivery links too, and so is Expires for a file written through S3 (Azure blobs have no Expires: it is not kept).

Keys and folders. Keys follow the storage rules: 1 to 1024 bytes of UTF-8, no control characters, no \, no //, no leading /, no . or .. path segment, and nothing beginning with .plycdn- (reserved, in any case, percent-encoded or in full-width letters). Spaces, +, % and non-ASCII characters are fine; use encoding-type=url in listings if your tool asks for it. A key ending in / is refused (invalid_key) except for one case: a zero-byte PUT of a key ending in / is a folder marker, as Cyberduck's New Folder and many consoles create. It lists as a zero-byte object, HEAD and GET answer it (a Range answers the whole, empty marker), and DELETE removes it, as in S3; it is not stored as a file and not charged. Creating one again changes nothing, and If-None-Match: * on an existing one is 412. A marker carries no metadata or object headers (a folder PUT with them is refused, NotImplemented) and is not a copy source (NoSuchKey). An organization holds at most 100 000 of them (QuotaExceeded).

10Security notes

Every request is signed, travels over HTTPS only, and is checked against your credential before anything is read or written. The bodies you upload are checked against every checksum and digest the request declares, and nothing is stored when one does not match.

Some clients do not sign the request body itself, only its headers. The body is then protected by HTTPS and by our servers rather than by your signature. This happens when:

  • an S3 client sends UNSIGNED-PAYLOAD (or STREAMING-UNSIGNED-PAYLOAD-TRAILER) with no Content-MD5, or protects the body only with a CRC checksum (x-amz-checksum-crc32, -crc32c, -crc64nvme), which catches accidents but is not a signature;
  • an Azure client sends Put Block List with a SAS (a SAS never signs a body), or with Shared Key and no Content-MD5 header.

For file contents, send a checksum or Content-MD5 (current AWS tools send a checksum by default): it is verified as described under "ETags, checksums and consistency". It matters most for four small XML bodies that say what to do:

Request Its body says
DeleteObjects Which keys to delete
CompleteMultipartUpload Which uploaded parts make up the file, in which order
CreateBucket The bucket's region (LocationConstraint)
Put Block List Which staged blocks make up the blob, in which order

For these, turn on payload signing (the SigV4 payload hash) or Content-MD5 where your tool allows it. Then the body is bound to your signature: any change to it after you signed is refused (SignatureDoesNotMatch / AuthenticationFailed). Many tools already sign these bodies; some SDKs skip body signing over HTTPS by default and have an option to turn it on. With Azure tools, prefer Shared Key with Content-MD5 on Put Block List, over a SAS, where the tool offers it. DeleteObjects must always carry Content-MD5 or an x-amz-checksum-* header; without one it is refused.

11Usage and pricing

Storage through these protocols is the same storage as through our API: every file counts towards Storage and Uploads exactly as the Storage guide describes, once, whichever door it came in by.

Three more metrics count use of the storage protocols themselves:

Metric Label on usage and statements Unit What it counts
gateway_egress_gb Storage API egress GB (2³⁰ bytes) Bytes sent to your tools: downloads, listings and other response bodies
gateway_write_requests_millions Storage API write requests Millions of requests PUT, POST and copy requests, and listings of objects: uploads (PutObject, UploadPart, Put Blob, Put Block), CreateMultipartUpload, CompleteMultipartUpload, Put Block List, copies (CopyObject, UploadPartCopy, Copy Blob), object listings (ListObjectsV2, ListObjects, ListParts, ListMultipartUploads, List Blobs), and bucket and container create and delete
gateway_read_requests_millions Storage API read requests Millions of requests GET and HEAD requests that are not object listings: downloads, object, blob, bucket and container properties, Get Block List, the configuration reads, ListBuckets, List Containers and Get Account Information
  • Deletes are not charged: DeleteObject, DeleteObjects, Delete Blob and AbortMultipartUpload (only deleting a bucket or container counts, as a write request). Uploaded bytes are not charged as egress (they are the free Uploads metric). Requests refused before we know whose they are are not counted.
  • A request sent to the global Azure host and forwarded to another region is counted once, by the region that served it.
  • Figures are counted per bucket, region and UTC day, and settle after the day ends. They appear in GET /usage, GET /usage/resources, on your statement and on the dashboard's Account → Usage page, each with its label. Requests that name no bucket (ListBuckets, List Containers, Get Account Information) are counted against your organization: in GET /usage/resources they carry your organization's ID as resourceId and resourceName: null.

Prices are set per plan and shown on your usage and statement lines (unitPrice). Unless your plan says otherwise, the list prices, the same on every plan, are:

Metric List price
Storage API egress Your plan's Delivery price for India & Asia, per GB, whatever the bucket's region: ₹5.00, ₹4.50 for the part of a month's volume between 10,000 and 50,000 GB, and ₹4.00 above 50,000 GB
Storage API write requests ₹420 per million
Storage API read requests ₹34 per million

All plans and prices are on Plans and prices.

No allowance is included: the first GB and the first request are charged like the rest.

Reading through the storage API is API access, not delivery. It is uncached, limited per credential, and priced as egress above. It is also not on the delivery path: an outage of the storage protocols never affects playback or delivery links. For viewers, use delivery links on plycdn.net (signed for private buckets); they do not pass through our servers. A presigned URL or SAS link handed to a viewer works, but it is charged as storage API egress, and it has none of delivery's caching, referrer rules or country rules.