OAuth apps
Use OAuth when your platform serves many Talki customers: each one connects your
app from a consent screen instead of pasting an API key. The resulting token
reaches the same /v1 paths and scopes as a key.
Register an app
Section titled “Register an app”Register it in the console. You get:
- a client ID,
talki_cid_…, which is public; - a client secret,
talki_cs_…, shown once. Keep it server-side: there are no public clients, so every token request authenticates with it.
A new app is pending until it is reviewed, and only an approved app can be authorized. Adding a redirect URI or a scope, or changing the app’s name, website or logo, sends an approved app back to review.
Redirect URIs must be https:// (plain http:// only for localhost,
127.0.0.1 and ::1), without a fragment, credentials or wildcards. Up to 10.
The authorization server metadata is at:
https://api.talki.it/.well-known/oauth-authorization-serverAuthorize
Section titled “Authorize”Send the customer’s browser to the consent page:
https://app.talki.it/autorizza ?response_type=code &client_id=talki_cid_4Hq2mZ8rVt1LxN3pYw7KcA &redirect_uri=https%3A%2F%2Fcrm.example.com%2Ftalki%2Fcallback &scope=calls%3Aread%20appointments%3Aread &state=Zk3q9… &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM &code_challenge_method=S256| Parameter | Rule |
|---|---|
response_type |
code |
redirect_uri |
Exactly one of the app’s registered URIs, byte for byte |
scope |
Space-separated; each must be among the app’s registered scopes |
state |
Recommended; up to 500 characters, returned unchanged |
code_challenge |
Required: the base64url SHA-256 of your code_verifier |
code_challenge_method |
Required: S256. plain is refused |
prompt |
Optional; the only accepted value is consent |
The customer signs in to Talki and approves. They may untick scopes, so read what was granted from the token response, never from your request.
The browser returns to your redirect_uri with code, state and iss. Check that state is yours
and iss is https://api.talki.it. A refusal, or a problem with a parameter
other than the client or redirect URI, arrives as error and
error_description instead — access_denied when the customer declines. An
unknown client or an unregistered redirect URI is never redirected; the customer
sees the error on Talki’s page.
Reconnecting
Section titled “Reconnecting”Authorizing again adds the new scopes to what the account already granted. To
replace the grant with exactly the scopes requested — to narrow it — send
prompt=consent.
Every successful authorization invalidates the installation’s earlier tokens, even when the scopes are unchanged. Keep only the newest pair.
Exchange the code
Section titled “Exchange the code”curl --fail-with-body https://api.talki.it/oauth/token \ -u "$TALKI_CLIENT_ID:$TALKI_CLIENT_SECRET" \ -d grant_type=authorization_code \ -d code="$CODE" \ -d redirect_uri=https://crm.example.com/talki/callback \ -d code_verifier="$CODE_VERIFIER"Authenticate with HTTP Basic, or with client_id and client_secret in the
form body — not both. redirect_uri must be sent again and match.
{ "access_token": "talki_at_…", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "talki_rt_…", "scope": "appointments:read calls:read", "account_id": "f7e39b9a-1ac8-45e4-93da-2600f17cb286", "installation_id": "5d0e8a2c-7b41-4f7e-9c3a-1e2f4b6d8a90", "livemode": true, "account_name": "Studio Bellini"}A code is valid for 5 minutes and works once:
- Presenting a spent code again revokes every token of that installation. Treat a duplicate callback as an error, not a retry.
- Exchanging a code invalidates any older, unexchanged code for the same app and
account; those return
invalid_grant.
Store tokens per installation_id. Use the access token exactly like an API key:
Authorization: Bearer talki_at_…Refresh
Section titled “Refresh”Access tokens last 1 hour. A refresh token lasts 60 days from when it was issued, and each refresh issues a new one, so an installation in use never expires.
curl --fail-with-body https://api.talki.it/oauth/token \ -u "$TALKI_CLIENT_ID:$TALKI_CLIENT_SECRET" \ -d grant_type=refresh_token \ -d refresh_token="$REFRESH_TOKEN"The refresh token rotates on every use: store the new one before using the new access token. Presenting a rotated refresh token again revokes every token of the installation, and the customer has to reconnect. One exception covers a lost response: within 60 seconds, the previous refresh token is accepted once more, provided the pair it produced has not been used.
scope may name a subset of the grant, to get a token pair with fewer scopes.
The grant itself does not shrink.
Token errors
Section titled “Token errors”/oauth/token and /oauth/revoke answer in the OAuth shape, not the API
envelope:
{ "error": "invalid_grant", "error_description": "The authorization code is invalid or expired." }error |
Meaning |
|---|---|
invalid_client |
Wrong client ID or secret (401) |
invalid_grant |
Code or refresh token expired, spent, revoked, or issued for a different redirect URI or client |
invalid_scope |
A refresh asked for a scope outside the grant |
invalid_request |
A parameter is missing or malformed |
unsupported_grant_type |
Not authorization_code or refresh_token |
rate_limit_exceeded |
Too many token requests (429, with Retry-After) |
ip_rate_limited |
Too many failed authentications, or requests, from your address (429, with Retry-After); see per-address limits |
Revoke
Section titled “Revoke”curl --fail-with-body https://api.talki.it/oauth/revoke \ -u "$TALKI_CLIENT_ID:$TALKI_CLIENT_SECRET" \ -d token="$REFRESH_TOKEN"Revoking any live token of an installation disconnects the whole
installation: every token stops working and the customer has to reconnect. It
always answers 200 {}, including for a token that is unknown or already dead.
Disconnection
Section titled “Disconnection”A customer can disconnect your app from the Talki app at any time. After
that, API calls with its token return 401 invalid_credentials, a refresh
returns invalid_grant, and the app receives
installation.revoked.
List your installations
Section titled “List your installations”Your server can list every account connected to your app, with the client credentials over HTTP Basic. It lists your app’s installations only, and needs no customer’s token.
curl --fail-with-body "https://api.talki.it/oauth/app/installations?status=active&limit=100" \ -u "$TALKI_CLIENT_ID:$TALKI_CLIENT_SECRET"{ "object": "list", "data": [ { "object": "installation", "id": "5d0e8a2c-7b41-4f7e-9c3a-1e2f4b6d8a90", "account_id": "f7e39b9a-1ac8-45e4-93da-2600f17cb286", "business_name": "Studio Bellini", "scopes": ["appointments:read", "calls:read"], "missing_scopes": [], "status": "active", "access": "active", "access_suspended_reason": null, "livemode": true, "test": false, "test_expires_at": null, "created_at": "2026-09-30T08:12:44.120000+00:00", "last_used_at": "2026-10-15T16:40:00+00:00", "revoked_at": null } ], "has_more": true, "next_cursor": "djE6MjAyNi0wOS0zMFQwODoxMjo0NC4xMiswMDowMHw1ZDBlOGEyYy03YjQx…"}| Parameter | Rule |
|---|---|
status |
active or revoked. Omitted: both |
livemode |
true or false. Omitted: both |
limit |
1–100, default 25 |
cursor |
The next_cursor of the previous page |
Rows come newest connection first. While has_more is true, request the next
page with cursor set to next_cursor; on the last page next_cursor is
null. To fetch every active account:
cursor=""while :; do page=$(curl --fail-with-body -s -u "$TALKI_CLIENT_ID:$TALKI_CLIENT_SECRET" \ "https://api.talki.it/oauth/app/installations?status=active&limit=100${cursor:+&cursor=$cursor}") echo "$page" | jq -r '.data[] | [.account_id, .business_name] | @tsv' [ "$(echo "$page" | jq -r .has_more)" = true ] || break cursor=$(echo "$page" | jq -r .next_cursor)donestatusisrevokedonce the customer disconnected. A revoked row keeps its dates and scopes, butbusiness_nameisnull.accessissuspendedwhen API calls for the account are refused right now, with the error code inaccess_suspended_reason(for examplesubscription_inactive);noneon a revoked row.missing_scopeslists the scopes your app is registered for that this customer did not grant.last_used_atis the last API call with this installation’s tokens, accurate to a few minutes. Listing does not change it.testistruefor a test account your app created;test_expires_atis when it is erased.
The list gives no access to the accounts: reading one still takes its
installation’s access token and scopes. Errors use the API envelope: 401 invalid_client for wrong credentials, 400 invalid_parameter or
invalid_cursor for a bad query, 409 client_disabled for an app Talki has
disabled.
Limits
Section titled “Limits”/oauth/token: 600 requests per minute per app. Failed client authentication also counts against the per-address budget in Errors and rate limits./oauth/app/installations: 120 requests per minute per address./oauth/app/usage(monthly usage): 60 requests per minute per address.- API calls with an access token are limited per installation, as an API key is limited per account.
- An access token cannot drive the sandbox controls
(
/v1/sandbox/*): they answer403 sandbox_api_key_required. Use the sandbox key from the console. The exception is a test account your app created, which takes fake calls with your app’s token.
Rotate the client secret
Section titled “Rotate the client secret”Rotate it in the console. The previous secret stops working at once, unless you ask for an overlap (up to 7 days) to deploy the new one first.
The contract version your app is served at is pinned on the app, for all of its installations — see Versioning.
