Connectors API
Kheish exposes connectors through two different HTTP surfaces:- runtime CRUD for operators
- ingress routes for external systems
Runtime CRUD endpoints
GET /v1/runtime/connectorsGET /v1/runtime/connectors/{kind}/{name}PUT /v1/runtime/connectors/{kind}/{name}DELETE /v1/runtime/connectors/{kind}/{name}
telegramslackhttpexternal
GET returns projected connector views with:
source:fileordaemon- non-secret transport settings
- redacted secret metadata such as
configured,source,secret_ref, orenv
- file-backed connectors are readable but not mutable through the daemon control plane
- deletion is rejected when the connector is still referenced by dependent config such as schedules or reply-target relationships
Runtime upsert semantics
PUT /v1/runtime/connectors/{kind}/{name} behaves like a field-aware upsert.
- If the connector does not exist, the daemon creates it.
- If the connector already exists, only fields present in the JSON payload are updated.
- Fields omitted from the payload keep their stored value.
PUT, but it is not a naive full-record replacement.
Shared helper shapes
ConnectorSecretInput
Secret-bearing runtime fields such as bot_token, signing_secret, secret_token, and bearer_token use the same write-only helper shape:
value: store or rotate a daemon-managed generic secret slotsecret_ref: point at an existing daemon-managed generic secret slotenv: read the secret from an environment variable
envcannot be combined withvalueorsecret_ref
ReplyHandle
Connector reply-target fields use raw ReplyHandle objects:
- HTTP connector
default_reply_targets - Slack connector
additional_reply_targets - Telegram connector
additional_reply_targets - HTTP ingress
reply_targets
SessionReplyTargetRequest surface used by /v1/sessions/{session_id}/reply-targets.
Plugin-specific address payloads commonly encode:
http: a raw URL string or serializedHttpReplyRoute(url, optionalheaders, optionalallow_private_networkfor trusted local sidecars)slack: serializedSlackReplyRoutetelegram: serializedTelegramReplyRouteexternal: serializedExternalReplyRoute
session_policy
Connectors can optionally bootstrap sessions for inbound traffic:
create_if_missingpersona_idcapability_scopecredential_scope
persona_id is validated against existing daemon persona records.
HTTP connectors
Runtime payload
PUT /v1/runtime/connectors/http/{name} accepts:
actor_idfixed_session_idbearer_tokenhmac_secretallow_unauthenticated_ingressrequire_hmac_signaturesignature_max_age_secsrequire_idempotency_keyingress_events_per_secondallow_payload_reply_targetsdefault_reply_targetsdefault_binding_keyssession_policy
- if no bearer token source is configured, configure
require_hmac_signature=truewith an HMAC secret or setallow_unauthenticated_ingress=true - if
require_hmac_signature=true, configurehmac_secretand keeprequire_idempotency_key=true signature_max_age_secsmust be between1and3600- empty bearer and HMAC secrets are rejected
- HTTP reply-route headers must be syntactically valid and cannot include
Authorization,Connection,Content-Length,Content-Type,Cookie,Forwarded,Host,Idempotency-Key,Proxy-Authorization,TE,Trailer,Transfer-Encoding,Upgrade,X-Api-Key, orX-Forwarded-* - ingress payload fields
reply_targets,reply_plugin, andreply_addressare accepted only when the request is authenticated andallow_payload_reply_targetsistrue; otherwise the connector uses only its configured defaults - unauthenticated ingress rejects payload
session_id, payloadbinding_keys, and asset/board payloads; use bearer or HMAC auth for those capabilities - daemon-configured
default_binding_keysremain allowed for public connectors because the caller cannot choose them - payload
metadatacannot set daemon-owned ingress keys such asconnector_ingress_keyorhttp_ingress_* - accepted idempotent receipts persist a payload fingerprint in the connector-ingress shard as well as
http_ingress_fingerprintin run metadata
Ingress endpoint
POST /v1/connectors/http/{name}
session_idbinding_keysactor_idcontentinput_itemsattachmentsmetadatareply_targetsreply_pluginreply_addressidempotency_key
- connector
fixed_session_id - payload
session_id - session already bound to the provided
binding_keys - connector-derived fallback session id based on the first binding key
- uses the
Authorization: Bearer ...header when a connector bearer token is configured - otherwise requires
allow_unauthenticated_ingress=true - when
require_hmac_signature=true, also requiresX-Kheish-TimestampandX-Kheish-Signature;X-Kheish-Timestampis a Unix timestamp in seconds - signatures use HMAC-SHA256 over
v1:POST:<path-and-query>:<timestamp>:<raw-body>and the signature header must be exactlyv1=<64 lowercase or uppercase hex chars>; duplicate timestamp/signature headers are rejected - accepted payloads require
idempotency_keyby default; reusing the same key with a different payload is rejected with409 - run metadata records hashed
connector_ingress_key/http_ingress_key,http_ingress_key_sha256, andhttp_ingress_fingerprintfor restart recovery and duplicate-payload conflict checks without persisting the raw idempotency key - ingress is rate-limited per connector and returns
429withRetry-Afterwhen the token bucket is exhausted
- sends
Idempotency-Key: kheish:<delivery_id>when delivering queued daemon output - owns the
Content-TypeandIdempotency-Keyheaders for the JSON delivery request - honors numeric and HTTP-date target
429 Retry-Afterheaders for durable retry scheduling - treats non-429
4xxtarget responses as terminal delivery failures - resolves hostname reply targets before delivery, rejects private/local IP results, pins the resolved addresses for the request, and ignores proxy environment variables
- reports delivery request failures with a redacted target instead of the raw URL
Slack connectors
Runtime payload
PUT /v1/runtime/connectors/slack/{name} accepts:
bot_tokensigning_secretallow_unauthenticated_ingressapi_base_urlfixed_session_idinclude_self_outputadditional_reply_targetsadditional_binding_keyssession_policyingress_events_per_secondallowed_api_app_idsallowed_enterprise_idsallowed_team_idsallowed_channel_idsallowed_file_hoststeam_bot_tokens
- if no signing secret source is configured,
allow_unauthenticated_ingressmust betrue
Ingress endpoint
POST /v1/connectors/slack/{name}
- verifies the Slack signature when a signing secret is configured
- rejects stale or far-future signed requests
- answers
url_verificationwith the Slackchallenge - enforces configured app, enterprise, team, and channel allowlists before run creation
- validates every Slack
authorizations[]enterprise/team entry against the configured allowlists - derives Enterprise Grid session IDs and reply routes from enterprise/team/channel/thread scope
- ignores bot-authored events
- derives one durable session from channel plus root thread timestamp when
fixed_session_idis not configured - automatically downloads inbound files when a matching bot token is configured
- blocks Slack file downloads from non-allowlisted hosts and does not follow connector media redirects
- prefers Slack
event_id - includes enterprise/team scope in the durable ingress key when Slack sends those fields
- posts text through
chat.postMessage - sends deterministic
client_msg_idvalues for queued text steps - uploads attachments through Slack external upload APIs
- persists per-delivery Slack output progress for posted text timestamps and upload
file_id/uploaded/completed steps so retries can skip already completed downstream steps - does not persist Slack external upload URLs in progress state
- validates persisted Slack reply targets against enterprise/team/channel allowlists before they can be stored on a session
- honors HTTP
429 Retry-Afterand Slack JSONratelimitedresponses for queued delivery retries - treats permanent Slack API errors such as invalid auth or channel-not-found as terminal delivery failures
External connectors
Runtime payload
PUT /v1/runtime/connectors/external/{name} accepts:
platformmodebase_urlallow_private_networkshared_tokenallow_unauthenticated_ingressfixed_session_idinclude_self_outputadditional_reply_targetsadditional_binding_keyssession_policyingress_events_per_secondchild_process
- non-secret transport config
- whether private-network remote targets are explicitly allowed
child_process.env_keyschild_process.credential_env_keys- redacted secret metadata for the shared token
child_process.credential_slots is configured, the daemon does not inject those platform secrets into the child environment directly. Each referenced slot must already exist as a non-empty generic secret when the connector config is resolved. The daemon starts the sidecar with one short-lived opaque credential token and the list of allowed env keys, and the sidecar fetches each concrete secret from the daemon on demand.
Ingress endpoints
POST /v1/connectors/external/{name}/eventsPOST /v1/connectors/external/{name}/events/batch
- single-event ingress returns
accepted,duplicate, orrejected - single-event rate limiting is HTTP
429withRetry-After; new events are throttled before a durable ingress receipt is reserved, while duplicate/pending event ids use the existing receipt path - batch ingress is ordered, non-atomic, and returns per-item statuses, including structured
retry_after_msfor rate-limited items - session derivation can use
thread.path,routing_key, orfixed_session_id - sidecar runtime endpoints stay on protocol
1, while ingress semantics now default to protocol2 - duplicate and conflicting idempotency replays return the original run/session identifiers
- receipt shards store hashed event ids and payload fingerprints; raw event ids stay out of connector-ingress receipt files
- daemon-to-sidecar delivery is at-least-once; sidecars must deduplicate repeated
delivery_idvalues
- when
include_self_output=true, replies default back to the sidecar-providedreply_route additional_reply_targetsare appended after that default self-target
Telegram connectors
Runtime payload
PUT /v1/runtime/connectors/telegram/{name} accepts:
bot_tokensecret_tokenallow_unauthenticated_ingressapi_base_urlingress_modepolling_timeout_secondsingress_events_per_secondallowed_chat_idsfixed_session_idinclude_self_outputadditional_reply_targetsadditional_binding_keyssession_policy
ingress_mode=pollingrequires a bot tokeningress_mode=webhookrequires either a secret token source orallow_unauthenticated_ingress=true- empty bot tokens and secret tokens are rejected
api_base_urlmust be an absolutehttp://orhttps://URL and must not include userinfo, query, or fragmentingress_events_per_secondis normalized to at least1- when
allowed_chat_idsis non-empty, ingress and output are restricted to those chat ids
Ingress endpoint
POST /v1/connectors/telegram/{name}
- accepts requests only for connectors configured with
ingress_mode=webhook - requires Telegram
update_idon webhook requests - verifies
x-telegram-bot-api-secret-tokenwhen a secret token is configured - derives one durable session from chat id plus topic thread id when
fixed_session_idis not configured - downloads inbound photos and documents through the Telegram Bot API when a bot token is available
- accepts
message,edited_message, andcallback_queryupdates - acknowledges authenticated unsupported webhook update types as skipped instead of returning a retryable daemon error
- validates and deduplicates callback queries before answering them; duplicate callback
update_idsubmissions return the existing run without another Bot API acknowledgement - rate-limits only new Telegram updates after
update_iddedupe; rate-limited webhooks return429withRetry-Afterandretry_after_ms, and are rejected before Bot API file metadata or media download calls - polling mode requests
message,edited_message, andcallback_queryupdates - polling mode honors capped Telegram
parameters.retry_afterand HTTPRetry-Afterflood-wait responses, including HTTP-date headers - Telegram Bot API and media clients ignore proxy environment variables and do not follow redirects
- keyed by Telegram
update_id - run metadata records
connector_ingress_keyso crash recovery can reconnect pending receipts to created runs - run metadata records
telegram_ingress_fingerprint; duplicateupdate_idsubmissions with a different payload return409
- when
include_self_output=true, replies default back to the same chat or topic additional_reply_targetsare appended after the self-target- reply targets outside
allowed_chat_idsare rejected when the allowlist is configured
- splits text outputs into Telegram-safe chunks of at most 4096 Unicode scalar values
- sends attachments through
sendPhoto,sendAudio, orsendDocument - honors capped Telegram
429/parameters.retry_afterand HTTPRetry-Afterflood-wait responses through the durable delivery queue - persists per-delivery step progress under
telegram-delivery-progress.dand skips already persisted chunks/attachments on retry - treats permanent auth/chat errors as terminal delivery failures
- remains at-least-once for a Telegram side effect that succeeds immediately before the daemon can persist step progress, because Telegram Bot API does not expose an idempotency key for these send methods
Output expectations
Connector ingress and output routing are separate concerns:- ingress routes submit work into Kheish
- reply targets determine where finalized output is delivered
- output delivery can stay daemon-local or flow through output plugins
- queued output can be inspected with
GET /v1/deliveries,GET /v1/deliveries/dead-letter,GET /v1/deliveries/{delivery_id}, and replayed from DLQ withPOST /v1/deliveries/{delivery_id}/replay RunView.deliveriesshows redacted delivery state for the run
