Bring your own storage (BYO Storage)
With your own storage (BYO Storage, short for “Bring Your Own”), Turbo Notify stores received media and contact profile pictures directly in your bucket. Without this setup, received media is not available for download.
How your own storage works
When a contact sends an image, video, audio, document or sticker to your WhatsApp number, Turbo Notify:
- Receives the media from WhatsApp.
- Uploads the file to your bucket (through the S3 API).
- Returns a presigned URL directly in the media object (
image.url,video.url, …) when you callGET /v1/numbers/{ref}/messages/{id}. - Sends the
message.receivedwebhook with the available media fields.
Without your own storage configured, the kind field returns "unavailable" with reason: "no_storage". The media content is not kept.
Supported providers
Turbo Notify uses the S3 HTTP API, the same protocol supported by all three providers:
| Provider | Notes |
|---|---|
| Cloudflare R2 | No egress fees; a good choice when media is downloaded often. |
| Supabase Storage | Useful if you already use Supabase in your project. |
| AWS S3 | Egress fees apply. |
How to set it up
The path is the same with all three providers: a private bucket, an access key, and the form in Storage (opens in a new tab) . Below is the path for each provider. Their screens change over time: if a name does not match, look for the equivalent.
The bucket must be private, with no public access. Turbo Notify accesses its content with the credentials you provide, not through public URLs. The access key needs these permissions. On R2 and AWS, restrict the key to that bucket; a Supabase key cannot be restricted to a bucket:
s3:PutObjects3:GetObjects3:DeleteObjects3:ListBucketCloudflare R2
- Sign in at
dash.cloudflare.comand open R2 object storage. The first time, activate R2: Cloudflare asks for a card and for you to accept its terms before it enables R2. - Right after activating, the account’s S3 address can take a few minutes to answer. If the connection test fails in that window, wait and try again.
- Click Create bucket, pick a name, leave the location on Automatic and keep public access off. Do not use a specific jurisdiction (EU or FedRAMP): Turbo Notify only accepts the account’s standard address.
- Go back to R2 object storage > Overview. Under API Tokens, click Manage, then Create Account API token (Create User API token also works). Choose the Object Read & Write permission and limit the token to the bucket.
- The token screen shows three values. Use the Access Key ID and the Secret Access Key. The Token value (the long text that starts with
cfat_orcfut_) is not used in Turbo Notify. The secret is shown only once: copy it before leaving the screen. - The Account ID has 32 characters and is under R2 object storage > Overview, in Account details. It is the same value that appears in the account’s S3 address (
https://<Account ID>.r2.cloudflarestorage.com).
Supabase Storage
- Open the project at
supabase.com/dashboard, go to Storage and create the bucket with New bucket, leaving Public bucket off. - In Storage, open the S3 page (in some dashboards it is under Settings), find S3 Access keys and click New access key. Copy the Access key ID and the Secret access key: the secret is shown only once. A Supabase S3 key works for every bucket in the project, not just this one.
- The Project Ref is the 20 lowercase letters in the project address (
https://<Project Ref>.supabase.co), also in the dashboard address (supabase.com/dashboard/project/<Project Ref>) and in the Project URL of the Connect dialog.
AWS S3
- Sign in at
console.aws.amazon.com(create an account if you do not have one: AWS asks for a card), open S3 and click Create bucket, pick the name and the region, and keep Block all public access on. - In IAM > Users, create a user without console access and attach this policy (replace
BUCKET-NAME):
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::BUCKET-NAME" }, { "Effect": "Allow", "Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"], "Resource": "arn:aws:s3:::BUCKET-NAME/*" } ]}- On the user, open Security credentials and click Create access key. Choose Application running outside AWS and copy the Access key ID and the Secret access key. The secret is shown only once.
Fill in the dashboard
Go to Storage (opens in a new tab) and fill in:
| Field | Value |
|---|---|
| Provider | AWS S3, Cloudflare R2 or Supabase Storage |
| Bucket | Name of the bucket you created |
| Region | The bucket’s region (for example us-east-1). R2 is always auto and Supabase is us-east-1 |
| Access Key ID | The access key created at the provider (on the form it appears as R2 Access Key ID or Supabase S3 Access Key ID; for R2, it is the Access Key ID, never the Token value) |
| Secret Access Key | The matching secret key |
| Cloudflare Account ID / Supabase Project Ref | Only for R2 and Supabase, respectively: the S3 address is built from this value |
Test the connection and save
Before saving, click Test connection. Turbo Notify checks the bucket (HEAD), writes a temporary object (PUT), reads it back (HEAD) and removes it (DELETE), validating the 4 required permissions (ListBucket/PutObject/GetObject/DeleteObject). Each check shows in a list with its result, and the save button is only enabled when all four pass. Once saved, media starts being stored immediately.
If a check fails:
- Key refused: with R2, check that you used the Access Key ID and not the Token value.
- Bucket not found: the name must be exact and the bucket must belong to the key’s account.
- R2 just activated: wait a few minutes and test again.
- Only one permission fails: review the policy or the token permission, which must include read, write, delete and list.
Behavior after setup
Received media
All media received after activation is copied to your bucket automatically. The media fields in GET /v1/numbers/{ref}/messages/{id} start returning kind: "available", for example:
{ "kind": "available", "url": "https://<bucket>.r2.cloudflarestorage.com/...?X-Amz-...", "expires_at": "2026-04-21T11:00:00Z", "mime_type": "image/jpeg", "size_bytes": 184320}Access to the media expires at expires_at. To get a new URL, fetch the message again within your plan’s content retention period. After that period, Turbo Notify deletes its reference to the file, and the file stays in your bucket.
When pictures appear
Profile pictures are only stored while your storage is active. They are not downloaded all at once, so your WhatsApp number is not overloaded:
| Picture of | When it is fetched |
|---|---|
| Your numbers | Right away, when the number connects or when you set up storage. If the first attempt does not work, it is tried again within about 6 hours. Shown in the dashboard. |
| Contacts | On the next message exchanged with the contact (received or sent, including through the API). If a check already ran in the last 6 hours, only after that. |
| Groups | On the next message from a group member or sent from the phone itself. Messages sent only through the API do not trigger the fetch. The same 6 hour window applies. |
| Changed picture | A stored picture is checked again every 7 days: numbers on their own, contacts and groups on the first message after that. |
| No picture | People who have no picture, or hide it in their WhatsApp privacy settings, do not appear. |
When the picture reaches your bucket, the contact.picture_changed (contacts) or group.picture_changed (groups) webhook is sent, and GET /v1/numbers/{ref}/contacts/{id}/profile-picture answers with a 302 redirect to the presigned URL. Until then, the answer is unavailable.
Media received before setup
Media received before your own storage was activated stays at kind: "unavailable". It is not recovered later: only new media is stored.
Troubleshooting
kind: "unavailable" with reason: "upload_failed"
The upload to the bucket failed after retries. Possible causes:
- Expired credentials or insufficient permissions.
- Deleted bucket or changed access policy.
- Bucket quota reached (check the provider’s console).
Go to Storage (opens in a new tab) and click Test connection to diagnose.
kind: "unavailable" with reason: "storage_circuit_open"
Your storage failed several times in a row and media uploads to it were paused temporarily. Turbo Notify retries automatically. When the connection is restored, you receive the storage.recovered webhook.
kind: "pending"
The media is still being uploaded to the bucket. Wait a few seconds and call GET /v1/numbers/{ref}/messages/{id} again. This state only shows while the upload is in progress: the message.received webhook is only sent once the media is available or unavailable.
Security
- Credentials are stored encrypted (AES-256-GCM), never in plain text.
- Turbo Notify never exposes credentials in logs or responses.
- Presigned URLs have a configurable validity (default: 15 minutes; up to 1 hour).
- We recommend least privilege: on R2 and AWS, credentials restricted to the Turbo Notify bucket. A Supabase S3 key covers the whole project: if that is a problem, use a project just for Turbo Notify.
Related
- Get a message: media fields as a union discriminated by
kind. - Webhooks: Storage and media: the
storage.degradedandstorage.recoveredevents. - Send media: sending media from a public URL.
- Profile picture: redirect to a presigned URL.