SSO
Create or Update SSO Connection
Create or replace the tenant’s SAML identity provider configuration and toggle whether SSO login is enabled.
PUT
/
v1
/
sso
/
connection
Create or update the tenant's SAML IdP configuration (owner/admin only)
curl --request PUT \
--url https://billing.example.com/v1/sso/connection \
--header 'Content-Type: application/json' \
--cookie recurso_session= \
--data '
{
"idp_entity_id": "<string>",
"idp_sso_url": "<string>",
"idp_certificate": "<string>",
"idp_metadata_xml": "<string>",
"enabled": true
}
'import requests
url = "https://billing.example.com/v1/sso/connection"
payload = {
"idp_entity_id": "<string>",
"idp_sso_url": "<string>",
"idp_certificate": "<string>",
"idp_metadata_xml": "<string>",
"enabled": True
}
headers = {
"cookie": "recurso_session=",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {cookie: 'recurso_session=', 'Content-Type': 'application/json'},
body: JSON.stringify({
idp_entity_id: '<string>',
idp_sso_url: '<string>',
idp_certificate: '<string>',
idp_metadata_xml: '<string>',
enabled: true
})
};
fetch('https://billing.example.com/v1/sso/connection', 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://billing.example.com/v1/sso/connection",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'idp_entity_id' => '<string>',
'idp_sso_url' => '<string>',
'idp_certificate' => '<string>',
'idp_metadata_xml' => '<string>',
'enabled' => true
]),
CURLOPT_COOKIE => "recurso_session=",
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://billing.example.com/v1/sso/connection"
payload := strings.NewReader("{\n \"idp_entity_id\": \"<string>\",\n \"idp_sso_url\": \"<string>\",\n \"idp_certificate\": \"<string>\",\n \"idp_metadata_xml\": \"<string>\",\n \"enabled\": true\n}")
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("cookie", "recurso_session=")
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.put("https://billing.example.com/v1/sso/connection")
.header("cookie", "recurso_session=")
.header("Content-Type", "application/json")
.body("{\n \"idp_entity_id\": \"<string>\",\n \"idp_sso_url\": \"<string>\",\n \"idp_certificate\": \"<string>\",\n \"idp_metadata_xml\": \"<string>\",\n \"enabled\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://billing.example.com/v1/sso/connection")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["cookie"] = 'recurso_session='
request["Content-Type"] = 'application/json'
request.body = "{\n \"idp_entity_id\": \"<string>\",\n \"idp_sso_url\": \"<string>\",\n \"idp_certificate\": \"<string>\",\n \"idp_metadata_xml\": \"<string>\",\n \"enabled\": true\n}"
response = http.request(request)
puts response.read_body{
"data": {
"tenant_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"enabled": true,
"configured": true,
"idp_entity_id": "<string>",
"idp_sso_url": "<string>",
"idp_certificate": "<string>",
"idp_metadata_xml": "<string>",
"sp_metadata_url": "<string>",
"sp_acs_url": "<string>"
}
}{
"error": {
"code": "validation_failed",
"message": "<string>"
}
}{
"error": {
"code": "validation_failed",
"message": "<string>"
}
}{
"error": {
"code": "validation_failed",
"message": "<string>"
}
}Creates the tenant’s SAML SSO connection, or replaces it if one already
exists. Each tenant has exactly one connection, so a
Whitespace around every string field is trimmed before it is stored.
Errors use the standard envelope — see Errors.
PUT is idempotent: send
the full IdP configuration every time, not a partial patch. Configure the
identity provider either by pasting its full metadata document in
idp_metadata_xml, or by supplying all three discrete fields —
idp_entity_id, idp_sso_url and idp_certificate. When both are present,
the metadata XML wins.
Set enabled: true to switch on the public
login and
ACS endpoints for the tenant. Enabling a
connection that is not fully configured is rejected, so you can save a
draft with enabled: false and flip it on once the IdP side is done. The
response includes the SP URLs to register with the IdP; read them back later
with GET /v1/sso/connection.
Only owners and admins can call this endpoint from a dashboard session;
members receive 403. API-key callers act with full tenant access.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
idp_metadata_xml | string | No | The IdP’s full SAML metadata document. Sufficient on its own; takes precedence over the discrete fields below. |
idp_entity_id | string | No | The IdP’s SAML entity id (issuer). Only checked when enabled is true: enabling requires either idp_metadata_xml, or all of idp_entity_id, idp_sso_url and idp_certificate. With enabled: false any subset — including none — is accepted and stored as a draft. |
idp_sso_url | string | No | The IdP’s HTTP-Redirect SingleSignOnService URL. |
idp_certificate | string | No | The IdP’s base64/PEM X.509 signing certificate. |
enabled | boolean | No | Whether SSO login is live for the tenant. Defaults to false. Can only be true when the connection is configured. |
Example Request
curl -X PUT https://api.recurso.dev/v1/sso/connection \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"idp_entity_id": "http://www.okta.com/exk1a2b3c4d5e6f7g8h9",
"idp_sso_url": "https://acme.okta.com/app/acme_recurso_1/exk1a2b3c4d5e6f7g8h9/sso/saml",
"idp_certificate": "-----BEGIN CERTIFICATE-----\nMIIDpDCCAoygAwIBAgIGAYt...\n-----END CERTIFICATE-----",
"enabled": true
}'
Response
{
"data": {
"tenant_id": "d4c3b2a1-0f9e-4765-8321-abcdef012345",
"idp_entity_id": "http://www.okta.com/exk1a2b3c4d5e6f7g8h9",
"idp_sso_url": "https://acme.okta.com/app/acme_recurso_1/exk1a2b3c4d5e6f7g8h9/sso/saml",
"idp_certificate": "-----BEGIN CERTIFICATE-----\nMIIDpDCCAoygAwIBAgIGAYt...\n-----END CERTIFICATE-----",
"idp_metadata_xml": "",
"enabled": true,
"configured": true,
"sp_metadata_url": "https://api.recurso.dev/auth/saml/d4c3b2a1-0f9e-4765-8321-abcdef012345/metadata",
"sp_acs_url": "https://api.recurso.dev/auth/saml/d4c3b2a1-0f9e-4765-8321-abcdef012345/acs"
}
}
Fields
| Field | Type | Description |
|---|---|---|
tenant_id | string (uuid) | The tenant that owns the connection. |
idp_entity_id | string | The stored IdP entity id, or empty when only idp_metadata_xml was submitted. |
idp_sso_url | string | The stored IdP SingleSignOnService URL. |
idp_certificate | string | The stored IdP signing certificate. |
idp_metadata_xml | string | The stored IdP metadata document, or empty when only the discrete fields were submitted. Fields are stored as sent (trimmed); nothing is blanked when both forms are present. |
enabled | boolean | Whether the public SP endpoints answer for this tenant. |
configured | boolean | Whether the stored detail is enough to attempt a login. |
sp_metadata_url | string | Public URL of the tenant’s SP metadata. Register it with the IdP. |
sp_acs_url | string | Public URL the IdP posts assertions to. |
Errors
| Status | Code | When | Fix |
|---|---|---|---|
400 | validation_failed | The body is not valid JSON, or enabled is true but the connection is not configured (no idp_metadata_xml and at least one of the three discrete fields missing). The same code is returned if the connection could not be saved. | Send a well-formed body; provide idp_metadata_xml or all of idp_entity_id, idp_sso_url and idp_certificate before enabling. |
401 | unauthorized | No API key or session cookie was sent. An invalid API key returns invalid_api_key; a key for the wrong mode (live vs test) returns key_mode_mismatch. | Send Authorization: Bearer <api_key> with a key for the right mode, or sign in to the dashboard. |
403 | forbidden | The dashboard session belongs to a member, not an owner or admin. | Ask an owner or admin to configure SSO, or use an API key. |
Authorizations
sessionCookiebearerAuth
Dashboard user session cookie (httpOnly) issued by POST /auth/register and POST /auth/login. v1 endpoints accept EITHER this cookie or the tenant API key (bearerAuth).
Body
application/json
Response
The upserted connection.
Show child attributes
Show child attributes
⌘I
Create or update the tenant's SAML IdP configuration (owner/admin only)
curl --request PUT \
--url https://billing.example.com/v1/sso/connection \
--header 'Content-Type: application/json' \
--cookie recurso_session= \
--data '
{
"idp_entity_id": "<string>",
"idp_sso_url": "<string>",
"idp_certificate": "<string>",
"idp_metadata_xml": "<string>",
"enabled": true
}
'import requests
url = "https://billing.example.com/v1/sso/connection"
payload = {
"idp_entity_id": "<string>",
"idp_sso_url": "<string>",
"idp_certificate": "<string>",
"idp_metadata_xml": "<string>",
"enabled": True
}
headers = {
"cookie": "recurso_session=",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {cookie: 'recurso_session=', 'Content-Type': 'application/json'},
body: JSON.stringify({
idp_entity_id: '<string>',
idp_sso_url: '<string>',
idp_certificate: '<string>',
idp_metadata_xml: '<string>',
enabled: true
})
};
fetch('https://billing.example.com/v1/sso/connection', 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://billing.example.com/v1/sso/connection",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'idp_entity_id' => '<string>',
'idp_sso_url' => '<string>',
'idp_certificate' => '<string>',
'idp_metadata_xml' => '<string>',
'enabled' => true
]),
CURLOPT_COOKIE => "recurso_session=",
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://billing.example.com/v1/sso/connection"
payload := strings.NewReader("{\n \"idp_entity_id\": \"<string>\",\n \"idp_sso_url\": \"<string>\",\n \"idp_certificate\": \"<string>\",\n \"idp_metadata_xml\": \"<string>\",\n \"enabled\": true\n}")
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("cookie", "recurso_session=")
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.put("https://billing.example.com/v1/sso/connection")
.header("cookie", "recurso_session=")
.header("Content-Type", "application/json")
.body("{\n \"idp_entity_id\": \"<string>\",\n \"idp_sso_url\": \"<string>\",\n \"idp_certificate\": \"<string>\",\n \"idp_metadata_xml\": \"<string>\",\n \"enabled\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://billing.example.com/v1/sso/connection")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["cookie"] = 'recurso_session='
request["Content-Type"] = 'application/json'
request.body = "{\n \"idp_entity_id\": \"<string>\",\n \"idp_sso_url\": \"<string>\",\n \"idp_certificate\": \"<string>\",\n \"idp_metadata_xml\": \"<string>\",\n \"enabled\": true\n}"
response = http.request(request)
puts response.read_body{
"data": {
"tenant_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"enabled": true,
"configured": true,
"idp_entity_id": "<string>",
"idp_sso_url": "<string>",
"idp_certificate": "<string>",
"idp_metadata_xml": "<string>",
"sp_metadata_url": "<string>",
"sp_acs_url": "<string>"
}
}{
"error": {
"code": "validation_failed",
"message": "<string>"
}
}{
"error": {
"code": "validation_failed",
"message": "<string>"
}
}{
"error": {
"code": "validation_failed",
"message": "<string>"
}
}