SolidHooksSolidHooks/ Docs

Private delivery

Deliver webhooks to services inside your private network with SolidHooks Connect, using an outbound-only agent and no public URL.

A destination does not have to be reachable from the internet. SolidHooks Connect lets you deliver to a service inside your own network or cluster by running a small agent there. The agent only makes outbound connections, so you open no inbound firewall rule and expose no public hostname.

For a walkthrough of the idea, see Private network delivery.

How it works

A connector represents one of your networks. It belongs to the organisation, not to a project, so one connector serves every project. Inside that network you run one or more replicas of the agent. Several replicas give you high availability.

When a delivery is due, SolidHooks dials the connector by name and the agent makes the HTTP request from inside your network. A private destination points at an in-network URL such as http://payments.default.svc.cluster.local:8080/hooks, and everything downstream (connections, retries, the attempts log, billing per delivery) behaves as it does for a public http destination.

Set it up

Create a connector

curl -s "$API/v1/connectors" \
  -H "authorization: Bearer $SDHK_KEY" \
  -H 'content-type: application/json' \
  -d '{ "name": "prod-cluster", "description": "EU production" }'

The response includes enrollmentKey, a bootstrap token starting cbt_. It is shown only here and when you regenerate it, so store it immediately. It does not expire.

Creating a connector mints a credential, so it needs an API key scoped connector: read & write. Anything less gets 403 INSUFFICIENT_API_KEY_PERMISSIONS.

Run the agent in your network

The dashboard shows the exact docker run or helm install command with your token in it. For a single replica with Docker:

docker run -e ENROLLMENT_KEY=cbt_... \
  -v solidhooks-connector:/ziti \
  interstellarlabs/solidhooks-connector

Mount a writable volume at /ziti: the agent keeps its identity and heartbeat token there, and the server stores only a hash of the heartbeat token, so a lost one cannot be fetched again. For high availability, use the solidhooks-connector Helm chart with --set replicaCount=N.

Each replica calls the public POST /v1/connectors/bootstrap with { "enrollmentKey": "cbt_..." } to enrol itself. It gets back a one-time enrolment token and a reveal-once heartbeat token starting crh_.

Create a private destination

curl -s "$API/v1/destinations" \
  -H "authorization: Bearer $SDHK_KEY" \
  -H 'content-type: application/json' \
  -d '{
    "name": "In-cluster payments service",
    "type": "private",
    "connectorId": "cntr_...",
    "url": "http://payments.default.svc.cluster.local:8080/hooks",
    "method": "POST"
  }'

url is still validated as a URL, but it does not need to resolve publicly. It is the address the agent resolves inside your network.

Provisioning takes a few seconds

A connector has a provisionedAt timestamp that is null until its infrastructure is ready. While it is null, creating a private destination against it returns 400 and deleting it returns 409 CONNECTOR_PROVISIONING. Both are retryable. This is separate from isDisabled, which is a switch you control.

Heartbeats and revocation

Each replica calls the public POST /v1/connectors/replicas/heartbeat with { "heartbeatToken": "crh_..." }. The stock agent does this every 60 seconds, tunable with HEARTBEAT_INTERVAL_SEC. A healthy beat returns { "ok": true }.

A replica is revoked, and its slot freed, only when both of these are true:

  • it has not sent a heartbeat for 24 hours, and
  • it does not appear connected.

Requiring both means a replica that is serving traffic but cannot reach the SolidHooks API is never revoked.

What a heartbeat returns for a bad token:

TokenResponse
Belongs to a revoked replica403 CONNECTOR_REPLICA_REVOKED, with a machine-readable reason: connector_deleted, replica_revoked or identity_absent. The stock agent logs the reason and stops its container. It does not re-enrol.
Unknown, or its revocation record has aged out (records are kept 30 days)An opaque 401 with a generic message, so the endpoint cannot be used to probe which credentials are live.

If an agent stops after a 403, remove that container and its volume, then start a fresh one with the same ENROLLMENT_KEY. It enrols as a new replica. Do not delete the connector to fix one replica: that revokes the healthy ones too, and is refused while any destination still uses it.

Reading health

GET /v1/connectors/{id} reports a status aggregated across replicas, plus connectedReplicas and totalReplicas.

StatusMeaning
pendingNo replica has enrolled yet.
connectedAt least one replica is up.
disconnectedReplicas are enrolled, all reported, and none is reachable.
unknownNothing is connected and at least one replica went unreported. SolidHooks could not reach the overlay controller, which says nothing about your agent. Retry rather than treating it as an outage.

GET /v1/connectors/{id}/replicas returns one row per enrolled replica with its own status (the same four values) and last-seen time. Use it to tell "3 of 3 up" from "1 of 3 up" when the aggregate says connected.

Plan limits

EntitlementWhat it capsError
maxConnectorsConnectors per organisation, checked at POST /v1/connectors403 CONNECTOR_LIMIT_REACHED with data: { limit, current }
maxReplicasPerConnectorReplicas enrolled per connector, checked at POST /v1/connectors/bootstrap403 REPLICA_LIMIT_REACHED with data: { limit, current }
PlanmaxConnectorsmaxReplicasPerConnector
Hobby0 (not available)not set (not available)
Launch12
Scale55
Enterpriseunlimitedunlimited

The limits apply only when you create. If your plan shrinks below what you already have, existing connectors and replicas keep working and only new ones are blocked.

Other connector endpoints

EndpointPurpose
GET /v1/connectorsList connectors with aggregate status.
GET /v1/connectors/optionsLightweight { id, name } list, for a destination form's picker.
POST /v1/connectors/{id}/regenerate-enrollmentMint a new bootstrap token and invalidate the previous one. Enrolled replicas keep running. Needs connector: read & write.
POST /v1/connectors/{id}/toggle-disabledA disabled connector rejects new private destinations. Enrolled replicas keep beating, because disabling is not revocation. Needs connector: read & write.
DELETE /v1/connectors/{id}Delete a connector. Blocked with 400 while any destination references it. Needs connector: read & write.

Private delivery is optional on a deployment

If the deployment has not been configured for private delivery, connector mutations return 400 BAD_REQUEST. Treat it as a capability that may not be available.

Next steps