GET /healthz
Checks service and database reachability. Input: no
query or body. Scope: none after hosted Cloudflare
Access. Output: 200 {"status":"ok"} or
503 {"status":"unavailable"}. This does not check a
LinkedIn session.
Every route below is relative to the Relay API base URL. The hosted
pilot uses https://rpa.essedo.com. JSON is the request and
response format except for 204 No Content. IDs in paths are
Relay UUIDs unless a route says otherwise.
Hosted requests need a named User-Agent,
CF-Access-Client-Id, and
CF-Access-Client-Secret. Every /v1/* request
also needs Authorization: Bearer <workspace-api-key>.
GET /healthz needs Cloudflare Access on the hosted pilot
but no workspace API key. Keep all credentials in your application's
secret store. Workspace keys are scoped; resources outside the key's
workspace are unavailable.
GET /v1/connections HTTP/1.1
User-Agent: customer-app/1.0
CF-Access-Client-Id: <access-client-id>
CF-Access-Client-Secret: <access-client-secret>
Authorization: Bearer <workspace-api-key>
Required scopes are shown for each endpoint. Authentication and scope
failures are 401 and 403; unknown or
inaccessible IDs are 404; validation errors are usually
400 with field detail. Some service conflicts return
{"code":"..."} and an HTTP 409; the
compatibility invitation route uses
{"error":{"code":"...","message":"...","details":{}}}. A
successful operation POST only confirms acceptance; inspect the
operation's eventual status, error_code, and
result.
| Field | Type | Meaning |
|---|---|---|
id |
UUID string | Relay connection ID. |
provider |
string | linkedin or simulated. |
external_actor |
object | {kind,id}. LinkedIn id can be null before
login. |
health |
string | ready, needs_login, or
invalid for LinkedIn. |
capabilities |
string[] | Available capability names. |
state_revision |
integer | Session generation; changes invalidate prior session ownership. |
region |
string | Two-letter LinkedIn region. |
regional_proxy_configured |
boolean | Whether an active proxy exists for that region. |
session_proxy |
object or null | {id,region,active,matches_region} for the pinned
session proxy. |
recruiter |
object | Separate Recruiter state: health, reason,
contract, verified_at,
verified_session_revision, operation_id. |
LinkedIn responses include the last four fields. They do not return the account password.
| Field | Type | Meaning |
|---|---|---|
id, connection_id |
UUID strings | Durable operation and its connection. |
capability |
string | Submitted capability. |
status |
string | queued, running,
waiting_user, succeeded, failed,
canceled, skipped, or
outcome_unknown. |
result |
object or null | Capability result on success or a structured skip reason. See the capability reference. |
error_code, error_details |
string/null, object/null | Safe failure classification and detail. |
challenge |
object or null | While waiting: {id,kind,expires_at,accepts_code}. |
cancel_requested |
boolean | A cancellation was requested; a running external action can still finish. |
created_at, updated_at |
ISO timestamps | Operation times. |
attempts |
object[] | Each has id, state_revision,
number, status, phase,
started_at, completed_at. |
timeline |
object[] | State changes with from, to,
reason, at. |
outcome_unknown means an external write may have
occurred. Inspect provider evidence before another send. The current
LinkedIn adapter has no automatic evidence reconciler.
Subscription has id,
connection_id, event_types,
callback_url, active, and
created_at. Creation alone adds secret,
returned once. Detail alone adds delivery_counts for
pending, sending, delivered,
failed, and canceled.
Event list items have id,
provider_event_id, type, and
received_at. Event detail adds connection_id
and data, the normalized provider event payload.
Delivery has id,
subscription_id, event_id,
status, attempt_count,
replay_count, next_attempt_at,
delivered_at, and last_error. Detail adds
attempts[], each with id,
sequence, outcome, status_code,
error, started_at, and
completed_at.
GET /healthzChecks service and database reachability. Input: no
query or body. Scope: none after hosted Cloudflare
Access. Output: 200 {"status":"ok"} or
503 {"status":"unavailable"}. This does not check a
LinkedIn session.
GET /v1/connectionsLists connections in the key's workspace, ordered by creation time.
Input: no query or body. Scope:
connections:read. Output:
200 {"connections":[Connection,…]}.
POST /v1/connectionsCreates a LinkedIn connection. Input: JSON
{"provider":"linkedin","username":"account@example.com","password":"<account-password>","region":"US"}.
Username is nonblank, at most 255 characters; password is nonblank, at
most 1024; region is a two-letter code. An operator must configure
regional proxy capacity. A synthetic test connection instead accepts
{"provider":"simulated","external_actor":{"kind":"person","id":"synthetic-1"}};
kind is person or installation.
Scope: connections:write.
Output: 201 Connection, initially
health:"needs_login" for LinkedIn. Possible
409 connection_conflict,
503 encryption_key_unavailable.
GET /v1/connections/{connection_id}Reads one connection. Input: path UUID, no query or
body. Scope: connections:read.
Output: 200 Connection.
PATCH /v1/connections/{connection_id}Changes a LinkedIn password, region, or both. Input:
path UUID and JSON with password (nonblank, at most 1024
characters) and/or region (two-letter code); no other keys.
Scope: connections:write.
Output: 200 Connection. A real change
increments the credential version, makes health
needs_login, and fences the old listener session; reconnect
with linkedin.login. An unchanged region alone is a no-op.
409 connection_busy if a lease is held.
POST /v1/connections/{connection_id}/disconnectClears the local LinkedIn session and stops the listener.
Input: path UUID, no body. Scope:
connections:write. Output:
200 {"health":"needs_login"}. It disables subscriptions,
cancels queued work and pending deliveries, and preserves event/delivery
history. After login, reactivate or recreate a subscription.
409 connection_busy is possible.
DELETE /v1/connections/{connection_id}Deletes a connection and its event/delivery history.
Input: path UUID, no body. Scope:
connections:write. Output:
204 with no body. Export needed history first.
409 connection_busy is possible.
POST /v1/connections/{connection_id}/operationsSubmits any listed LinkedIn capability. Input: path
UUID; required Idempotency-Key header (1–128 printable
characters); JSON
{"capability":"linkedin.identity.get","input":{}}. The
input schema varies by capability and is fully listed in
the capability reference.
Scope: operations:write;
linkedin.message.send,
linkedin.invitation.send, and
linkedin.attachment.send also need
linkedin:send. Output:
202 Operation for a new submission,
200 Operation plus Idempotency-Replayed: true
for the same key and same request. Reusing a key with different input
returns 409 {"code":"idempotency_conflict"}. A key is
unique across the workspace.
{"capability":"linkedin.people.search","input":{"keywords":"engineer","start":0,"count":25}}GET /v1/operations/{operation_id}Reads the current status, result, attempts, and timeline.
Input: path UUID, no query or body.
Scope: operations:read.
Output: 200 Operation. Poll this ID until
a terminal status. waiting_user requires challenge
handling; outcome_unknown needs manual evidence review.
POST /v1/operations/{operation_id}/challengeSubmits a code for a waiting login or Recruiter switch.
Input: path UUID and JSON
{"challenge_id":"<challenge-uuid>","code":"123456"};
code is 1–32 printable characters. The challenge_id is from
the operation's challenge.id. Scope:
operations:write. Output:
202 {"status":"accepted"}. Only a current, unexpired
email, sms, or authenticator
challenge accepts a code; otherwise
409 {"code":"challenge_not_accepting_code"}. Continue
polling the operation.
POST /v1/operations/{operation_id}/cancelRequests cancellation. Input: path UUID, no body.
Scope: operations:write.
Output: 200 Operation. Queued work becomes
canceled; running or waiting work gets
cancel_requested:true and may still have reached LinkedIn.
This cannot undo a provider action.
POST /v1/operations/{operation_id}/reconcileAsks the service to reconcile an unknown outcome.
Input: path UUID, no body. Scope:
operations:write. Output:
200 Operation; 409 not_outcome_unknown if the
operation is not unknown, or 409 connection_busy. The
current LinkedIn adapter has no automatic evidence reconciler, so its
unknown status remains unknown.
POST /v1/accounts/{account_id}/linkedin/inviteCompatibility invitation route for an older request shape.
{account_id} is the Relay connection UUID or LinkedIn
username. Input: JSON
{"hash_id":"<recipient-actor-id>","message":"Optional note"};
optional return_original:true requests raw provider data
for diagnosis. Supply Idempotency-Key for retries; without
it each call gets a new key. Optional Prefer: respond-async
skips the brief wait. Scope:
operations:write and linkedin:send.
Output:
202 {"data":{"operation_id":"...","status":"queued"}} for
async/pending;
200 {"data":{"result":{"code":"SENT","message":null}}} on
success (actual code/message depend on LinkedIn). Response headers
include Idempotency-Key and X-Operation-Id.
Unknown outcome is 409 with
error.details.operation_id; provider failure
424; account not ready 423. Poll the durable
operation ID for the full result. Raw response may contain provider data
and is for controlled diagnosis.
Selectable event types are message_received,
message_sent, sponsored_inmail_received,
custom_connection_request_accepted,
connection.stream_degraded, and
connection.stream_restored. The connection-request
classification can be a fallback; inspect provider state before treating
it as proof of acceptance.
GET /v1/connections/{connection_id}/subscriptionsLists subscriptions for a connection. Input: path
UUID, no query or body. Scope:
events:read. Output:
200 {"subscriptions":[Subscription,…]}.
POST /v1/connections/{connection_id}/subscriptionsCreates a callback subscription. Input: path UUID
and JSON
{"event_types":["message_received"],"callback_url":"https://hooks.example.com/relay"}.
event_types must be a nonempty list of distinct supported
values. callback_url must be public HTTPS with no query or
fragment. Optional secret is 32–255 characters; otherwise
one is generated. Scope: events:write.
Output: 201 Subscription plus
secret, shown only in this response. Save it securely to
verify callbacks. Relay persists accepted events before delivery
attempts.
GET /v1/subscriptions/{subscription_id}Reads configuration and delivery counts. Input: path
UUID, no query or body. Scope:
events:read. Output:
200 Subscription plus delivery_counts.
PATCH /v1/subscriptions/{subscription_id}Activates or deactivates a subscription. Input: path
UUID and exactly {"active":true} or
{"active":false}. Scope:
events:write. Output:
200 Subscription. Activation of a LinkedIn subscription
requires a ready connection or returns
409 connection_not_ready. Deactivation cancels
pending/sending deliveries.
GET /v1/connections/{connection_id}/listenerReads stream and continuity health. Input: path
UUID, no query or body. Scope:
events:read. Output: 200
object with stream_status, connection_health,
continuity_status, open_gap_count,
last_frame_at, last_confirmed_at, plus
status, reason, last_event_id,
last_event_at, heartbeat_at, and up to 50
gaps[]. Each gap has id, reason,
last_event_id, started_at,
closed_at. A connected stream does not establish that an
earlier gap was backfilled.
GET /v1/connections/{connection_id}/eventsLists persisted event metadata, newest first. Input:
path UUID, optional offset integer 0–100000 (default 0);
100 items per page. Scope: events:read.
Output:
200 {"next_offset":100|null,"events":[Event summary,…]}.
Continue while next_offset is non-null.
GET /v1/events/{event_id}Reads one normalized event payload. Input: path
UUID, no query or body. Scope:
events:read. Output:
200 Event detail including data. Treat payload
as customer data.
GET /v1/subscriptions/{subscription_id}/deliveriesLists callback delivery state, newest event first.
Input: path UUID; optional offset integer
0–100000 and status of pending,
sending, delivered, failed, or
canceled. 100 items per page. Scope:
events:read. Output:
200 {"next_offset":100|null,"deliveries":[Delivery,…]}.
GET /v1/deliveries/{delivery_id}Reads a delivery and its attempt history. Input:
path UUID, no query or body. Scope:
events:read. Output:
200 Delivery plus attempts[].
POST /v1/deliveries/{delivery_id}/replayQueues an explicit retry after a failed delivery.
Input: path UUID, no body. Scope:
events:write. Output:
202 Delivery with the new queued state, or
409 {"code":"<reason>"} if replay is not allowed.
Relay sends a JSON event callback with X-CS-Event-Id,
X-CS-Delivery-Id, X-CS-Timestamp, and
X-CS-Signature: v1=<hex>. The signature is
HMAC-SHA256 using the subscription secret over the ASCII timestamp, a
period, then the raw request body bytes. Compare in
constant time and reject stale timestamps. Return 2xx promptly, then
process asynchronously. Delivery is at least once; deduplicate by event
ID. Non-2xx responses and timeouts retry up to five attempts with 1, 5,
30, and 120 second delays before failed; replay is
explicit. Redirects are not followed. There is no automatic event
retention timer yet. LinkedIn's stream has no verified replay cursor, so
use conversation/message reads to investigate continuity gaps.
The callback JSON body is
{id,type,connection_id,provider_event_id,received_at,data}.
id is the Relay event UUID and matches
X-CS-Event-Id; data is the normalized event
payload. X-CS-Delivery-Id identifies this delivery attempt
series and is different from the event ID.