Integrating a service consumer
Your service — a hosted document editor, a voice assistant, a remote terminal, whatever — needs clipboard access. This walkthrough gets you from "we want to build this" to "user consented, we're operating on their clipboard, we close cleanly."
What you need before you start#
- A registered
service_id. For ScaiLabs services, this is baked into the SCSG registry. Third parties email platform@scailabs.ai — the flow lands an entry in the static registry plus a ScaiKey OAuth app. - OAuth2
client_credentialsfrom ScaiKey against your service identity. Follow the ScaiKey docs — the shape you get is a JWT withsub = cli_<service_id>_<instance>andtoken_type = "client_credentials". - The user's tenant must have opted your service in (
allow_scsg_<service_id>set onmod_scaiclip_tenant_policy). Tenant admins do this from the admin UI.
Choose your capabilities#
Ask for the minimum you need. Users see every capability in the consent prompt — overreach reads as untrustworthy.
| Bit | Name | When you need it |
|---|---|---|
0x01 |
READ | Any service that reads what the user copied |
0x02 |
WRITE | Any service that puts things back into the clipboard |
0x04 |
CLAIM | You consume ONE_SHOT clips (secrets, MFA codes) |
0x08 |
SCRATCH_READ | You read large files from central storage |
0x10 |
SCRATCH_WRITE | You upload large content back to central storage |
0x20 |
PUBLIC_SHARE | You mint public share URLs on behalf of the user |
Combine with |. Example: a voice assistant that reads and writes: CAP_READ | CAP_WRITE = 0x03.
Use the reference client#
The Python reference client (modules.scaiclip.services.scsg_client.SCSGClient in the ScaiGrid repo, distributable as a package once we cut one) wraps the WS protocol so you don't touch CBOR framing.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | |
The consent prompt#
When you call session.open, the grid pushes session.grant_request to every online device the user owns. The T1 client (native app, browser extension, mobile app) renders a prompt.
Craft the reason field like a real user will read it — because they will. Compare:
- Bad:
session=abc123 caps=0x03 scope=user_default - Good:
call with alice@example.com
Every user-facing field:
service_display_name— pulled from the registry ("ScaiWave (voice)"). You can't change this per-request.reason— free text you set atsession.open. Truncated to 512 chars.capabilities— rendered as human-readable lines ("Read what you copied", "Send things back to your clipboard").duration_max_s— rendered as wall-clock ("until 15:42 today" beats "for 3600 seconds").
Handling the terminal states#
Four ways a session ends. Your service must handle all of them.
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Grid also pushes session.expired mid-session at TTL. Your client's pump loop handles that — same shape as revoke.
Multi-session per process#
The client tracks multiple grants concurrently. Open one per user session:
1 2 3 4 5 | |
One WS connection, many grants. Reconnect handling is deliberately not built into the reference client — production services should wrap it themselves with backoff.
Rate limits#
You get 5 session.open calls per second per (tenant, service_id) with a 20-token burst. Beyond that: E_RATE_LIMITED with retry_after_ms.
You get 100 concurrent sessions per (tenant, service_id). Beyond that: E_QUOTA_EXCEEDED { scope, used, limit }.
If you routinely hit these, ask for an uplift.
Audit trail#
Every session you open lands in the tenant's audit_log:
scaiclip.scsg.session_openedat request time.scaiclip.scsg.session_grantedwhen the user consents.scaiclip.scsg.session_closed(ordenied/expired/revoked) at the end.
Tenant admins can grep for service_id = "your_service" to see every session your service has opened.
Common gotchas#
- You forgot to opt in the tenant.
session.openreturnsE_FORBIDDENwithservice_id + tenant_idindetail. Ask the tenant admin to flipallow_scsg_<your_service>. - The user has no online device.
E_NO_ONLINE_DEVICE. You cannot consent-prompt a user who isn't running any ScaiClip client. Surface a UX like "please open ScaiClip on your phone / desktop". - You requested
CLAIMbut the tenant hasn't opted in for one-shot consumption.session.opensucceeds; the firstclip.claimyou attempt returnsE_FORBIDDEN. Read requires opt-in ofallow_agent_claimat the tenant. - You touch a channel outside
granted_channel_ids.E_OUT_OF_SCOPE. The grant scope is enforced on every downstream verb. - You forgot to close. Grid times out at
duration_max_sand emitssession.expired, but leaving stale sessions eats your concurrent-session budget. Always close.
Related#
- Server-side integrations — conceptual overview.
- SCSG verb reference — every verb spec.
- Error codes — the
E_*you'll see.