Overview
Request Key Handoff
Get an owner-confirmed dashboard link for creating or rotating an API key
POST
/
api
/
v1
/
api-key-handoff
Request Key Handoff
curl --request POST \
--url https://api.sequenzy.com/api/v1/api-key-handoff \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "<string>",
"preset": "<string>",
"scopes": [
"<string>"
],
"replaceApiKeyId": "<string>"
}
'import requests
url = "https://api.sequenzy.com/api/v1/api-key-handoff"
payload = {
"name": "<string>",
"preset": "<string>",
"scopes": ["<string>"],
"replaceApiKeyId": "<string>"
}
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({
name: '<string>',
preset: '<string>',
scopes: ['<string>'],
replaceApiKeyId: '<string>'
})
};
fetch('https://api.sequenzy.com/api/v1/api-key-handoff', 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.sequenzy.com/api/v1/api-key-handoff",
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([
'name' => '<string>',
'preset' => '<string>',
'scopes' => [
'<string>'
],
'replaceApiKeyId' => '<string>'
]),
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://api.sequenzy.com/api/v1/api-key-handoff"
payload := strings.NewReader("{\n \"name\": \"<string>\",\n \"preset\": \"<string>\",\n \"scopes\": [\n \"<string>\"\n ],\n \"replaceApiKeyId\": \"<string>\"\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://api.sequenzy.com/api/v1/api-key-handoff")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"<string>\",\n \"preset\": \"<string>\",\n \"scopes\": [\n \"<string>\"\n ],\n \"replaceApiKeyId\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.sequenzy.com/api/v1/api-key-handoff")
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 \"name\": \"<string>\",\n \"preset\": \"<string>\",\n \"scopes\": [\n \"<string>\"\n ],\n \"replaceApiKeyId\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"handoff": {
"url": "https://sequenzy.com/dashboard/company/company_abc123/settings?tab=api-keys&action=create-api-key&keyName=Production+sync+%28rotated%29&keyPreset=agent_safe&replaces=key_abc123",
"manageUrl": "https://sequenzy.com/dashboard/company/company_abc123/settings?tab=api-keys",
"keyType": "company",
"name": "Production sync (rotated)",
"preset": "agent_safe",
"scopes": null,
"permissions": {
"preset": "agent_safe",
"fullAccess": false,
"selectedScopeCount": 24,
"currentScopeCount": 63,
"description": "24 of 63 current permissions are enabled."
},
"replaces": {
"id": "key_abc123",
"name": "Production sync",
"prefix": "seq_live_abcd",
"isCurrentKey": true
},
"canSelfServe": false,
"deliversKeyToCaller": false
},
"message": "Give this link to the workspace owner. It opens the dashboard create-key form with the requested name and permissions prefilled. The new key is shown in the browser and is never returned here.",
"nextSteps": [
"Send the URL to the workspace owner and stop. Nothing is created until they review the form and click Create.",
"The owner copies the new key from the browser and puts it in your secret store. It is shown once and is never returned through the API.",
"Verify the replacement by calling the account endpoint with the new key before retiring anything.",
"Revoke the previous key: the dashboard offers it after the replacement is created, or the replacement can revoke it through the API when the owner grants it api_keys:manage."
]
}
{
"error": "Unsupported API key scope: not:a:scope"
}
{
"error": "replaceApiKeyId is not a valid API key ID"
}
{
"error": "Invalid API key"
}
{
"error": "API key is missing required scope: account:read"
}
Build a link that opens the dashboard’s create-key form with a name and
permissions already filled in. Use this when key management is blocked because
the calling key does not have
api_keys:manage.
Every other /api/v1/api-keys route requires api_keys:manage, and that scope
cannot be granted through the API by a key that is missing it - otherwise any
leaked key could mint a full-access successor for itself. This endpoint needs
only account:read because it creates nothing, changes nothing, and returns no
secret. The new key is issued in the browser, under the owner’s own
authenticated session, after they review the form and click Create.
When the caller is an account-scoped seq_user_ key, select the company with
the x-company-id header. Personal keys are routed to Account → API Keys;
company keys to workspace Settings → API Keys.
Request
string
Suggested name for the new key. Trimmed to 80 characters in the link.
string
Suggested permission preset. Supported presets:
full_access, read_only,
agent_safe, ai_drafting, data_ingest_safe, data_ingest_automations,
transactional_sender, marketing_sender.string[]
Suggested explicit permission scopes. Overrides
preset. An unsupported scope
is rejected here rather than silently dropped from the owner’s form.string
ID of the key this one replaces. Pass the literal string
current for the key
the request is authenticated with. The dashboard offers to revoke the
predecessor once the replacement exists, which finishes a rotation in a single
visit.curl -X POST "https://api.sequenzy.com/api/v1/api-key-handoff" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "x-company-id: company_abc123" \
-H "Content-Type: application/json" \
-d '{"name": "Production sync (rotated)", "preset": "agent_safe", "replaceApiKeyId": "current"}'
Responses
{
"success": true,
"handoff": {
"url": "https://sequenzy.com/dashboard/company/company_abc123/settings?tab=api-keys&action=create-api-key&keyName=Production+sync+%28rotated%29&keyPreset=agent_safe&replaces=key_abc123",
"manageUrl": "https://sequenzy.com/dashboard/company/company_abc123/settings?tab=api-keys",
"keyType": "company",
"name": "Production sync (rotated)",
"preset": "agent_safe",
"scopes": null,
"permissions": {
"preset": "agent_safe",
"fullAccess": false,
"selectedScopeCount": 24,
"currentScopeCount": 63,
"description": "24 of 63 current permissions are enabled."
},
"replaces": {
"id": "key_abc123",
"name": "Production sync",
"prefix": "seq_live_abcd",
"isCurrentKey": true
},
"canSelfServe": false,
"deliversKeyToCaller": false
},
"message": "Give this link to the workspace owner. It opens the dashboard create-key form with the requested name and permissions prefilled. The new key is shown in the browser and is never returned here.",
"nextSteps": [
"Send the URL to the workspace owner and stop. Nothing is created until they review the form and click Create.",
"The owner copies the new key from the browser and puts it in your secret store. It is shown once and is never returned through the API.",
"Verify the replacement by calling the account endpoint with the new key before retiring anything.",
"Revoke the previous key: the dashboard offers it after the replacement is created, or the replacement can revoke it through the API when the owner grants it api_keys:manage."
]
}
deliversKeyToCaller is always false. canSelfServe is true when the
calling key already holds api_keys:manage and could create the replacement
directly with Create API Key; the link is then
only needed when policy requires a human to confirm.permissions is the receipt for the suggested selection, and scopes is the
expanded list. Both are null when the request suggested no preset and no
scopes, because the form then opens on the dashboard’s own default rather than
on full access.replaces.name and replaces.prefix are populated only when the replaced key
is the one making the request. Reading metadata for any other key requires
api_keys:manage, so the dashboard resolves it under the owner’s session.{
"error": "Unsupported API key scope: not:a:scope"
}
{
"error": "replaceApiKeyId is not a valid API key ID"
}
{
"error": "Invalid API key"
}
{
"error": "API key is missing required scope: account:read"
}
Rotating a key you cannot manage
- Call this endpoint with
replaceApiKeyId: "current"and the permissions the replacement needs. - Hand the URL to the workspace owner and stop. Do not poll: the key never comes back through the API.
- The owner reviews the prefilled form, creates the key, and stores it in your secret store.
- Verify the replacement with Get Account before retiring anything.
- Revoke the predecessor from the dashboard prompt, or with
Revoke API Key if the owner granted the new
key
api_keys:manage.
⌘I
Request Key Handoff
curl --request POST \
--url https://api.sequenzy.com/api/v1/api-key-handoff \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "<string>",
"preset": "<string>",
"scopes": [
"<string>"
],
"replaceApiKeyId": "<string>"
}
'import requests
url = "https://api.sequenzy.com/api/v1/api-key-handoff"
payload = {
"name": "<string>",
"preset": "<string>",
"scopes": ["<string>"],
"replaceApiKeyId": "<string>"
}
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({
name: '<string>',
preset: '<string>',
scopes: ['<string>'],
replaceApiKeyId: '<string>'
})
};
fetch('https://api.sequenzy.com/api/v1/api-key-handoff', 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.sequenzy.com/api/v1/api-key-handoff",
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([
'name' => '<string>',
'preset' => '<string>',
'scopes' => [
'<string>'
],
'replaceApiKeyId' => '<string>'
]),
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://api.sequenzy.com/api/v1/api-key-handoff"
payload := strings.NewReader("{\n \"name\": \"<string>\",\n \"preset\": \"<string>\",\n \"scopes\": [\n \"<string>\"\n ],\n \"replaceApiKeyId\": \"<string>\"\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://api.sequenzy.com/api/v1/api-key-handoff")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"<string>\",\n \"preset\": \"<string>\",\n \"scopes\": [\n \"<string>\"\n ],\n \"replaceApiKeyId\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.sequenzy.com/api/v1/api-key-handoff")
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 \"name\": \"<string>\",\n \"preset\": \"<string>\",\n \"scopes\": [\n \"<string>\"\n ],\n \"replaceApiKeyId\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"handoff": {
"url": "https://sequenzy.com/dashboard/company/company_abc123/settings?tab=api-keys&action=create-api-key&keyName=Production+sync+%28rotated%29&keyPreset=agent_safe&replaces=key_abc123",
"manageUrl": "https://sequenzy.com/dashboard/company/company_abc123/settings?tab=api-keys",
"keyType": "company",
"name": "Production sync (rotated)",
"preset": "agent_safe",
"scopes": null,
"permissions": {
"preset": "agent_safe",
"fullAccess": false,
"selectedScopeCount": 24,
"currentScopeCount": 63,
"description": "24 of 63 current permissions are enabled."
},
"replaces": {
"id": "key_abc123",
"name": "Production sync",
"prefix": "seq_live_abcd",
"isCurrentKey": true
},
"canSelfServe": false,
"deliversKeyToCaller": false
},
"message": "Give this link to the workspace owner. It opens the dashboard create-key form with the requested name and permissions prefilled. The new key is shown in the browser and is never returned here.",
"nextSteps": [
"Send the URL to the workspace owner and stop. Nothing is created until they review the form and click Create.",
"The owner copies the new key from the browser and puts it in your secret store. It is shown once and is never returned through the API.",
"Verify the replacement by calling the account endpoint with the new key before retiring anything.",
"Revoke the previous key: the dashboard offers it after the replacement is created, or the replacement can revoke it through the API when the owner grants it api_keys:manage."
]
}
{
"error": "Unsupported API key scope: not:a:scope"
}
{
"error": "replaceApiKeyId is not a valid API key ID"
}
{
"error": "Invalid API key"
}
{
"error": "API key is missing required scope: account:read"
}