Skip to content

Storage API

Endpoints for managing ZevCloud Storage from your own code. Every request requires the x-team-id header identifying the team the request operates on. Authentication uses the same bearer token scheme as the rest of the Services API. See Authentication for token mechanics and scopes.

  • read — list plans, list regions, list buckets, get bucket detail, browse objects, mint download URLs.
  • full — create buckets, delete buckets, mint upload URLs, delete objects, rotate keys, switch plans, attach custom domains.

Owner and admin roles can call any endpoint within scope. Member role can list and read but cannot fetch raw credentials or write objects.

GET
/v1/storage/plans

Returns the active storage plans available to the team. Includes per-plan storage cap (storageBytesCap), Naira price (priceNgnMonthly), whether the plan allows public-read buckets (allowsPublicRead), and the plan’s slug for use in switch/upgrade calls.

[
{
"id": "...",
"slug": "free",
"name": "Free",
"storageBytesCap": "...",
"priceNgnMonthly": 0,
"isFree": true,
"allowsPublicRead": false
}
]
GET
/v1/storage/regions

Returns regions the team can pick from when creating a bucket. Each entry has a region code (region) and a human-readable label (label).

GET
/v1/storage/subscription

Returns the team’s current storage subscription with current usage, role-derived capabilities for the caller, and any delinquency state. Returns null if the team has no subscription yet — call auto-provision to create the Free-plan subscription.

{
"subscription": { "status": "active", "planId": "..." },
"plan": { "slug": "starter", "allowsPublicRead": true },
"usage": { "bytesUsed": "...", "bucketCount": 3 },
"delinquency": { "state": "current" },
"role": "owner",
"capabilities": {
"canCreateBuckets": true,
"canViewCredentials": true
}
}
POST
/v1/storage/subscription/auto-provision

Idempotent. Materializes the Free plan subscription for the team if they don’t already have one. Used by the dashboard’s first-time empty state.

Switch plan (Free-to-Free or free downgrade)

Section titled “Switch plan (Free-to-Free or free downgrade)”
POST
/v1/storage/subscription/switch
{
"planSlug": "free"
}

Owner-only. Completes synchronously. Use upgrade instead for paid plans, which goes through invoice settlement.

POST
/v1/storage/subscription/upgrade
{
"planSlug": "starter",
"applyCredit": true
}

Owner-only. Creates a draft invoice for the target plan, attaches a payment method, and returns { invoiceId, paymentUrl, paymentMethod, action }. The subscription is not changed yet — the plan flip happens when the invoice settles.

For a Free-plan switch this endpoint completes synchronously with action: "switched" and no invoice.

GET
/v1/storage/subscription/switch-eligibility?planSlug=starter

Returns whether a plan switch is currently allowed, with structured reasons if not (reason, message, blockers, cooldownHoursRemaining). Useful for disabling destinations in a UI before submit.

GET
/v1/storage/buckets/check-availability?suffix=uploads

Live-feedback helper for create forms. Returns { available, reason, fullName }. reason is one of:

  • ok — the clean name {prefix}{suffix} is free.
  • taken_globally_will_append_tail — another team has the clean name; create will succeed but append a unique tail.
  • taken_in_team — your team already has a bucket with that suffix.
GET
/v1/storage/buckets

Returns every bucket on the team with status, region, size, and a canViewCredentials flag derived from the caller’s role.

POST
/v1/storage/buckets
{
"nameSuffix": "uploads",
"region": "...",
"projectId": "...",
"isPublicRead": false,
"objectLockEnabled": false
}

Owner or admin. projectId optionally links the bucket to one of the team’s projects for organisational purposes. isPublicRead requires a plan with allowsPublicRead: true. objectLockEnabled is permanent on the bucket once set.

The response includes the new bucket plus an accessKeyId and a one-time secretAccessKey. The secret is only returned in this response. Subsequent reads do not include it.

GET
/v1/storage/buckets/:bucketId

Returns the bucket row, the access key ID (owners and admins only), the bucket endpoint, and effective serving state:

{
"id": "...",
"fullName": "...",
"region": "...",
"status": "active",
"isPublicRead": true,
"endpoint": "...",
"accessKeyId": "...",
"canViewCredentials": true,
"effectivePublicRead": true,
"publicUrl": "...",
"planAllowsPublicRead": true
}

The secret access key is not included on this read. Use rotate to get a fresh one.

effectivePublicRead combines the bucket’s own flag with the team’s plan capability — a public-read bucket on a downgraded plan returns effectivePublicRead: false and publicUrl: null until the team is back on a plan that allows it.

POST
/v1/storage/buckets/:bucketId/refresh-usage

Forces an immediate recalculation of the bucket’s bytesUsed. Useful after large external changes to surface the new size in the dashboard right away rather than waiting for the periodic refresh.

PATCH
/v1/storage/buckets/:bucketId/visibility

Owner or admin. Flips an existing bucket between public-read and private. The response is the full bucket-detail shape so the caller can drop it into local state without an extra GET.

{
"isPublicRead": true
}

Enabling public-read (isPublicRead: true) requires a plan with allowsPublicRead. On Free plans this returns a 400 with an upgrade hint and the bucket is unchanged.

The endpoint wires both sides of the gate: the bucket row’s isPublicRead column (what the dashboard renders) and the underlying gateway’s website-access flag (what the public URL actually serves). Disabling is best-effort on the gateway side — the DB flag flips even if the gateway call hiccups, with a log warning, so the customer’s intent is honored. Enabling fails atomically (DB stays as-is) if the gateway can’t be flipped, so we never advertise a URL that won’t serve.

Public URLs become available immediately at <publicUrl> from the response when effectivePublicRead is true.

DELETE
/v1/storage/buckets/:bucketId

Owner or admin. Drains all objects synchronously then removes the bucket. Long-running on large buckets — set generous client timeouts.

POST
/v1/storage/buckets/:bucketId/keys/rotate

Owner or admin. Returns the new credential pair. The secret is shown once. The previous key remains valid for a 24-hour grace period before automatic revocation.

{
"accessKeyId": "...",
"secretAccessKey": "...",
"previousKeyExpiresAt": "..."
}
POST
/v1/storage/buckets/:bucketId/retry

Only valid on buckets in error status. Re-runs provisioning against the same row, preserving the original name. On success the bucket flips to active and the response includes a fresh secret access key.

GET
/v1/storage/buckets/:bucketId/connected-services

Returns the services on the team that have this bucket wired in as managed environment variables.

GET
/v1/storage/buckets/:bucketId/objects?prefix=uploads/&delimiter=/

Lists objects in a bucket using S3 prefix + delimiter semantics. Pass delimiter=/ to get back folders (common prefixes) alongside objects. Page through results with continuationToken.

Query parameters:

ParameterRequiredDefaultNotes
prefixNoemptyFilter to keys starting with this prefix
delimiterNo/Folder simulation. Pass delimiter= (empty) for a flat listing
continuationTokenNoFrom the previous page’s response
maxKeysNo1000Max objects per page

Response:

{
"objects": [
{
"key": "uploads/avatar.png",
"size": 12345,
"lastModified": "...",
"etag": "..."
}
],
"folders": ["uploads/2026/"],
"continuationToken": null,
"isTruncated": false
}
POST
/v1/storage/buckets/:bucketId/objects/upload-url
{
"key": "uploads/avatar.png",
"contentType": "image/png"
}

Owner or admin. Returns a presigned PUT URL the browser can upload to directly, plus the expiration timestamp.

{
"url": "...",
"method": "PUT",
"headers": { "Content-Type": "image/png" },
"expiresAt": "..."
}

Default URL lifetime is 1 hour. Use the returned headers exactly as given — adding extra signed headers on the client will break the signature.

For larger upload volumes or full S3 protocol features (multipart uploads, advanced ACLs), use the bucket’s access key directly with an S3 SDK instead.

GET
/v1/storage/buckets/:bucketId/objects/download-url?key=uploads/avatar.png

Any team member. Returns a presigned GET URL for a single object. Default lifetime 1 hour; max 24 hours.

{
"url": "...",
"method": "GET",
"expiresAt": "..."
}
DELETE
/v1/storage/buckets/:bucketId/objects?key=uploads/avatar.png

Owner or admin. Idempotent — does not error on a missing key.

GET
/v1/storage/domains/managed-eligible

Returns the ZevCloud-registered domains on the team that can be attached as a subdomain to any bucket. Team-scoped; not specific to a particular bucket. Use this to populate a domain picker before attaching.

GET
/v1/storage/buckets/:bucketId/domains

Returns the custom domains attached to a bucket along with the platform’s cnameTarget value:

{
"domains": [
{
"id": "...",
"hostname": "assets.example.com",
"status": "active",
"verifiedAt": "...",
"lastCheckedAt": "..."
}
],
"cnameTarget": "..."
}

Status is one of pending_verification, active, or error.

POST
/v1/storage/buckets/:bucketId/domains

Two body shapes:

External domain (you manage DNS):

{
"hostname": "assets.example.com"
}

The row starts in pending_verification status. The response includes the CNAME target to point the hostname at. Trigger verify after pointing the CNAME.

Managed domain (registered through ZevCloud):

{
"managedRegistrationId": "...",
"subdomain": "assets"
}

The CNAME is written to your DNS automatically and the row is active immediately.

Either path: the bucket must be public-read AND on a plan that allows public-read. Owner or admin only.

POST
/v1/storage/buckets/:bucketId/domains/:domainId/verify

Triggers a verification check on a pending_verification domain. The platform fetches a verification token at https://{hostname}/.well-known/zevcloud-verify/{token}; the domain flips to active if it round-trips. Rate-limited.

DELETE
/v1/storage/buckets/:bucketId/domains/:domainId

Owner or admin. Removes the domain from the bucket. Traffic on the hostname stops resolving to the bucket immediately. You can leave the CNAME pointing at the platform target afterwards — nothing breaks — or remove it on your DNS side.

Connecting a bucket to a service (env-vars API)

Section titled “Connecting a bucket to a service (env-vars API)”

The bucket-to-service connect flow lives under the env-vars API rather than under /storage. Documented in full at Environment Variables; the two relevant endpoints are:

  • POST /v1/services/:serviceId/env/connect-bucket — body { bucketId }. Injects the five managed S3 env vars onto the service.
  • DELETE /v1/services/:serviceId/env/connect-bucket/:bucketId — removes them.

The managed env vars stay synchronized with bucket key rotations automatically — no extra call needed.