sone centralgeliştirici

Zengin mesajlar

Ürün ve hizmet kartları, düğmeler, hızlı yanıtlar ve dosyalar SoneMessage nesnesi olarak gelir; her birinin düz metin karşılığı da vardır.

Asistan WhatsApp'ta ürün kartı, düğme ya da belge gönderebiliyorsa API'de de gönderir. API onları belirli bir uygulamanın biçiminde değil, sizin istediğiniz gibi çizebileceğiniz yapılandırılmış veri olarak verir. Bu verinin adı SoneMessage. Ekibiniz Mesajlar ekranından bir ürün, hizmet ya da belge paylaştığında da aynı yapı gelir, role: "staff" ile: Ekibin paylaştıkları.

Nerede gelir

BiçimYer
Chat CompletionsAkışsız yanıtta choices[0].message.sone.messages. Akışta her mesaj, kendi son parçasının üst düzeyindeki sone.message alanında gelir.
ResponsesHer message çıktı öğesinin sone alanı.
Vercel AI SDKdata-sone-catalog, data-sone-actions, data-sone-file ve data-sone-staff parçaları. Asistanın düz metin mesajları text parçası olarak gelir.
Sohbetin mesajlarıGET /conversations/{id}/messages yanıtının data dizisi.

Vercel AI SDK parçalarının data alanı SoneMessage'ın tamamını değil, çizmek için gerekeni taşır: text, role ve mesajın türüne göre catalog, actions ya da file. data-sone-staff yalnız ekibinizin düz metin mesajları içindir; ekip kart, düğme ya da dosya gönderirse ilgili parça data.role: "staff" ile gelir. Ayrıntılar: Vercel AI SDK.

Düz metin her zaman vardır

Her SoneMessage'ın text alanı doludur. Ürün kartı içeren bir mesajda bu, ürünlerin adını, fiyatını ve adresini sayan düz bir metindir. Zengin mesajları çizmeyen bir istemci yalnız text'i gösterebilir. Çok uzun bir katalog paylaşımında text kısalır, kayıtların tamamı yine catalog.items'tadır: Katalog. OpenAI biçimlerinde aynı metin content alanında da vardır.

SoneMessage

sone-message.ts
export type SoneMessage = {
  id: string; // "msg_…"
  role: 'assistant' | 'staff' | 'user'; // staff: ekibinizden biri yazdı
  type: 'text' | 'catalog' | 'actions' | 'file';
  text: string; // düz metin karşılığı
  created_at: number; // Unix saniyesi
  catalog?: SoneCatalog;
  actions?: SoneActions;
  file?: SoneFile;
};

export type SoneCatalog = {
  // İşletmenin seçtiği sunum: "card", "card_button", "image_text", "link_button", "text", "carousel" ya da "list"
  format: string;
  items: SoneCatalogItem[];
};

export type SoneCatalogItem = {
  id: string; // kaydın Sonecentral'daki kimliği
  kind: 'inventory' | 'offering'; // ürün ya da hizmet
  title: string;
  description?: string;
  price?: string; // biçimlendirilmiş fiyat: "1.290,00 TL"
  detail?: string; // stok, süre gibi kısa bilgi
  attributes?: string; // renk, beden gibi özellikler, düz metin
  image_url?: string;
  url?: string; // ürünün ya da hizmetin sayfası
  button_title?: string; // url düğmesinin yazısı; url varsa gelir
};

export type SoneActions = {
  format: 'buttons' | 'quick_replies' | 'card' | 'link_button';
  title?: string;
  text?: string;
  image_url?: string;
  buttons?: { title: string; url: string }[];
  choices?: string[];
};

export type SoneFile = {
  id: string; // "file_…"
  name: string;
  mime_type: string;
  size: number; // bayt
  url: string; // imzalı indirme bağlantısı
};

role: "user" yalnız sohbetin mesaj listesinde görünür ve ziyaretçinin kendi satırlarıdır; bu satırların type değeri her zaman text olur. Bir turun yanıtındaki mesajlar her zaman işletme tarafındandır: assistant ya da staff.

Boş alanlar gönderilmez: description, price, image_url gibi bir alan yoksa nesnede de yoktur.

Katalog

Bir paylaşım 1 ile 10 kayıt taşır, asistanın seçtiği sırayla.

{
  "id": "msg_c4t2l9g7x1p3h8q",
  "role": "assistant",
  "type": "catalog",
  "text": "Keten gömlek, taş rengi\n1.290,00 TL\nM ve L stokta\nhttps://www.ornek.com/urun/keten-gomlek-tas\n\nKeten gömlek, lacivert\n1.290,00 TL\nYalnız M kaldı\nhttps://www.ornek.com/urun/keten-gomlek-lacivert",
  "created_at": 1760866872,
  "catalog": {
    "format": "carousel",
    "items": [
      {
        "id": "r8k2m9p4q7w1x3z",
        "kind": "inventory",
        "title": "Keten gömlek, taş rengi",
        "price": "1.290,00 TL",
        "detail": "M ve L stokta",
        "image_url": "https://cdn.ornek.com/urun/keten-tas.jpg",
        "url": "https://www.ornek.com/urun/keten-gomlek-tas",
        "button_title": "Ürünü incele"
      },
      {
        "id": "r4q7w1x8k2m9a5z",
        "kind": "inventory",
        "title": "Keten gömlek, lacivert",
        "price": "1.290,00 TL",
        "detail": "Yalnız M kaldı",
        "url": "https://www.ornek.com/urun/keten-gomlek-lacivert",
        "button_title": "Ürünü incele"
      }
    ]
  }
}

format işletmenin bu kayıtlar için seçtiği sunumdur. Tek kayıtta kaydın kendi sunumu gelir: card, card_button, image_text, link_button ya da text. Birden çok kayıt carousel olur; hepsi liste olarak seçilmişse list. Size yol gösterir ama bağlamaz: bir listeyi kart olarak çizmek de doğrudur. Hizmetler kind: "offering" ile aynı alanlarla gelir; detail genellikle süreyi taşır.

text, her kayıt için ad, açıklama, fiyat, kısa bilgi, özellikler ve adresi alt alta yazar; kayıtlar arasında boş bir satır olur.

text en fazla 4.000 karakterdir. Örneğin uzun açıklamalı on ürün buna sığmazsa yer kayıtlar arasında paylaştırılır. Payına sığan kayıt tam yazılır. Sığmayan kayıtta önce ad, fiyat ve kısa bilgi gelir, adres sığıyorsa en sona yazılır; özellikler ve açıklamanın tam cümleleri kalan yere eklenir. Bir alan ya tam yazılır ya hiç yazılmaz, ortadan kesilmez. catalog.items her kaydı her zaman eksiksiz taşır; kartları oradan çizin, text'i yedek olarak kullanın.

Düğmeler ve hızlı yanıtlar

{
  "id": "msg_a7c1t9n2k4q8z3w",
  "role": "assistant",
  "type": "actions",
  "text": "Randevunuz için hangi gün uygun?\nPerşembe\nCuma\nCumartesi",
  "created_at": 1760866923,
  "actions": {
    "format": "quick_replies",
    "text": "Randevunuz için hangi gün uygun?",
    "choices": ["Perşembe", "Cuma", "Cumartesi"]
  }
}
formatNe taşır
buttonstext ve 1 ile 3 arası seçenek, choices içinde.
quick_repliestext ve 1 ile 13 arası seçenek, choices içinde.
link_buttontext ve bir bağlantı düğmesi, buttons içinde.
cardtitle, kısa bir text, isteğe bağlı image_url ve en fazla 3 bağlantı düğmesi, buttons içinde.

Bir seçenek ve bir düğme başlığı en fazla 20 karakterdir. text alanı başlığı, metni, her bağlantı düğmesini Başlık: adres biçiminde ve seçenekleri alt alta yazar.

  • Her düğme bir url taşır ve o adresi yeni sekmede açar.
  • Bir seçeneğe tıklanınca başlığını ziyaretçinin yeni mesajı olarak gönderin. Asistan onu ziyaretçinin yazdığı bir mesaj gibi okur.
  • Seçenekler yalnız ziyaretçinin bir sonraki mesajına kadar anlamlıdır. Asistan seçeneklerin ardından bir metin mesajı da yazabilir; seçenekleri ziyaretçi yeniden yazana kadar gösterin, sonra gizleyin.

Dosya

{
  "id": "msg_f1l3d0c5x9w2m6r",
  "role": "assistant",
  "type": "file",
  "text": "Fiyat listesi",
  "created_at": 1760867010,
  "file": {
    "id": "file_p9x2k4m7q1w8e3t",
    "name": "fiyat-listesi-2026.pdf",
    "mime_type": "application/pdf",
    "size": 188416,
    "url": "https://pb.sonecentral.com/api/v1/files/file_p9x2k4m7q1w8e3t?exp=1761471810&sig=…"
  }
}

text dosyanın açıklamasıdır; açıklama yoksa dosyanın adıdır. Bağlantıyı bir <a download> ile verin. Bir dosya en fazla 32 MB olabilir.

Aynı dosya aynı sohbette yeniden paylaşılırsa file.id aynı kalır. Başka bir sohbette dosya kendi kimliğiyle ve kendi bağlantısıyla gelir. Kaç sohbette paylaşılırsa paylaşılsın dosya çalışma alanınızın depolamasında bir kez yer tutar.

Dosyayı indirmek

file.url, GET /files/{id} uç noktasının imzalı adresidir:

GET https://pb.sonecentral.com/api/v1/files/{id}?exp={son_geçerlilik}&sig={imza}
  • Anahtar gerekmez, adresteki imza yeter. Adresi olduğu gibi kullanın.
  • Bağlantı yedi gün geçerlidir. Sohbetin mesajlarını her okuduğunuzda yedi gün geçerli yeni bir bağlantı gelir.
  • Yanıt her zaman indirme olarak gelir (Content-Disposition: attachment) ve dosyanın adını taşır; Türkçe harfli bir ad filename* ile gelir. Tarayıcı dosyayı sayfanızın içinde açmaz.
  • Süresi dolmuş ya da bozuk bir bağlantı 404 file_not_found alır. Dosya o an okunamıyorsa 503 file_unavailable döner.
  • Aynı dosya kimliği saatte en fazla 10 kez indirilir; fazlası 429 file_download_limit alır. Çalışma alanının günlük indirme sınırı (varsayılan 2 GiB, UTC gününe göre) dolunca 429 file_egress_limit gelir. İkisi de Retry-After taşır ve isteğin hangi adresten geldiğine bakmaz.
  • Aynı adresten dakikada 120'den fazla indirme isteği de 429 alır; bu yanıtın gövdesi OpenAI hata biçiminde değildir.

Çizmek

Aşağıdaki işlev bir SoneMessage'ı DOM'a çevirir. Bütün metinleri textContent ile yazar, innerHTML kullanmaz; böylece hiçbir alan sayfanıza kod sokamaz.

render-sone-message.js
function el(tag, props = {}, ...children) {
  const node = document.createElement(tag);
  for (const [key, value] of Object.entries(props)) {
    if (value == null) continue;
    if (key === 'class') node.className = value;
    else if (key.startsWith('on')) node.addEventListener(key.slice(2), value);
    else node.setAttribute(key, value);
  }
  for (const child of children.flat()) {
    if (child == null || child === false) continue;
    node.append(typeof child === 'string' ? document.createTextNode(child) : child);
  }
  return node;
}

export function renderSoneMessage(message, { onChoose }) {
  if (message.type === 'catalog' && message.catalog) {
    return el(
      'div',
      { class: 'sone-catalog' },
      message.catalog.items.map((item) =>
        el(
          'article',
          { class: 'sone-item' },
          item.image_url ? el('img', { src: item.image_url, alt: '', loading: 'lazy' }) : null,
          el('strong', {}, item.title),
          item.detail ? el('span', {}, item.detail) : null,
          item.price ? el('span', { class: 'sone-price' }, item.price) : null,
          item.url ? el('a', { href: item.url, target: '_blank', rel: 'noopener' }, item.button_title || 'İncele') : null,
        ),
      ),
    );
  }

  if (message.type === 'actions' && message.actions) {
    const { title, text, buttons = [], choices = [] } = message.actions;
    return el(
      'div',
      { class: 'sone-actions' },
      title ? el('strong', {}, title) : null,
      text ? el('p', {}, text) : null,
      buttons.map((button) => el('a', { href: button.url, target: '_blank', rel: 'noopener' }, button.title)),
      choices.map((choice) => el('button', { type: 'button', onclick: () => onChoose(choice) }, choice)),
    );
  }

  if (message.type === 'file' && message.file?.url) {
    return el('a', { class: 'sone-file', href: message.file.url, download: message.file.name }, message.file.name);
  }

  return el('p', { class: message.role === 'staff' ? 'sone-text sone-staff' : 'sone-text' }, message.text);
}

Tam bir pencere içinde kullanımı: Site içine sohbet penceresi.

Bu sayfada