curl --request POST \
--url https://api.finosu.com/sso/portal-sessions \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"externalCustomerId": "example-customer-1"
}
'import requests
url = "https://api.finosu.com/sso/portal-sessions"
payload = { "externalCustomerId": "example-customer-1" }
headers = {
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({externalCustomerId: 'example-customer-1'})
};
fetch('https://api.finosu.com/sso/portal-sessions', 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://api.finosu.com/sso/portal-sessions",
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([
'externalCustomerId' => 'example-customer-1'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Key: <api-key>"
],
]);
$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://api.finosu.com/sso/portal-sessions"
payload := strings.NewReader("{\n \"externalCustomerId\": \"example-customer-1\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-API-Key", "<api-key>")
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://api.finosu.com/sso/portal-sessions")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"externalCustomerId\": \"example-customer-1\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.finosu.com/sso/portal-sessions")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"externalCustomerId\": \"example-customer-1\"\n}"
response = http.request(request)
puts response.read_body{
"url": "<string>",
"expiresAt": "2023-11-07T05:31:56Z",
"expiresIn": 123
}{
"detail": "<string>"
}{
"detail": "<string>"
}{
"detail": "<string>"
}{
"detail": "<string>"
}{
"detail": "<string>"
}{
"detail": "<string>"
}Create Portal Session
Return a single-use URL that opens the borrower portal already signed in, for a borrower your platform has already authenticated. Call this from your backend and redirect the borrower to the returned URL. The URL is a short-lived bearer credential: anyone holding it can exchange it once for that borrower’s session, so it must not be logged, stored, or placed anywhere it can be read back. Redeeming it starts an ordinary portal session, which is not cut short by the URL’s own expiry. Available only to explicitly approved integrations; contact Finosu to have it enabled for your account.
curl --request POST \
--url https://api.finosu.com/sso/portal-sessions \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"externalCustomerId": "example-customer-1"
}
'import requests
url = "https://api.finosu.com/sso/portal-sessions"
payload = { "externalCustomerId": "example-customer-1" }
headers = {
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({externalCustomerId: 'example-customer-1'})
};
fetch('https://api.finosu.com/sso/portal-sessions', 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://api.finosu.com/sso/portal-sessions",
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([
'externalCustomerId' => 'example-customer-1'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Key: <api-key>"
],
]);
$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://api.finosu.com/sso/portal-sessions"
payload := strings.NewReader("{\n \"externalCustomerId\": \"example-customer-1\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-API-Key", "<api-key>")
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://api.finosu.com/sso/portal-sessions")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"externalCustomerId\": \"example-customer-1\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.finosu.com/sso/portal-sessions")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"externalCustomerId\": \"example-customer-1\"\n}"
response = http.request(request)
puts response.read_body{
"url": "<string>",
"expiresAt": "2023-11-07T05:31:56Z",
"expiresIn": 123
}{
"detail": "<string>"
}{
"detail": "<string>"
}{
"detail": "<string>"
}{
"detail": "<string>"
}{
"detail": "<string>"
}{
"detail": "<string>"
}Overview
Returns a single-use URL that opens the borrower portal already signed in, for a borrower your platform has already authenticated. Your backend requests the URL; you redirect the borrower to it. This exists so a borrower who has just signed in on your side is not asked to sign in a second time. No passwords, cookies or session tokens are shared between your platform and Finosu.403. Contact Finosu
to have it enabled and to obtain your API key.Flow
- A borrower signs in on your platform.
- Your backend calls this endpoint with your own identifier for that borrower.
- You redirect the borrower’s browser to the returned
url. - They land on the portal dashboard, signed in.
The URL is a credential
The returnedurl is a short-lived bearer credential. Anyone holding it can
exchange it once for that borrower’s session.
- Do not write it to logs, analytics, error trackers, or support tickets.
- Do not store it, email it, or include it in a redirect you record.
- Redirect the borrower to it immediately and discard your copy.
expiresIn.
Responses are served with Cache-Control: no-store.
Expiry does not limit the session
expiresIn is how long the URL remains redeemable. Once redeemed, the
borrower gets an ordinary portal session governed by the portal’s normal session
policy, and that session is not cut short when the URL expires.
Request a URL at the moment you are about to redirect, rather than issuing one
ahead of time and holding it.
Identifying the borrower
externalCustomerId is your own identifier for the borrower, the same reference
you send Finosu elsewhere. The lookup is scoped to the company your API key
authenticates as, so a reference belonging to another company returns 404.
externalLoanId is optional. A borrower’s loans at one company share a single
portal account, so naming a loan does not change what the session can reach. Send
it when a borrower’s loans span more than one of your brands, so the link opens
the right branded portal.
If the loan reference does not identify a live loan belonging to that borrower —
whether it names another borrower’s loan, a loan that has been removed, or a
reference we have never seen — the request is rejected with 409 and no URL is
issued. That check exists to catch a mis-keyed mapping before it hands a borrower
someone else’s account, so treat a 409 as a data problem on your side rather
than something to retry.
Example
curl -X POST "https://api.finosu.com/sso/portal-sessions" \
-H "X-API-Key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"externalCustomerId": "example-customer-1"}'
{
"url": "https://portal.example-lender.com/sso?t=hJ4k...",
"expiresAt": "2026-01-15T18:04:21+00:00",
"expiresIn": 60
}
url. Nothing else in the response needs to be stored.
Errors
| Status | Meaning | What to do |
|---|---|---|
401 | Missing, invalid, or inactive API key | Provide an active key |
403 | Partner sign-on is not enabled for your account | Contact Finosu |
404 | No borrower matches that reference in the company your key authenticates as | Check the reference; confirm the borrower was sent to Finosu |
409 | The loan reference does not identify a live loan belonging to that borrower | Fix the mapping; do not retry |
422 | Malformed request body | Check the request schema |
429 | Issuance quota exceeded | Wait for the Retry-After interval |
503 | Sign-on is temporarily unavailable | Retry; no URL was issued |
404 and a 409 are different failures. 404 means no borrower matches that
reference within the company your key authenticates as — a borrower belonging
to another company is not visible to your key. 409 means the borrower was
found, but the loan reference does not resolve to a live loan of theirs.
Redemption failures
If the borrower opens a URL that has already been used, has expired, or was never issued, the portal shows one neutral message asking them to return to your platform and open a new link. Those three cases are deliberately indistinguishable, so a URL cannot be probed to learn whether it ever existed. If a borrower reports that a link did not work, issue a new one rather than asking them to retry the old one — a spent URL can never work again.Body
Optional. A borrower's loans at one company share a single portal account, so this does not change what the session can reach. Send it when a borrower's loans span more than one of your brands, so the link opens the right branded portal. If it does not identify a live loan belonging to that borrower — another borrower's loan, a removed loan, or an unknown reference — the request is rejected with 409.
1 - 255Response
Single-use sign-in URL. Served with Cache-Control: no-store.