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-connectorMount 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:
| Token | Response |
|---|---|
| Belongs to a revoked replica | 403 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.
| Status | Meaning |
|---|---|
pending | No replica has enrolled yet. |
connected | At least one replica is up. |
disconnected | Replicas are enrolled, all reported, and none is reachable. |
unknown | Nothing 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
| Entitlement | What it caps | Error |
|---|---|---|
maxConnectors | Connectors per organisation, checked at POST /v1/connectors | 403 CONNECTOR_LIMIT_REACHED with data: { limit, current } |
maxReplicasPerConnector | Replicas enrolled per connector, checked at POST /v1/connectors/bootstrap | 403 REPLICA_LIMIT_REACHED with data: { limit, current } |
| Plan | maxConnectors | maxReplicasPerConnector |
|---|---|---|
| Hobby | 0 (not available) | not set (not available) |
| Launch | 1 | 2 |
| Scale | 5 | 5 |
| Enterprise | unlimited | unlimited |
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
| Endpoint | Purpose |
|---|---|
GET /v1/connectors | List connectors with aggregate status. |
GET /v1/connectors/options | Lightweight { id, name } list, for a destination form's picker. |
POST /v1/connectors/{id}/regenerate-enrollment | Mint a new bootstrap token and invalidate the previous one. Enrolled replicas keep running. Needs connector: read & write. |
POST /v1/connectors/{id}/toggle-disabled | A 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
- Destinations — the
privatedestination type - Retries — retries work the same for private destinations
- Plans — connector entitlements per plan
- Private network delivery — the use-case guide