Skip to content

Group picture

Returns the picture of a WhatsApp group: redirects (302) to a presigned URL when the picture is in your BYO Storage; otherwise, answers 200 with the picture object (state and reason). The response format is the same as the contact profile picture endpoint, but what happens when the presigned URL cannot be generated is different in the two: see Behavior by picture.kind.

Endpoint

GET /v1/groups/{group_id}/picture

Required scope: groups:read. For extra numbers: GET /v1/numbers/{ref}/groups/{group_id}/picture, where {ref} is the number_id in the envelope of the group event (num_…, it never changes) or the number’s current alias. group_id is the public id (grp_{hex32}).

Behavior by picture.kind

The returned picture has only two possible states: available (picture in your BYO Storage) or unavailable (any other situation, with the cause in reason):

picture.kindHTTP response
available302 Found: presigned BYO Storage URL (R2/S3/Supabase), never the object key in the bucket
unavailable200 OK: JSON body { "kind": "unavailable", "reason": ..., "setup_hint"?: ... } (see the reason field below)

The picture.kind = "pending" value only appears in webhook payloads: this read endpoint never returns it.

If the picture is already in your BYO Storage but the presigned URL could not be generated on this request, the endpoint answers 200 OK with unavailable and the cause in reason. It never returns 404 for an unavailable picture.

Turbo Notify checks a picture marked unavailable again on the next message from a group member (or sent from the phone itself) after you enable BYO Storage, after the 6 hour window if a check already ran. Messages sent only through the API do not trigger the fetch (see When pictures appear and the picture becomes available later).

Example: picture available

Terminal window
curl -I "https://api.turbonotify.com/v1/groups/grp_.../picture" \
-H "Authorization: Bearer YOUR_KEY"
HTTP/1.1 302 Found
Location: https://<bucket>.r2.cloudflarestorage.com/groups/.../picture.jpg?...
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache

The presigned URL is short-lived: 15 minutes by default (configurable between 1 minute and 1 hour in your storage integration).

Example: picture unavailable (no storage)

HTTP status: 200
{
  "kind": "unavailable",
  "reason": "no_storage",
  "setup_hint": {
    "setup_url": "https://dashboard.turbonotify.com/storage",
    "docs_url": "https://docs.turbonotify.com/general/byo-storage-setup/",
    "message": "Configure object storage (S3, Cloudflare R2, or Supabase Storage, free tiers available) to receive media. Visit your dashboard or read the docs."
  }
}

The reason field tells you the cause: no_storage (no storage configured), storage_circuit_open (your storage had been failing and uploads were paused for a while), upload_failed (uploading the picture to your bucket failed this time) or peer_has_none (the group has no picture). setup_hint is only filled in for no_storage and storage_circuit_open. For upload_failed and peer_has_none it comes as null. It is the same format used by media and by the contact picture.

The picture field in GET /v1/numbers/{ref}/groups/{group_id}

The picture field in the full group payload shows the same state: it carries the presigned URL when the picture is available, and the right reason and setup_hint when it is unavailable.

Cache

  • The response comes with Cache-Control: no-store, no-cache, must-revalidate and Pragma: no-cache: it must not be cached, including the 302 redirect. Call again whenever you need the current state. The presigned URL given in Location stays valid for its own lifetime, and that URL is the one to reuse if you want to avoid a new download.
  • The presigned URL is short-lived: 15 minutes by default (configurable between 1 minute and 1 hour). Do not reuse the URL past that time; call again to get a new one.