App Review playbook
What to request on your own provider app, per Koil capability, and what each provider's review takes.
Koil runs every live connection through your own provider app, so the permissions a capability needs are ones your app must hold. This page says which. The tables are generated from the same declarations that admit a grant at runtime (the data GET /v1/providers serves, rendered when the docs are built): a capability's scopes are every scope any call it may make needs, so request what the table says for the capabilities you use and nothing more. Provider review is faster for a narrower ask.
Each capability lists the credentials any one of which runs it. A grant credential is the token your user authorized; a connect credential is one the provider issues for the profile when it is connected (a Facebook Page access token, minted by Koil from the grant); an app credential is your app's own OAuth client, for configuration Koil performs on your app's behalf. A capability with several credentials runs with whichever your app authorizes through.
The notes around the tables are written by hand: review timelines, app permission names, quota arithmetic and known difficulty live nowhere in code.
Koil connects Facebook Pages through your Meta app with Facebook Login (auth provider facebook). Every permission below needs Advanced Access, which is App Review plus Business Verification, for Pages your app's own developers do not administer.
pages_manage_metadata is the one to watch. It is needed by the once-per-profile webhook subscription (webhooks.subscribe), not by any read or write, so a customer who reasons from the comment operations alone requests a set that passes every runtime check and still leaves their Page silently unsubscribed. A grant without it still connects: reads and publishes work, the subscription is refused before any Meta call, and the reason is recorded on the profile's credential Connection. pages_messaging goes through Messenger use-case review, the slowest of the set.
Scopes to request
| Auth provider | Every scope any capability needs |
|---|---|
facebook | pages_manage_engagement, pages_manage_metadata, pages_manage_posts, pages_messaging, pages_read_engagement, pages_read_user_content, pages_show_list, publish_video |
Credentials
facebook_page_token(facebook): a token the provider issues for the profile when it is connected, bound tofacebookPageId.facebook_user_token(facebook): the token your user authorized.
Capabilities
| Capability | Credential | Scopes |
|---|---|---|
dm.messages.list | facebook_page_token | pages_manage_metadata, pages_messaging, pages_read_engagement, pages_show_list |
dm.messages.publish | facebook_page_token | pages_manage_metadata, pages_messaging, pages_read_engagement, pages_show_list |
dm.threads.get | facebook_page_token | pages_manage_metadata, pages_messaging, pages_read_engagement, pages_show_list |
dm.threads.list | facebook_page_token | pages_manage_metadata, pages_messaging, pages_read_engagement, pages_show_list |
media.comments.delete | facebook_page_token | pages_manage_engagement, pages_read_engagement, pages_read_user_content, pages_show_list |
media.comments.get | facebook_page_token | pages_read_engagement, pages_read_user_content, pages_show_list |
media.comments.list | facebook_page_token | pages_read_engagement, pages_read_user_content, pages_show_list |
media.comments.moderate | facebook_page_token | pages_manage_engagement, pages_read_engagement, pages_read_user_content, pages_show_list |
media.comments.publish | facebook_page_token | pages_manage_engagement, pages_read_engagement, pages_read_user_content, pages_show_list |
media.items.get | facebook_page_token | pages_read_engagement, pages_show_list |
media.items.list | facebook_page_token | pages_read_engagement, pages_show_list |
media.items.publish | facebook_page_token | pages_manage_posts, pages_read_engagement, pages_show_list, publish_video |
profiles.connect | facebook_user_token | pages_show_list |
profiles.discover | facebook_user_token | pages_show_list |
stories.items.list | facebook_page_token | pages_read_engagement, pages_show_list |
stories.items.publish | facebook_page_token | pages_manage_posts, pages_read_engagement, pages_show_list, publish_video |
webhooks.subscribe | facebook_page_token | pages_manage_metadata, pages_show_list |
Not offered
dm.messages.delete: provider not supporteddm.messages.get: provider not supporteddm.messages.moderate: provider not supporteddm.messages.update: provider not supporteddm.threads.delete: provider not supporteddm.threads.moderate: provider not supporteddm.threads.publish: provider not supporteddm.threads.update: provider not supportedmedia.comments.update: provider not supportedmedia.items.delete: provider not supportedmedia.items.moderate: provider not supportedmedia.items.update: provider not supportedmedia.mentions.delete: provider not supportedmedia.mentions.get: not applicablemedia.mentions.list: provider no endpointmedia.mentions.moderate: provider not supportedmedia.mentions.publish: provider not supportedmedia.mentions.update: provider not supportedstories.items.delete: provider not supportedstories.items.get: provider no endpointstories.items.moderate: provider not supportedstories.items.update: provider not supportedwebhooks.reconcile: not applicable — This provider does not configure webhooks per customer app.
Koil reaches Instagram professional accounts two ways, and the permission names differ: through Facebook Login (facebook) the account is reached via the Page it is connected to and acts with that Page's access token; through Instagram Login (instagram) it is reached directly with the grant. Profile discovery and connect are Facebook Login only, on purpose: Koil finds Instagram accounts through the Pages a user has a role on and connect mints that Page's access token, which Instagram Messaging requires and which an Instagram Login grant cannot produce.
Known review difficulty: instagram_manage_messages is the slow one, and Advanced Access needs a screencast showing the data in use.
Scopes to request
| Auth provider | Every scope any capability needs |
|---|---|
facebook | instagram_basic, instagram_content_publish, instagram_manage_comments, instagram_manage_messages, pages_read_engagement, pages_show_list |
instagram | instagram_business_basic, instagram_business_content_publish, instagram_business_manage_comments, instagram_business_manage_messages |
Credentials
facebook_user_token(facebook): the token your user authorized.instagram_login_token(instagram): the token your user authorized, bound toinstagramUserId.instagram_page_token(facebook): a token the provider issues for the profile when it is connected, bound tofacebookPageId,instagramUserId.
Capabilities
| Capability | Credential | Scopes |
|---|---|---|
dm.messages.list | instagram_page_token | instagram_manage_messages, pages_read_engagement, pages_show_list |
instagram_login_token | instagram_business_basic, instagram_business_manage_messages | |
dm.messages.publish | instagram_page_token | instagram_manage_messages, pages_read_engagement, pages_show_list |
instagram_login_token | instagram_business_basic, instagram_business_manage_messages | |
dm.threads.list | instagram_page_token | instagram_manage_messages, pages_read_engagement, pages_show_list |
instagram_login_token | instagram_business_basic, instagram_business_manage_messages | |
media.comments.get | instagram_page_token | instagram_basic, pages_read_engagement |
instagram_login_token | instagram_business_basic | |
media.comments.list | instagram_page_token | instagram_basic, pages_read_engagement |
instagram_login_token | instagram_business_basic | |
media.comments.moderate | instagram_page_token | instagram_basic, instagram_manage_comments, pages_read_engagement |
instagram_login_token | instagram_business_basic, instagram_business_manage_comments | |
media.comments.publish | instagram_page_token | instagram_basic, instagram_manage_comments, pages_read_engagement |
instagram_login_token | instagram_business_basic, instagram_business_manage_comments | |
media.items.get | instagram_page_token | instagram_basic, pages_read_engagement |
instagram_login_token | instagram_business_basic | |
media.items.list | instagram_page_token | instagram_basic, pages_read_engagement |
instagram_login_token | instagram_business_basic | |
media.items.publish | instagram_page_token | instagram_basic, instagram_content_publish, pages_read_engagement |
instagram_login_token | instagram_business_basic, instagram_business_content_publish | |
profiles.connect | facebook_user_token | pages_show_list |
profiles.discover | facebook_user_token | instagram_basic, pages_show_list |
stories.items.get | instagram_page_token | instagram_basic, pages_read_engagement |
instagram_login_token | instagram_business_basic | |
stories.items.list | instagram_page_token | instagram_basic |
instagram_login_token | instagram_business_basic | |
stories.items.publish | instagram_page_token | instagram_basic, instagram_content_publish, pages_read_engagement |
instagram_login_token | instagram_business_basic, instagram_business_content_publish |
Not offered
dm.messages.delete: provider not supporteddm.messages.get: provider not supporteddm.messages.moderate: provider not supporteddm.messages.update: provider not supporteddm.threads.delete: provider not supporteddm.threads.get: provider not supporteddm.threads.moderate: provider not supporteddm.threads.publish: provider not supporteddm.threads.update: provider not supportedmedia.comments.delete: provider not supportedmedia.comments.update: provider not supportedmedia.items.delete: provider not supportedmedia.items.moderate: provider not supportedmedia.items.update: provider not supportedmedia.mentions.delete: provider not supportedmedia.mentions.get: not applicablemedia.mentions.list: provider no endpointmedia.mentions.moderate: provider not supportedmedia.mentions.publish: provider not supportedmedia.mentions.update: provider not supportedstories.items.delete: provider not supportedstories.items.moderate: provider not supportedstories.items.update: provider not supportedwebhooks.reconcile: not applicable — This provider does not configure webhooks per customer app.webhooks.subscribe: not implemented — Instagram deliveries ride the app subscription of the linked Facebook Page, which this provider does not manage yet.
Koil connects WhatsApp Business Platform numbers through your Meta app via Facebook Login for Business (Embedded Signup), so the auth provider is facebook and the grant is a business token scoped to the WhatsApp Business Accounts the user shared. Both WhatsApp permissions need Advanced Access and Business Verification. business_management is for the Business Portfolio walk discovery falls back to when a token names no specific accounts.
There is no read capability to request: the Cloud API keeps no message history and exposes no conversation list, so inbound messages and delivery statuses arrive only as events.
Scopes to request
| Auth provider | Every scope any capability needs |
|---|---|
facebook | business_management, whatsapp_business_management, whatsapp_business_messaging |
Credentials
facebook_user_token(facebook): the token your user authorized.whatsapp_number_token(facebook): the token your user authorized, bound towhatsappBusinessAccountId,whatsappPhoneNumberId.
Capabilities
| Capability | Credential | Scopes |
|---|---|---|
dm.messages.publish | whatsapp_number_token | whatsapp_business_management, whatsapp_business_messaging |
profiles.discover | facebook_user_token | business_management, whatsapp_business_management |
webhooks.subscribe | whatsapp_number_token | whatsapp_business_management |
Not offered
dm.messages.delete: provider not supporteddm.messages.get: provider no endpointdm.messages.list: provider no endpointdm.messages.moderate: provider not supporteddm.messages.update: provider not supporteddm.statuses.delete: not applicabledm.statuses.get: not applicabledm.statuses.list: provider no endpointdm.statuses.moderate: not applicabledm.statuses.publish: not applicabledm.statuses.update: not applicableprofiles.connect: not applicable — A WhatsApp profile acts with the Embedded Signup grant's business token; there is no per-number credential to mint.webhooks.reconcile: not applicable — This provider does not configure webhooks per customer app.
TikTok
Koil connects TikTok through your TikTok API for Business app only (auth provider tiktok-accounts). Register as a developer with a company-domain email, create an app with the TikTok Accounts permission, and request scopes per surface through the Accounts API Access Application Form. TikTok's app permissions map onto the scopes as: Business User (user.info.*, user.account.type), Business Media (video.list), Business Content (video.publish, video.upload), Get Business Comment (comment.list), Manage Business Comment (comment.list.manage), Mentions (biz.brand.insights), Business Messaging (message.list.*).
Koil registers your app's webhook callbacks itself (webhooks.reconcile, with your app's client and no user scope), one subscription per event type, and only while some Event Destination is interested. Known review difficulty: Business Messaging is the slow scope (weeks; DSPR review, and USDS review for US accounts). Publishing additionally requires the media host to be a URL property your app has verified; see TikTok publishing.
Scopes to request
| Auth provider | Every scope any capability needs |
|---|---|
tiktok-accounts | biz.brand.insights, comment.list, comment.list.manage, message.list.read, message.list.send, user.account.type, user.info.basic, user.info.username, video.list, video.publish, video.upload |
Credentials
tiktok_account_token(tiktok-accounts): the token your user authorized, bound totiktokBusinessId.tiktok_app(tiktok-accounts): your app's own OAuth client.tiktok_grant(tiktok-accounts): the token your user authorized.
Capabilities
| Capability | Credential | Scopes |
|---|---|---|
dm.messages.list | tiktok_account_token | message.list.read |
dm.messages.publish | tiktok_account_token | message.list.send |
dm.threads.list | tiktok_account_token | message.list.read |
posts.comments.delete | tiktok_account_token | comment.list.manage |
posts.comments.get | tiktok_account_token | comment.list |
posts.comments.list | tiktok_account_token | comment.list |
posts.comments.moderate | tiktok_account_token | comment.list.manage |
posts.comments.publish | tiktok_account_token | comment.list.manage |
posts.items.get | tiktok_account_token | video.list |
posts.items.list | tiktok_account_token | video.list |
posts.items.publish | tiktok_account_token | video.publish, video.upload |
posts.mentions.get | tiktok_account_token | biz.brand.insights, comment.list |
posts.mentions.list | tiktok_account_token | biz.brand.insights, comment.list |
profiles.discover | tiktok_account_token | user.account.type, user.info.basic, user.info.username |
tiktok_grant | user.account.type, user.info.basic, user.info.username | |
webhooks.reconcile | tiktok_app | none |
Not offered
dm.messages.delete: provider not supporteddm.messages.get: provider no endpointdm.messages.moderate: provider not supporteddm.messages.update: provider not supporteddm.threads.delete: provider no endpointdm.threads.get: provider no endpointdm.threads.moderate: provider not supporteddm.threads.publish: not applicabledm.threads.update: not applicableposts.comments.update: provider not supportedposts.items.delete: provider no endpointposts.items.moderate: provider not supportedposts.items.update: provider no endpointposts.mentions.delete: not applicableposts.mentions.moderate: provider not supportedposts.mentions.publish: not applicableposts.mentions.update: not applicableprofiles.connect: not applicable — A TikTok profile acts with the grant token itself: TikTok issues one token per account, so there is no credential to mint at connect.webhooks.subscribe: not applicable — TikTok registers webhook callbacks per developer app, not per account; the provider's webhook registration operation reconciles them from destination interest.
YouTube
Koil connects YouTube through your Google Cloud project (auth provider youtube). Enable the YouTube Data API v3 on the project and configure the OAuth consent screen for the one scope below: Google gates every Data API method on "at least one of" a scope set, youtube.force-ssl is in every set Koil's surfaces touch, and it is the only scope that unlocks comment writes, so one scope keeps the consent screen and the verification to a single item. A grant carrying only youtube.readonly is refused with scope_insufficient.
It is a sensitive scope: verification needs a homepage, a privacy policy on the same domain, Search Console domain proof, a per-scope justification and an unlisted demo video, typically three to five business days. It is not a restricted scope, so no CASA assessment. Uploads from a project that has not passed the YouTube API Services compliance audit are made private by YouTube whatever privacyStatus you asked for, and an OAuth app still in Testing expires every grant seven days after consent. The daily quota (10,000 units) is per project; Koil's polling spends one or two units per poll, so your own writes are what spend it.
Scopes to request
| Auth provider | Every scope any capability needs |
|---|---|
youtube | https://www.googleapis.com/auth/youtube.force-ssl |
Credentials
youtube_channel_token(youtube): the token your user authorized, bound toyoutubeChannelId.youtube_grant(youtube): the token your user authorized.
Capabilities
| Capability | Credential | Scopes |
|---|---|---|
profiles.discover | youtube_channel_token | https://www.googleapis.com/auth/youtube.force-ssl |
youtube_grant | https://www.googleapis.com/auth/youtube.force-ssl | |
videos.comments.delete | youtube_channel_token | https://www.googleapis.com/auth/youtube.force-ssl |
videos.comments.get | youtube_channel_token | https://www.googleapis.com/auth/youtube.force-ssl |
videos.comments.list | youtube_channel_token | https://www.googleapis.com/auth/youtube.force-ssl |
videos.comments.moderate | youtube_channel_token | https://www.googleapis.com/auth/youtube.force-ssl |
videos.comments.publish | youtube_channel_token | https://www.googleapis.com/auth/youtube.force-ssl |
videos.comments.update | youtube_channel_token | https://www.googleapis.com/auth/youtube.force-ssl |
videos.items.delete | youtube_channel_token | https://www.googleapis.com/auth/youtube.force-ssl |
videos.items.get | youtube_channel_token | https://www.googleapis.com/auth/youtube.force-ssl |
videos.items.list | youtube_channel_token | https://www.googleapis.com/auth/youtube.force-ssl |
videos.items.publish | youtube_channel_token | https://www.googleapis.com/auth/youtube.force-ssl |
videos.items.update | youtube_channel_token | https://www.googleapis.com/auth/youtube.force-ssl |
Not offered
profiles.connect: not applicable — A YouTube profile acts with the grant token itself; a Google OAuth token is the channel's own credential, so there is nothing to mint at connect.videos.items.moderate: not applicablewebhooks.reconcile: not applicable — This provider does not configure webhooks per customer app.webhooks.subscribe: not applicable — YouTube publishes no comment webhooks to subscribe the customer's app to; PubSubHubbub announces uploads only, and every inbound event is polled.
Google Business Profile
Koil connects Google Business Profile through your Google Cloud project (auth provider google-business-profile). One scope covers every Business Profile service, so the consent screen and the verification are a single item — but the scope is not the gate here.
The gate is an access application, and it comes before your first call. An unapproved project is capped at zero queries per minute: not a slow start, no calls at all. Enable the Business Profile APIs on the project, then file the "Application for Basic API Access" from an email listed as an owner or manager on the profile. Google's stated prerequisites are a Business Profile verified and active for 60+ days, a website representing that business, and a profile kept complete and current. No turnaround is published; quota is the signal, since approval moves the project from 0 to 300 QPM. Google's own error for an unapproved project never says why — it arrives as a throttle or a bare permission error — so Koil's platform_rate_limit and scope_insufficient for this provider both name the access application in their message.
business.manage is a sensitive scope, so OAuth verification applies on top: a homepage, a privacy policy on the same domain, Search Console domain proof, a per-scope justification and an unlisted demo video. Replying additionally requires the location to be verified, and in a Workspace tenant requires the admin to have enabled Google Search and/or Maps for the organization.
One grant discovers every location under every account it administers, so a chain connects all its shops from one Connection — and each location is a Connected Profile, which is what the platform fee is charged per.
Scopes to request
| Auth provider | Every scope any capability needs |
|---|---|
google-business-profile | https://www.googleapis.com/auth/business.manage |
Credentials
google_business_grant(google-business-profile): the token your user authorized.google_business_location_token(google-business-profile): the token your user authorized, bound togoogleBusinessAccountId,googleBusinessLocationId.
Capabilities
| Capability | Credential | Scopes |
|---|---|---|
locations.review_replies.delete | google_business_location_token | https://www.googleapis.com/auth/business.manage |
locations.review_replies.get | google_business_location_token | https://www.googleapis.com/auth/business.manage |
locations.review_replies.publish | google_business_location_token | https://www.googleapis.com/auth/business.manage |
locations.reviews.get | google_business_location_token | https://www.googleapis.com/auth/business.manage |
locations.reviews.list | google_business_location_token | https://www.googleapis.com/auth/business.manage |
profiles.discover | google_business_grant | https://www.googleapis.com/auth/business.manage |
google_business_location_token | https://www.googleapis.com/auth/business.manage |
Not offered
locations.review_replies.list: provider no endpoint — Google exposes no listing of replies; each appears on the review it answers.locations.review_replies.moderate: not applicable — Google moderates the owner's reply, not Koil.locations.review_replies.update: not applicable — Publishing again for the same review replaces the reply; Google keeps one per review.locations.reviews.delete: provider not supported — Google exposes no method that deletes a review; flagging one for a policy violation is a Business Profile UI action with no API.locations.reviews.moderate: provider not supported — Google exposes no hide, spam, or report method on a review. Replying is the only owner action, on google_business_profile.locations.review_replies.locations.reviews.publish: not applicable — A review is written by a customer on Google, never by the business.locations.reviews.update: provider not supported — Google exposes no method that edits a review.profiles.connect: not applicable — A Business Profile profile acts with the grant token itself; a Google OAuth token carries the whole Business Profile surface, so there is nothing to mint at connect.webhooks.reconcile: not applicable — This provider does not configure webhooks per customer app.webhooks.subscribe: not applicable — Google publishes review notifications to a Cloud Pub/Sub topic rather than an HTTP callback, so Koil polls; polling needs no per-profile setup.
Koil connects LinkedIn Pages through your LinkedIn developer app with the Community Management API product (auth provider linkedin). The member who connects must hold an approved role on the Page; one member discovers every Page they hold a role on, and each Page is a Connected Profile.
The gate is product access, and it comes in two tiers, both before any call. Request Development Tier from your app's Products tab: LinkedIn requires a registered legal organization, a verified business email, and the app verified by a super admin of the LinkedIn Page it belongs to. Development Tier can only be requested from a new app with no other API products — an organization that already holds Advertising API access creates a second app. It allows 500 calls per app and 100 per member per day, which is enough to integrate and far too little to run an inbox, and it has comment notifications switched off. Standard Tier is a second form with a screencast showing the OAuth flow, a post created through your app, a member's comment rendered in it, and which fields of the commenter's profile you display. A rejection at either tier cannot be appealed on the same app. No turnaround is published for either.
The scopes split into two families LinkedIn grants separately: the Page's own posts (r_organization_social, w_organization_social) and the social actions on them (r_organization_social_feed, w_organization_social_feed), plus rw_organization_admin for discovery. Requesting a different set later invalidates every existing token for your app, so each Page's member must re-authorize: request the full set for the capabilities you will use up front.
Two obligations come with LinkedIn's terms rather than its review. Your agreement with Koil must be at least as protective of LinkedIn's APIs and content as LinkedIn's own terms, and you must keep a list of the independent contractors you share your LinkedIn credentials with, naming Koil. And LinkedIn's storage rules apply to what you keep from Koil's events: a member's comment text may be stored for 48 hours, and a commenter's profile data cached for 24 hours and never stored.
Scopes to request
| Auth provider | Every scope any capability needs |
|---|---|
linkedin | r_organization_social, r_organization_social_feed, rw_organization_admin, w_organization_social, w_organization_social_feed |
Credentials
linkedin_member_grant(linkedin): the token your user authorized.linkedin_organization_token(linkedin): the token your user authorized, bound tolinkedinMemberId,linkedinOrganizationId.
Capabilities
| Capability | Credential | Scopes |
|---|---|---|
feed.comments.delete | linkedin_organization_token | w_organization_social_feed |
feed.comments.get | linkedin_organization_token | r_organization_social_feed |
feed.comments.list | linkedin_organization_token | r_organization_social_feed |
feed.comments.publish | linkedin_organization_token | r_organization_social_feed, w_organization_social_feed |
feed.items.delete | linkedin_organization_token | w_organization_social |
feed.items.get | linkedin_organization_token | r_organization_social |
feed.items.list | linkedin_organization_token | r_organization_social |
feed.items.publish | linkedin_organization_token | w_organization_social |
feed.reactions.delete | linkedin_organization_token | w_organization_social_feed |
feed.reactions.get | linkedin_organization_token | r_organization_social_feed |
feed.reactions.list | linkedin_organization_token | r_organization_social_feed |
feed.reactions.publish | linkedin_organization_token | w_organization_social_feed |
profiles.discover | linkedin_member_grant | rw_organization_admin |
linkedin_organization_token | rw_organization_admin | |
webhooks.subscribe | linkedin_organization_token | rw_organization_admin |
Not offered
feed.comments.moderate: provider not supported — LinkedIn has no hide, spam or review action on a comment. The only thread-wide control, closing comments on a post, deletes every existing comment on it, so Koil does not offer it as moderation.feed.comments.update: not implementedfeed.items.moderate: provider not supported — LinkedIn exposes no moderation action on a post.feed.items.update: not implementedfeed.mentions.delete: not applicablefeed.mentions.get: provider no endpoint — Reading a member's post needs r_member_social, which LinkedIn has closed to new requests.feed.mentions.list: provider no endpoint — Reading a member's post needs r_member_social, which LinkedIn has closed to new requests; mentions arrive only as events.feed.mentions.moderate: provider not supportedfeed.mentions.publish: not applicable — A mention is written by a member, never by the Page.feed.mentions.update: not applicablefeed.reactions.moderate: provider not supportedfeed.reactions.update: not applicable — Publishing a reaction again replaces the Page's reaction on that entity.profiles.connect: not applicable — A LinkedIn profile acts with the member's grant token itself: LinkedIn issues one token per member, valid for every Page they hold a role on, so there is nothing to mint at connect.webhooks.reconcile: not applicable — This provider does not configure webhooks per customer app.
Bring your own provider app
Koil runs every live connection through your own provider OAuth app — what you need, how to set it up, and how test mode lets you integrate before app review completes.
API overview
How the Koil API is shaped — resources, ids, content addressing — and where each part is documented.