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.netreaches the containers in that region directly. Use it: it is the one the dashboard shows for each bucket.{account}.blob.plycdn.netgoes 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 withInvalidResourceName, 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.
managecan 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) orAuthorizationPermissionMismatch(Azure). - A credential can have an expiry date (
expiresAt, a time with its timezone offset, such as2027-01-31T00:00:00Zor2027-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.
- Rotate, and copy the new secret.
- Deploy it to the tools and servers that use the old one.
- 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-Expiresfrom 1 to 604 800 seconds (7 days), as in S3. - Azure SAS: service SAS (
sr=corsr=b) and account SAS (ssincludingb;srtfroms,c,o), signed with an account key, on every Azure operation listed here.svversions from2020-12-06up to2026-06-06(a newersvis refused withNotImplemented);st,se,sipandsprare honoured. A SAS lasts at most 7 days, counted from itsst, or from the moment of the request when it has nost. 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 overridesrscc,rscd,rsce,rsclandrsct: a SAS carrying any of them is refused withNotImplemented, 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.CreateClientAsyncreads it from the region'sendpointsinGET /regions, so you set nothing. SettingHostLabel(PlycdnCompatOptions.HostLabel) skips the lookup.PlycdnS3.CreateClientandAddPlycdnS3build the client without calling the API, so they need the label from you: copy it from the region'sendpoints.s3inGET /regions(in-muminhttps://s3.in-mum.plycdn.net) intoPlycdn:S3:HostLabel, thehostLabelargument orPlycdnCompatOptions.HostLabel. Without it they refuse to start with a message that names the setting. The label is not the region code (a host built frommumbaidoes 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
endpointis the S3 endpoint of the bucket's region, exactly as the dashboard shows it on the bucket's Connect with S3 tools panel (orendpoints.s3fromGET /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; addforce_path_style = falseif 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 themanagepermission; create buckets in the dashboard instead. A credential withoutreadcannot look, so setno_check_bucket = truefor 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:pathis a local folder or any rclone remote.--transfers 4×--s3-upload-concurrency 4keeps at most 16 transfers open at once, the limit for one client address (see "Limits"); more only earnsSlowDownanswers, which rclone retries. To go faster, run more machines rather than raising these.--s3-chunk-size 64Msends 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.--checksumcompares 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 noContent-MD5; anx-ms-blob-content-md5sent with the list is still checked. - A file uploaded through the dashboard or our upload API gets the same ETag, so
aws s3 syncandrclone checkverify it. Azure clients see the same value as the blob's ETag. - The
etagfield 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(orSTREAMING-UNSIGNED-PAYLOAD-TRAILER) with noContent-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 Listwith a SAS (a SAS never signs a body), or with Shared Key and noContent-MD5header.
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 BlobandAbortMultipartUpload(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: inGET /usage/resourcesthey carry your organization's ID asresourceIdandresourceName: 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.