VYSLO DEVELOPER PLATFORM
Back to vyslo.ai

WHAT TO BUILD

Four kinds of project, one design rule

Pipe conversations into your own workflow, grow a knowledge base out of them, answer questions about what was said, or make your app the capture surface. All four use the same SDK and cloud API, and all four follow one rule, so start there.

把對話接進自己的工作流、用它長出一個知識庫、回答「當時說了什麼」、或讓你的 App 成為採集入口。 四種項目用的是同一套 SDK 與雲端 API,也都遵守同一條原則,所以先從那裡開始。

THE RULE: KEEP YOUR OWN COPY OF WHAT YOU PROCESS

recording.ready tells you a recording is ready; listRecordings() and searchTranscripts() let you page through and search the user's recordings, so a delivery you missed can always be picked up again. What the platform does not do is keep your derived data for you: once a user hides a recording, deletes it or revokes your app, it stops being readable.

So: store the raw response the moment you process it, and reconcile against the list regularly. Every design below follows from that.

原則:你處理過的,自己留一份

recording.ready 告訴你一條錄音好了;listRecordings() 與 searchTranscripts() 讓你分頁瀏覽、搜索用戶的錄音,漏掉的投遞隨時能補回來。平台不會替你保存的, 是你自己加工出來的數據:用戶一旦隱藏或刪除某條錄音、或撤銷你的應用,它就不再可讀。

所以:處理的那一刻就把原始回應存下來,並定期對照列表核對。下面每一種設計都是從這條推出來的。

What you actually get

Per recordingAcross recordings and the card
title, status, recordedOn, timeLabel, createdAt
segments[] — speaker, text, timestamp, identity
summaryLines[] — the summary points
identity for the recording as a whole, with its source
the original audio file, behind its own scope
Listing and search across recordings
Webhook events for recordings, capture, smart wake, button and charging
Weekly insights, speaking statistics and to-dos
Card status, events and live audio over BLE
Transcription of any audio file
每條錄音跨錄音與卡片
title、status、recordedOn、timeLabel、 createdAt
segments[]——speaker、text、timestamp、 identity
summaryLines[]——摘要要點
整條錄音的 identity,附來源
音頻原件,在獨立權限之下
跨錄音的列表與搜索
錄音、起停錄音、智能喚醒、按鍵與充電的 webhook 事件
週洞察、發言統計與待辦
經 BLE 的卡片狀態、事件與實時音頻
任意音頻檔案的轉寫
USE identity, NOT speaker

segments[].speaker is a diarization label like S1 and is not tied to a person. segments[].identity is: it says whether that line was the owner, likely the owner or someone else, and where that verdict came from. Build "the sentences I said" on identity, and show its source next to it.

用 identity,不要用 speaker

segments[].speaker 是 S1 這樣的分離標籤,不綁定具體的人。 segments[].identity 才是:它說明這句是機主、可能是機主還是別人,以及這個判決來自哪裡。 「找出我說過的句子」請建立在 identity 上,並在旁邊展示它的來源。

The four patterns

A

Forward

One recording becomes one record in the tool your team already lives in. Stateless, an afternoon's work.

B

Accumulate

Many recordings become your own store. You own the schema and the index.

C

Recall

Answer questions over your store, or over ours with search and weekly insights.

D

Capture

Your app becomes the surface people record into, with the Android or iOS SDK.

A

轉發

一條錄音變成團隊日常工具裡的一條記錄。無狀態,一個下午能做完。

B

沉澱

很多條錄音長成你自己的庫。schema 和索引都歸你。

C

回憶

在你的庫上回答問題,或用搜索與週洞察直接在我們的數據上回答。

D

採集

用 Android 或 iOS SDK,讓你的 App 成為人們錄音的入口。


A · Forward: into the workflow you already have

What the user gets: a meeting ends, and the notes are already in Notion, Lark, Slack or your own ticket system — nobody typed them up.

Shape: event → verify → acknowledge → fetch → map → write. Stateless apart from one mapping table, because the record you write is the storage.

Scope: recordings.read only. You do not need the audio to do this.

用戶得到什麼:會一開完,紀要已經在 Notion、飛書、Slack 或你自己的工單系統裡了——沒有人去敲。

形狀:事件 → 校驗 → 應答 → 拉取 → 映射 → 寫入。除了一張映射表之外無狀態, 因為你寫出去的那條記錄就是存儲。

權限:只要 recordings.read。做這件事不需要音頻。

node · the mapping is the whole design
// Signature verification first — see the Quickstart. Then:
async function onRecordingReady(event) {
  const rec = await vyslo.get(`/open/ext/recordings/${event.recordingId}`);

  await notion.pages.create({
    parent: { database_id: dbFor(event.userId) },   // your mapping table
    properties: {
      Name:  title(rec.title || 'Conversation'),
      Date:  date(rec.recordedOn),
      Source: select(rec.ownerVerified ? 'Owner verified' : 'Unverified'),
      VysloId: rich_text(rec.recordingId),          // idempotency key, visible
    },
    children: [
      ...rec.summaryLines.map(bullet),              // the summary is the body
      toggle('Transcript', rec.segments.map(s =>
        paragraph(`[${s.timestamp}] ${s.speaker}: ${s.text}`))),
    ],
  });
}

Design notes. Put recordingId in a visible property, not just your own database — when you re-process, you need to find the existing page without a second store. Map userId to your account on the day they authorize, not lazily later. And acknowledge the webhook before you call Notion: their API being slow must not turn into a delivery we give up on.

設計要點。把 recordingId 寫進一個可見欄位,而不只是你自己的資料庫—— 重放時你需要在沒有第二份存儲的情況下找到已有的頁面。userId 到你的帳號的映射, 在用戶授權當天就建好,不要等用到時再補。還有:先應答 webhook 再去調 Notion, 別讓對方 API 慢變成我們這邊放棄投遞。


B · Accumulate: grow your own knowledge base

What the user gets: everything they said, in their own second brain, searchable years later.

Shape: backfill once with listRecordings(), then follow recording.ready. Store the raw response the moment you fetch it and enrich from your own copy, so a bug in your parser never depends on the recording still being readable.

用戶得到什麼:他說過的一切,都在自己的第二大腦裡,幾年後還搜得到。

形狀:先用 listRecordings() 回填一次,之後跟著 recording.ready 走。拉取的那一刻就存下原始回應,所有加工都從你自己的副本出發—— 這樣解析器出 bug 時,不必指望那條錄音還能再讀。

store raw first, enrich later
recordings
  recording_id   text primary key     -- idempotency key
  user_id        text                 -- VYSLO user, mapped to yours
  recorded_on    date
  raw            jsonb                -- the API response, verbatim, never rewritten
  owner_verified boolean
  fetched_at     timestamptz

chunks                                -- derived; safe to drop and rebuild from raw
  recording_id   text references recordings
  idx            int
  text           text
  embedding      vector

sync_state                            -- how you notice your own holes
  user_id        text primary key
  last_event_at  timestamptz          -- last recording.ready you accepted
  failed_count   int

Design notes.

  • Raw column, always. Derived tables are rebuildable; the fetch is not.
  • Your uptime is their archive's completeness. Queue the webhook body to durable storage inside the request, before any parsing that could throw.
  • Reconcile against the list. Page through listRecordings() on a schedule and fetch anything you do not have. When a user says "that meeting isn't here", you can check instead of guessing.
  • Say what the user is signing up for. Tell them at authorization whether you import their history or start from today, and let them choose.
  • Audio is a separate decision. Transcripts are small and cheap to keep forever. Original audio is not, and it carries the voice of everyone else in the room. Ask for that scope only if a feature needs it.

設計要點。

  • 永遠留原始欄位。派生表可以重建,那次拉取不能。
  • 你的可用性就是他檔案的完整度。在請求內、在任何可能拋異常的解析之前,先把 webhook 原文落到持久隊列。
  • 對照列表核對。定期分頁調 listRecordings(),把你還沒有的補拉回來。用戶說「那場會不在這裡」時,你能查證,而不是靠猜。
  • 把用戶在同意什麼說清楚。授權時就告訴他:你會導入歷史錄音,還是從今天開始——並讓他自己選。
  • 音頻是另一個決定。轉寫很小,永久保存很便宜;音頻原件不是,而且裡面有房間裡每個人的聲音。真的有功能需要,才申請那個權限。

C · Recall: answer questions over that store

What the user gets: "what did I agree to last week, and with whom?" — answered from their own conversations.

Shape: either build it on your B store, where retrieval, ranking and the answer are entirely yours, or call searchTranscripts() and fetchWeeklyInsights() and let the platform do the retrieval.

用戶得到什麼:「上週我答應了誰什麼?」——用他自己的對話回答。

形狀:要麼建在你的 B 庫上,檢索、排序和成文全歸你;要麼直接調 searchTranscripts() 與 fetchWeeklyInsights(),讓平台替你做檢索。

Design notes.

  • Always cite. An answer with recordingId plus timestamp, linking back to the moment, is trusted. An uncited one is a guess the user has to verify by hand — which is the work you were supposed to remove.
  • Chunk on the transcript, keep the summary separate. summaryLines is already distilled; it makes an excellent retrieval target and a poor source of quotes.
  • Be precise about "I". Filter on segments[].identity for what the owner said, and show the verdict's source with the answer.
  • Time is a first-class filter. "Last week" is most of what people ask. recordedOn is cheap to index and worth indexing on day one.

設計要點。

  • 永遠給出處。帶 recordingId 與 timestamp、能跳回那一刻的答案,用戶才敢信。沒出處的答案等於要他手動核對一遍——而那正是你本來要替他省掉的活。
  • 在轉寫上分塊,摘要單獨存。summaryLines 已經是提煉過的:當檢索目標很好用,當引文來源很糟。
  • 對「我」要精確。按 segments[].identity 篩出機主說的話,並在答案旁展示判決來源。
  • 時間是一等過濾條件。人們問的多半是「上週」。recordedOn 建索引很便宜,第一天就該建。
OR HAND IT TO THE USER'S OWN ASSISTANT

If your product does not need to own the answer, the user can ask Claude, ChatGPT or Cursor directly through the official MCP server, with the same consent and the same identity layer.

或者交給用戶自己的 AI 助手

如果你的產品不需要自己掌握答案,用戶可以經官方 MCP server 直接在 Claude、ChatGPT 或 Cursor 裡問,授權方式與身份層都一樣。


D · Capture: make your app the surface

What the user gets: they record from inside your app, and what they said lands in your product without a detour through ours.

Shape: the Android and iOS SDKs connect to the card over BLE: start and stop recording from your own UI under the control lease, follow card events, and pull audio off the card or stream it live. The same SDK handles authorization, the token store and the cloud results.

Design note. Call renewIfNeeded() on foreground start and handle 403 as a real state in your UI, not an error toast — the user revoked you, and your app should say so and offer to reconnect.

用戶得到什麼:他在你的 App 裡錄音,說過的話直接進你的產品,不用繞一圈我們的。

形狀:Android 與 iOS SDK 經 BLE 連接卡片: 持有控制權租約時在你自己的介面裡起停錄音、接收卡片事件、從卡上導出音頻或實時串流。 授權、令牌存儲與雲端結果也由同一個 SDK 負責。

設計要點。前台啟動時調 renewIfNeeded(),並把 403 當成介面裡的一個真實狀態,而不是一個錯誤提示——那是用戶撤銷了你,App 應該如實說明並提供重新連接。


Seven questions before you write code

QuestionWhy it decides the design
One recording, or all of them?Forward needs no store. Accumulate needs one from day one, backfilled with listRecordings().
How does userId map to your account?Webhooks carry ours. Resolve it at authorization time, when you have both.
What is your idempotency key?recordingId. Retries repeat it, and a slow 2xx we timed out on looks like a success to you.
What happens when your endpoint is down?We retry three times, then stop. Pick it up later with listRecordings(). Acknowledge fast, persist before parsing.
Do you really need the audio?A separate scope, a separate consent, and the voices of people who are not your user.
What does your app do on 403?The user revoked you. Stop syncing, say so plainly, offer to reconnect. Do not retry.
And what happens to what you already have?Revocation stops the flow; it cannot recall what you stored. Give the user deletion on your side too — that is your half of the promise.
問題為什麼它決定了設計
你要一條,還是全部?轉發不需要存儲。沉澱從第一天就需要,並用 listRecordings() 回填。
userId 怎麼映射到你的帳號?Webhook 帶的是我們的 id。在授權那一刻解析,因為那時兩邊你都有。
你的幂等鍵是什麼?recordingId。重試會重複它;而我們這邊超時的那次慢 2xx,在你那邊是一次成功。
你的端點宕機時會怎樣?我們重試三次後停止,之後用 listRecordings() 補回。先應答,解析之前先落盤。
你真的需要音頻嗎?那是另一個權限、另一次同意,而且裡面有不是你用戶的那些人的聲音。
收到 403 你的 App 怎麼做?用戶撤銷了你。停止同步、如實說明、提供重新連接。不要重試。
那你已經存下的呢?撤銷能停住流,收不回你存過的東西。請在你這邊也給用戶刪除入口——那是這個承諾裡屬於你的一半。