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çim | Yer |
|---|---|
| Chat Completions | Akış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. |
| Responses | Her message çıktı öğesinin sone alanı. |
| Vercel AI SDK | data-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
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"]
}
}format | Ne taşır |
|---|---|
buttons | text ve 1 ile 3 arası seçenek, choices içinde. |
quick_replies | text ve 1 ile 13 arası seçenek, choices içinde. |
link_button | text ve bir bağlantı düğmesi, buttons içinde. |
card | title, 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
urltaşı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 adfilename*ile gelir. Tarayıcı dosyayı sayfanızın içinde açmaz. - Süresi dolmuş ya da bozuk bir bağlantı
404 file_not_foundalır. Dosya o an okunamıyorsa503 file_unavailabledöner. - Aynı dosya kimliği saatte en fazla 10 kez indirilir; fazlası
429 file_download_limitalır. Çalışma alanının günlük indirme sınırı (varsayılan 2 GiB, UTC gününe göre) dolunca429 file_egress_limitgelir. İkisi deRetry-Aftertaşır ve isteğin hangi adresten geldiğine bakmaz. - Aynı adresten dakikada 120'den fazla indirme isteği de
429alı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.
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.