curl --request POST \
--url https://app.base44.com/api/apps/{app_id}/ownership/transfer \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"new_owner_email": "jane@acme.com",
"destination": "external_email",
"disconnect_integrations": true,
"integration_decisions": [
{
"action": "keep_connected",
"integration_type": "slack"
}
],
"secret_decisions": [
{
"action": "require_new_value",
"secret_name": "STRIPE_API_KEY"
}
],
"keep_sender_as_collaborator": false
}
'import requests
url = "https://app.base44.com/api/apps/{app_id}/ownership/transfer"
payload = {
"new_owner_email": "jane@acme.com",
"destination": "external_email",
"disconnect_integrations": True,
"integration_decisions": [
{
"action": "keep_connected",
"integration_type": "slack"
}
],
"secret_decisions": [
{
"action": "require_new_value",
"secret_name": "STRIPE_API_KEY"
}
],
"keep_sender_as_collaborator": False
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
new_owner_email: 'jane@acme.com',
destination: 'external_email',
disconnect_integrations: true,
integration_decisions: [{action: 'keep_connected', integration_type: 'slack'}],
secret_decisions: [{action: 'require_new_value', secret_name: 'STRIPE_API_KEY'}],
keep_sender_as_collaborator: false
})
};
fetch('https://app.base44.com/api/apps/{app_id}/ownership/transfer', 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.base44.com/api/apps/{app_id}/ownership/transfer",
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([
'new_owner_email' => 'jane@acme.com',
'destination' => 'external_email',
'disconnect_integrations' => true,
'integration_decisions' => [
[
'action' => 'keep_connected',
'integration_type' => 'slack'
]
],
'secret_decisions' => [
[
'action' => 'require_new_value',
'secret_name' => 'STRIPE_API_KEY'
]
],
'keep_sender_as_collaborator' => false
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"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.base44.com/api/apps/{app_id}/ownership/transfer"
payload := strings.NewReader("{\n \"new_owner_email\": \"jane@acme.com\",\n \"destination\": \"external_email\",\n \"disconnect_integrations\": true,\n \"integration_decisions\": [\n {\n \"action\": \"keep_connected\",\n \"integration_type\": \"slack\"\n }\n ],\n \"secret_decisions\": [\n {\n \"action\": \"require_new_value\",\n \"secret_name\": \"STRIPE_API_KEY\"\n }\n ],\n \"keep_sender_as_collaborator\": false\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
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.base44.com/api/apps/{app_id}/ownership/transfer")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"new_owner_email\": \"jane@acme.com\",\n \"destination\": \"external_email\",\n \"disconnect_integrations\": true,\n \"integration_decisions\": [\n {\n \"action\": \"keep_connected\",\n \"integration_type\": \"slack\"\n }\n ],\n \"secret_decisions\": [\n {\n \"action\": \"require_new_value\",\n \"secret_name\": \"STRIPE_API_KEY\"\n }\n ],\n \"keep_sender_as_collaborator\": false\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.base44.com/api/apps/{app_id}/ownership/transfer")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"new_owner_email\": \"jane@acme.com\",\n \"destination\": \"external_email\",\n \"disconnect_integrations\": true,\n \"integration_decisions\": [\n {\n \"action\": \"keep_connected\",\n \"integration_type\": \"slack\"\n }\n ],\n \"secret_decisions\": [\n {\n \"action\": \"require_new_value\",\n \"secret_name\": \"STRIPE_API_KEY\"\n }\n ],\n \"keep_sender_as_collaborator\": false\n}"
response = http.request(request)
puts response.read_body{
"id": "68d4b1e9c2a7f3001e5b8c40",
"app_id": "6820f3a4e7b91d003c45a1f2",
"new_owner_email": "jane@acme.com",
"status": "pending",
"expires_at": "2026-10-05T09:23:41.512000Z"
}Start ownership transfer
Invites a new owner for the app by email, which starts an ownership transfer.
The recipient gets an email with a link, and the transfer completes only once they sign in and accept it. Nothing about the app changes until then. The invitation expires after 7 days. Check first that it can be sent, and what would transfer with the app, with Check ownership transfer. Afterwards, follow it with Get pending ownership transfer, Resend ownership transfer, or Cancel ownership transfer.
When the recipient accepts, they pick the workspace that receives the app. If it’s a different workspace, the app’s current collaborators lose access and its AI chat history is cleared. Integrations and secrets follow the choices in the request. To pass the app to a member of its workspace right away, with no invitation, use Transfer ownership to a workspace member.
An app can have only one pending transfer. Starting another while one is pending is rejected, whoever it’s for. An expired invitation is replaced.
You need to own the app or be an owner or admin of its workspace. A workspace admin who doesn’t own the app also needs an Enterprise workspace or approved partner status. An invitation to someone outside the workspace needs that status too, and only workspace owners and admins can send one. The workspace’s transfer policy can narrow that to owners, or turn it off.
This is limited to 30 requests a minute per workspace. Some workspaces have a different limit. Invitation emails are also limited to 5 an hour and 20 a day per caller, and to 3 an hour and 10 a day per recipient. Resending counts toward the same limits.
curl --request POST \
--url https://app.base44.com/api/apps/{app_id}/ownership/transfer \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"new_owner_email": "jane@acme.com",
"destination": "external_email",
"disconnect_integrations": true,
"integration_decisions": [
{
"action": "keep_connected",
"integration_type": "slack"
}
],
"secret_decisions": [
{
"action": "require_new_value",
"secret_name": "STRIPE_API_KEY"
}
],
"keep_sender_as_collaborator": false
}
'import requests
url = "https://app.base44.com/api/apps/{app_id}/ownership/transfer"
payload = {
"new_owner_email": "jane@acme.com",
"destination": "external_email",
"disconnect_integrations": True,
"integration_decisions": [
{
"action": "keep_connected",
"integration_type": "slack"
}
],
"secret_decisions": [
{
"action": "require_new_value",
"secret_name": "STRIPE_API_KEY"
}
],
"keep_sender_as_collaborator": False
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
new_owner_email: 'jane@acme.com',
destination: 'external_email',
disconnect_integrations: true,
integration_decisions: [{action: 'keep_connected', integration_type: 'slack'}],
secret_decisions: [{action: 'require_new_value', secret_name: 'STRIPE_API_KEY'}],
keep_sender_as_collaborator: false
})
};
fetch('https://app.base44.com/api/apps/{app_id}/ownership/transfer', 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.base44.com/api/apps/{app_id}/ownership/transfer",
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([
'new_owner_email' => 'jane@acme.com',
'destination' => 'external_email',
'disconnect_integrations' => true,
'integration_decisions' => [
[
'action' => 'keep_connected',
'integration_type' => 'slack'
]
],
'secret_decisions' => [
[
'action' => 'require_new_value',
'secret_name' => 'STRIPE_API_KEY'
]
],
'keep_sender_as_collaborator' => false
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"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.base44.com/api/apps/{app_id}/ownership/transfer"
payload := strings.NewReader("{\n \"new_owner_email\": \"jane@acme.com\",\n \"destination\": \"external_email\",\n \"disconnect_integrations\": true,\n \"integration_decisions\": [\n {\n \"action\": \"keep_connected\",\n \"integration_type\": \"slack\"\n }\n ],\n \"secret_decisions\": [\n {\n \"action\": \"require_new_value\",\n \"secret_name\": \"STRIPE_API_KEY\"\n }\n ],\n \"keep_sender_as_collaborator\": false\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
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.base44.com/api/apps/{app_id}/ownership/transfer")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"new_owner_email\": \"jane@acme.com\",\n \"destination\": \"external_email\",\n \"disconnect_integrations\": true,\n \"integration_decisions\": [\n {\n \"action\": \"keep_connected\",\n \"integration_type\": \"slack\"\n }\n ],\n \"secret_decisions\": [\n {\n \"action\": \"require_new_value\",\n \"secret_name\": \"STRIPE_API_KEY\"\n }\n ],\n \"keep_sender_as_collaborator\": false\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.base44.com/api/apps/{app_id}/ownership/transfer")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"new_owner_email\": \"jane@acme.com\",\n \"destination\": \"external_email\",\n \"disconnect_integrations\": true,\n \"integration_decisions\": [\n {\n \"action\": \"keep_connected\",\n \"integration_type\": \"slack\"\n }\n ],\n \"secret_decisions\": [\n {\n \"action\": \"require_new_value\",\n \"secret_name\": \"STRIPE_API_KEY\"\n }\n ],\n \"keep_sender_as_collaborator\": false\n}"
response = http.request(request)
puts response.read_body{
"id": "68d4b1e9c2a7f3001e5b8c40",
"app_id": "6820f3a4e7b91d003c45a1f2",
"new_owner_email": "jane@acme.com",
"status": "pending",
"expires_at": "2026-10-05T09:23:41.512000Z"
}Authorizations
Personal access token, sent as Authorization: Bearer <token>.
Path Parameters
ID of the app.
Body
The ownership transfer invitation to send.
Email address of the new owner. The invitation goes there, and they need a Base44 account with this email to accept it.
"jane@acme.com"
Either same_workspace, to invite an owner, admin, editor, or member of the app's workspace, or external_email, to invite anyone. Defaults to external_email.
same_workspace, external_email "external_email"
Whether to disconnect all of the app's integrations when the recipient accepts into another workspace (true) or keep them connected (false). It's used only when you send neither integration_decisions nor secret_decisions, and has no effect when the recipient accepts into the app's current workspace. To disconnect integrations in that case, send integration_decisions. Defaults to true.
true
What happens to each of the app's integrations when the recipient accepts. An integration you leave out is disconnected, and so is one whose can_keep_connected is false, whatever you choose. If you send secret_decisions without this, it counts as empty, which is rejected when the app has integrations.
Show child attributes
Show child attributes
[
{
"action": "keep_connected",
"integration_type": "slack"
}
]
What happens to each of the app's secrets when the recipient accepts. A secret you leave out needs a new value. Values never go in the request, and are copied on the server. If you send integration_decisions without this, it counts as empty, which is rejected when the app has secrets. When you send neither list, every secret keeps its value.
Show child attributes
Show child attributes
[
{
"action": "require_new_value",
"secret_name": "STRIPE_API_KEY"
}
]
Whether the app's current owner keeps editor access to the app after the transfer (true) or loses their direct access to it (false). An owner or admin of the workspace the app ends up in keeps access through that workspace either way. Defaults to false.
false
Response
The new transfer, waiting for the recipient to accept it.
An ownership transfer waiting for its recipient to accept it.
ID of the ownership transfer.
"68d4b1e9c2a7f3001e5b8c40"
ID of the app being transferred.
"6820f3a4e7b91d003c45a1f2"
Email address the transfer invitation was sent to.
"jane@acme.com"
Either pending, while the invitation waits for the recipient, or accepting, while the recipient's acceptance is being applied.
"pending"
When the invitation expires, as a UTC timestamp in ISO 8601 format. The recipient can't accept it after that.
"2026-10-05T09:23:41.512000Z"
Was this page helpful?