Skip to content

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 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-server

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.

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.

Terminal window
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_…

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.

Terminal window
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.

/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
Terminal window
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.

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.

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.

Terminal window
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:

Terminal window
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)
done
  • status is revoked once the customer disconnected. A revoked row keeps its dates and scopes, but business_name is null.
  • access is suspended when API calls for the account are refused right now, with the error code in access_suspended_reason (for example subscription_inactive); none on a revoked row.
  • missing_scopes lists the scopes your app is registered for that this customer did not grant.
  • last_used_at is the last API call with this installation’s tokens, accurate to a few minutes. Listing does not change it.
  • test is true for a test account your app created; test_expires_at is 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.

  • /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 answer 403 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 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.