Relay / HTTP API referencePRIVATE PILOT

HTTP API reference

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.

Authentication, scopes, and errors

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.

Shared response objects

Connection

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.

Operation

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, event, and delivery

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.

Service and connections

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.

GET /v1/connections

Lists connections in the key's workspace, ordered by creation time. Input: no query or body. Scope: connections:read. Output: 200 {"connections":[Connection,…]}.

POST /v1/connections

Creates 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}/disconnect

Clears 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.

Durable operations

POST /v1/connections/{connection_id}/operations

Submits 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}/challenge

Submits 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}/cancel

Requests 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}/reconcile

Asks 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/invite

Compatibility 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.

Subscriptions, events, and deliveries

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}/subscriptions

Lists subscriptions for a connection. Input: path UUID, no query or body. Scope: events:read. Output: 200 {"subscriptions":[Subscription,…]}.

POST /v1/connections/{connection_id}/subscriptions

Creates 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}/listener

Reads 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}/events

Lists 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}/deliveries

Lists 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}/replay

Queues 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.

Callback contract

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.

Downloads