Skip to content

Integrating non-platform services

You want LinkedIn search, or a niche CRM, or some industry-specific SaaS — and LinkWorld doesn’t ship a platform skill for it. There are three legitimate paths and one tempting dead end.

The headline. Partner-app code runs in a sandboxed container with restricted egress. You cannot run a headless browser, you cannot maintain a long-lived scraping session, and you cannot bypass the CSP. The platform can — and that’s the architecture for anything browser-shaped.

┌─ Does LinkWorld ship a platform skill? ─┐
│ │
YES NO
│ │
▼ ▼
Declare the scope. ┌─ Does a hosted API exist? ─┐
Use ctx.tools.call. │ │
YES NO
│ │
▼ ▼
Use BYO API key via ctx.secrets File a platform-skill
(ProxyCurl, Hunter, Stripe…) request — you need
a new first-party skill

Path 1: Platform skill exists → use the scope

Section titled “Path 1: Platform skill exists → use the scope”

Easy case. Declare the scope in your manifest, call the scoped tool, the platform handles auth + rate limit + audit.

required_scopes:
- mail.send
- linkedin.write
await ctx.tools.call("linkedin_post", text=draft)

See scope catalog for the full list of available services. If yours is there, you’re done.

Path 2: Hosted third-party API → BYO key

Section titled “Path 2: Hosted third-party API → BYO key”

If the service has a normal REST/HTTP API and the tenant has (or can sign up for) an account, this is the right path. Each tenant brings their own API key, your app stores it through ctx.secrets, your handler calls the API directly.

# manifest
install_settings:
- key: PROXYCURL_KEY
label: "ProxyCurl API key"
type: secret
required: true
async def lookup_profile(ctx, url: str):
key = await ctx.secrets.get("PROXYCURL_KEY")
if not key:
raise RuntimeError("Configure PROXYCURL_KEY in app settings.")
async with httpx.AsyncClient(timeout=20) as client:
resp = await client.get(
"https://nubela.co/proxycurl/api/v2/linkedin",
headers={"Authorization": f"Bearer {key}"},
params={"url": url},
)
resp.raise_for_status()
return resp.json()

See BYO API keys for the full pattern (graceful not-configured handling, two-tier dev-default+tenant-override, etc).

This is the pattern for: ProxyCurl, Hunter.io, Stripe, SendGrid, SerpAPI, Brave Search, Algolia, OpenAI (when not using the platform LLM), PostHog, Mixpanel, Notion, Linear, GitHub PATs, any AWS service, any GCP service.

Path 3: No hosted API → request a platform skill

Section titled “Path 3: No hosted API → request a platform skill”

If the only way to reach the service is through their web UI (LinkedIn lead-search, Sales Navigator, some legacy ERPs, niche industry portals) the answer isn’t a partner-app workaround. It’s a platform skill — written by the LinkWorld team, running outside the partner-app sandbox, with proper cookie management and session persistence.

Reference implementations:

SkillWhat it scrapesHow
facebook_groupsGroup feeds + commentsmbasic.facebook.com via httpx, cookies in SecretVault
whatsapp_personalPersonal WhatsApp messagesneonize (Go client port), QR-pair per user, leader-gated hub
reddit_userUser-authenticated RedditOAuth cookies via SecretVault

What goes into a platform-skill request (open a GitHub issue):

  • Service + URL of the page(s) to scrape
  • Tenant-auth model: do tenants log in once and we store the cookie? Per-user or per-tenant? OAuth or session-cookie?
  • Rate-limit profile: how often can it be safely called? (Most B2B sites tolerate ~1-2 req/sec per cookie.)
  • Tool shape: 1-3 specific operations you’d call from your app (linkedin_search_people, linkedin_send_dm, etc.) — not “a browser”.
  • Compliance posture: ToS implications, GDPR considerations, user-consent requirements.

The platform team writes it under packages/core/src/skills/<service>/, adds a scope in scope_registry.py, and your app declares the scope.

Don’t: try to scrape from inside the partner-app sandbox

Section titled “Don’t: try to scrape from inside the partner-app sandbox”
# ❌ DON'T do this in a partner app
from playwright.async_api import async_playwright
async def scrape_linkedin(ctx, url):
async with async_playwright() as p:
browser = await p.chromium.launch()
# ...

Why it won’t work, and shouldn’t:

  • The partner-app sandbox doesn’t have Chromium / Playwright pre-installed; you’d need to ship hundreds of megabytes of binary in your bundle.
  • Egress restrictions in the sandbox block long-lived browser sessions and JS-heavy page loads.
  • Even if you got past that, the cookie/session would belong to the bundle’s runtime user, not a real tenant identity — the service you’re scraping would detect a serverless-looking fingerprint and ban quickly.
  • ToS enforcement falls on you as the app developer, not on LinkWorld. You’d be one cease-and-desist away from delisting.

The platform-skill path puts the cookie management, fingerprint matching, and ToS posture on the platform side where they can be operationalized.

Don’t: build a “thin proxy” through your own external server

Section titled “Don’t: build a “thin proxy” through your own external server”
partner-app sandbox → your-own-server.example.com → target service

You’re back to running your own infrastructure — the very thing LinkWorld is supposed to remove. Plus: your server now holds tenant credentials, has to be SOC2’d separately, has to scale with tenant count, and is a fresh attack surface. If you find yourself reaching for this, file the platform-skill request instead.

Don’t: scrape with the tenant’s M365 / Google credentials

Section titled “Don’t: scrape with the tenant’s M365 / Google credentials”
# ❌ DON'T do this
# Using mail.read to read a tenant's mail, then automated logins to
# other sites with credentials harvested from welcome-mails

Credentials from one connector can’t be used for unrelated services. Even if you technically could, doing it would violate both the scope contract (mail.read is read-only, not auth-bridge) and tenant consent. Each integration auths separately.

Don’t: hardcode the data provider — abstract behind a config

Section titled “Don’t: hardcode the data provider — abstract behind a config”

If your app supports lookups via ProxyCurl, but tomorrow you want to add People Data Labs as an alternative, don’t bake the choice in. Expose it through install_settings:

install_settings:
- key: LEAD_PROVIDER
label: "Lead enrichment provider"
type: enum
options: [proxycurl, peopledatalabs]
default: proxycurl

Then route in your handler:

provider = ctx.config.get("LEAD_PROVIDER", "proxycurl")
key = await ctx.secrets.get(f"{provider.upper()}_KEY")

This is also how you avoid lock-in to a provider whose pricing shifts.

  • You’re building the platform skill itself, not consuming one. Then the BROWSER_AUTOMATION internal docs in docs/skills/BROWSER_AUTOMATION.md apply — full session management, CAPTCHA hooks, recovery strategies — but that surface is for platform contributors, not partner apps.
  • You need a one-shot, manual scrape (e.g. “user pastes a Sales Nav URL”). You can still call a hosted scraper API (ProxyCurl etc.) on that URL via the BYO-key path — you just can’t drive the browser yourself.