curl --request POST \
--url https://app.demand-iq.com/api/auth/sso/exchange \
--header 'Content-Type: application/json' \
--data '
{
"code": "<string>",
"spoof": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
'import requests
url = "https://app.demand-iq.com/api/auth/sso/exchange"
payload = {
"code": "<string>",
"spoof": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({code: '<string>', spoof: '3c90c3cc-0d44-4b50-8888-8dd25736052a'})
};
fetch('https://app.demand-iq.com/api/auth/sso/exchange', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.demand-iq.com/api/auth/sso/exchange",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'code' => '<string>',
'spoof' => '3c90c3cc-0d44-4b50-8888-8dd25736052a'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://app.demand-iq.com/api/auth/sso/exchange"
payload := strings.NewReader("{\n \"code\": \"<string>\",\n \"spoof\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.demand-iq.com/api/auth/sso/exchange")
.header("Content-Type", "application/json")
.body("{\n \"code\": \"<string>\",\n \"spoof\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.demand-iq.com/api/auth/sso/exchange")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"code\": \"<string>\",\n \"spoof\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n}"
response = http.request(request)
puts response.read_body{
"token": "<string>",
"expiresAt": "2023-11-07T05:31:56Z",
"idleTimeoutSeconds": 123,
"absoluteMaxAgeSeconds": 123,
"user": {
"id": "<string>",
"email": "<string>",
"name": "<string>"
},
"organization": {
"id": "<string>",
"name": "<string>"
}
}{
"error": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}Portal SSO token-bearer exchange
Token-bearer variant of the SSO flow for cross-origin clients (for example, the portal calling AI Presentations APIs from the browser).
Differs from GET /api/auth/sso in four ways:
- Accepts the SSO code in a JSON body, not a query parameter
- Returns the session token in the response body instead of setting a cookie
- Emits CORS headers so the portal origin can read the response
- Does not set the embedded or theme cookies (those are iframe-only concerns)
The returned token is the same opaque session ID that backs the cookie flow. Clients send it as Authorization: Bearer <token> on subsequent requests.
When spoof is present, the session is minted for the target company via Core’s loginAs. Core authorizes the impersonation; a rejection returns 403 spoof_forbidden. A spoof value equal to the authenticating user’s own company is treated as a no-op, not an error. The organization field in the response reflects the effective (spoof target) tenant. Unlike the GET SSO flow, this endpoint does not revoke prior sessions for the user — there is no signal to identify the caller’s prior bearer session, and revoking all sessions would kill any active iframe session in another tab.
On success, default email notification subscriptions are provisioned for the authenticating user’s own organization before any spoof is applied.
curl --request POST \
--url https://app.demand-iq.com/api/auth/sso/exchange \
--header 'Content-Type: application/json' \
--data '
{
"code": "<string>",
"spoof": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
'import requests
url = "https://app.demand-iq.com/api/auth/sso/exchange"
payload = {
"code": "<string>",
"spoof": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({code: '<string>', spoof: '3c90c3cc-0d44-4b50-8888-8dd25736052a'})
};
fetch('https://app.demand-iq.com/api/auth/sso/exchange', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.demand-iq.com/api/auth/sso/exchange",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'code' => '<string>',
'spoof' => '3c90c3cc-0d44-4b50-8888-8dd25736052a'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://app.demand-iq.com/api/auth/sso/exchange"
payload := strings.NewReader("{\n \"code\": \"<string>\",\n \"spoof\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.demand-iq.com/api/auth/sso/exchange")
.header("Content-Type", "application/json")
.body("{\n \"code\": \"<string>\",\n \"spoof\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.demand-iq.com/api/auth/sso/exchange")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"code\": \"<string>\",\n \"spoof\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n}"
response = http.request(request)
puts response.read_body{
"token": "<string>",
"expiresAt": "2023-11-07T05:31:56Z",
"idleTimeoutSeconds": 123,
"absoluteMaxAgeSeconds": 123,
"user": {
"id": "<string>",
"email": "<string>",
"name": "<string>"
},
"organization": {
"id": "<string>",
"name": "<string>"
}
}{
"error": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}Body
Response
Session minted. The token is the bearer session ID; send it as Authorization: Bearer <token> on subsequent requests.
Opaque session ID
Session deadline at mint time. Rolling: it moves forward on every successful renewal (POST /api/auth/session/renew), so a live client's real expiry drifts past this value. Never a hard cap.
Sliding idle window measured against the session's last activity. Default 25200 (7h). Independent of absoluteMaxAgeSeconds — either can force re-auth first.
Rolling session lifetime in seconds (SESSION_LIFETIME, default 86400). Re-set at creation and on every renewal, so this is the size of the rolling window, not an absolute cap. The wire key stays absoluteMaxAgeSeconds for portal compatibility.
Show child attributes
Show child attributes
Effective tenant — the spoof target when spoofing, otherwise the user's own company.
Show child attributes
Show child attributes