https://app.roamtale.io/auth/youtubeyoutube.readonly (read own channel stats & comments) ·
youtube.force-ssl (upload own videos & reply to comments on own videos)
What the app is: RoamTale is a multi-tenant SaaS application for travel and lifestyle content creators. Each creator signs up for their own isolated account ("tenant") and connects their own YouTube channel via Google OAuth. Every API call is performed exclusively on the authenticated user's own channel, at the user's explicit request. RoamTale never reads from or writes to any other user's channel, and never sells or shares user data. Each tenant's data is stored in a separate, isolated data area.
Standard Google OAuth 2.0 Authorization Code flow with offline access (refresh tokens).
Implementation: YouTubeOAuthService.cs; callback endpoint /auth/youtube;
UI entry point: Personas → <persona> → Profile & Platforms → YouTube → "Connect with YouTube".
https://accounts.google.com/o/oauth2/v2/auth with client_id,
redirect_uri=https://app.roamtale.io/auth/youtube, response_type=code,
scope=youtube.readonly youtube.force-ssl, access_type=offline,
prompt=consent and a random state./auth/youtube?code=…&state=…; the app validates
state and exchanges the code at https://oauth2.googleapis.com/token
(grant_type=authorization_code) for an access_token + refresh_token.GET /youtube/v3/channels?part=id&mine=true; tokens +
channel id are stored per persona, inside that creator's isolated tenant area.grant_type=refresh_token) ~20 min before expiry, so automation never uses an expired token.https://oauth2.googleapis.com/revoke
and deletes the stored tokens for that tenant.Implementation: YouTubePublisher.cs. The creator uploads one of their own generated videos to
their own channel via videos.insert (multipart):
POST https://www.googleapis.com/upload/youtube/v3/videos?uploadType=multipart&part=snippet,status
Authorization: Bearer <access_token>
Content-Type: multipart/related
part 1 (application/json):
{ "snippet": { "title": …, "description": …, "categoryId": "22" },
"status": { "privacyStatus": "public", "selfDeclaredMadeForKids": false } }
part 2 (video/mp4): <the creator's own video bytes>
Short uploads append #Shorts so YouTube classifies the vertical (<60 s) clip as a Short.
The API returns the new video id; the app builds the watch/Shorts URL and shows a success message. Quota/error
responses (e.g. quotaExceeded) are surfaced to the creator verbatim
(the quota ceiling is the reason for this quota-increase request).
Implementation: Connectors.cs → YouTubeConnector.GetStatsAsync, pulled periodically by
MetricsPullService and shown on the Dashboard (/). Read-only, own channel only:
GET /youtube/v3/channels?part=statistics,contentDetails&id={channelId}
→ subscriberCount, videoCount, uploads playlist.GET /youtube/v3/videos?part=statistics&id={videoIds} → sums per-video
viewCount + likeCount (accurate for Shorts, where the channel aggregate lags).The dashboard displays the connected channel's subscriber count, total views, total likes and video count — only on that creator's own private dashboard. No data from any other channel is read or displayed.
Implementation: Connectors.cs (YouTubeConnector) + SocialAutomationService.cs;
UI: the Inbox page.
GET /youtube/v3/commentThreads?allThreadsRelatedToChannelId={channelId}&part=snippet.POST /youtube/v3/comments?part=snippet with
{ "snippet": { "parentId": <comment id>, "textOriginal": <reply text> } } —
posted as the channel owner, only on the creator's own videos, at the creator's request
(optionally assisted by an AI-drafted suggestion that the creator sends).The app never reads or writes comments on videos that do not belong to the connected creator's own channel.
{TENANT_DATA_ROOT}/config/personas.json); never sent to any third party.https://app.roamtale.io); the OAuth redirect
URI and the OAuth consent screen domain match this production domain.| Requirement | Scope | Code | UI |
|---|---|---|---|
| OAuth login | both | YouTubeOAuthService.cs, /auth/youtube | Personas → YouTube → "Connect" |
| Upload videos | force-ssl | YouTubePublisher.cs (videos.insert) | Persona → "Upload as Short / video" |
| Channel analytics | readonly | Connectors.cs YouTubeConnector (channels.list + videos.list) | Dashboard / |
| Reply to comments | readonly + force-ssl | Connectors.cs (commentThreads.list, comments.insert) | Inbox |
| Disconnect / revoke | — | YouTubeOAuthService.DisconnectAsync (oauth2/revoke) | YouTube → "Disconnect" |