Connect — pair an app without copy-pasting a key
Getting an instanceId + sk_... API key into a piece of software normally means: log into the
Tarceel dashboard, open an instance, copy the key, paste it somewhere else. Connect is a one-click
alternative for a third-party app to get that same pair itself — modeled on how CLI tools like
gh/Docker authenticate (the Device Authorization
Grant, field names only, not full OAuth compliance) —
with no public callback URL required on your side, unlike a Stripe-Connect-style redirect.
That also makes it work for an integration with no public HTTPS endpoint of its own (a
self-hosted backend behind a firewall, for example).
The flow
- Your app calls
POST /connect/device-code. Tarceel returns adeviceCode(a long secret only your app ever sees), auserCode(a short human-readable code), and averificationUri. - Your app opens
verificationUriin a browser (or shows it as a link). - The user logs into their Tarceel account (if not already), picks an existing instance or creates a new one, and approves. This step happens on Tarceel's own site — it can't be done from inside your app, the same reason "Sign in with Google" briefly bounces to Google's own site even when triggered from inside another app.
- Meanwhile, your backend polls
POST /connect/tokenevery few seconds (theintervalfrom step 1) with thedeviceCode. The moment step 3 completes, that poll returns{ status: 'approved', instanceId, apiKey }— the only place the key is ever handed to you; it's never shown on the approval page itself. - Stop polling. A second poll after that returns
{ status: 'claimed' }with no key — the reveal happens exactly once, so if you miss it, the user needs to run the flow again.
Endpoints
POST /connect/device-code — unauthenticated
{ "appName": "My App" }
appName is optional but shown to the user on the approval page and recorded against the
resulting key (visible on the instance's detail page as "Connected via: ...") — always send
something identifiable.
Response (201):
{
"deviceCode": "9f2c...(64 hex chars)",
"userCode": "WKPX-7QRT",
"verificationUri": "https://app.tarceel.com/dashboard/connect?code=WKPX-7QRT",
"expiresIn": 600,
"interval": 5
}
expiresIn is seconds until the whole request expires unapproved (10 minutes).
POST /connect/token — unauthenticated
{ "deviceCode": "9f2c..." }
Always returns 200 with a status field — poll on a plain interval, don't treat any of these
as an error:
status | Meaning | instanceId / apiKey |
|---|---|---|
pending | Not yet approved or denied | both null |
denied | The user denied it | both null — stop polling |
expired | 10 minutes passed with no decision | both null — stop polling |
approved | This exact call just revealed the key | both present — save apiKey now, it never comes back |
claimed | Already revealed by an earlier poll | both null — stop polling |
A 404 means the device code is unknown — treat it the same as expired.
Worked example (Python)
import time
import requests
BASE_URL = "https://app.tarceel.com"
def connect():
resp = requests.post(f"{BASE_URL}/connect/device-code", json={"appName": "My App"})
resp.raise_for_status()
data = resp.json()
print(f"Open this link and approve: {data['verificationUri']}")
deadline = time.time() + data["expiresIn"]
while time.time() < deadline:
time.sleep(data["interval"])
poll = requests.post(f"{BASE_URL}/connect/token", json={"deviceCode": data["deviceCode"]})
if poll.status_code == 404:
raise RuntimeError("Device code no longer valid")
result = poll.json()
if result["status"] == "approved":
return result["instanceId"], result["apiKey"] # save both now
if result["status"] in ("denied", "expired", "claimed"):
raise RuntimeError(f"Connect flow ended: {result['status']}")
# else: still pending, keep polling
raise TimeoutError("User never approved the connect request")
Good to know
- Approving with an existing instance rotates its key (an instance only ever has one active key) — any other integration already using that key stops working until reconnected. The approval page shows this warning explicitly before it happens.
- Both endpoints above are unauthenticated and currently unrate-limited — each call is a single cheap lookup, so this isn't a concern at normal usage.