Skip to main content
Suppio calls your customer action executor through a public HTTPS URL. The same endpoint can receive customer action requests and ticket closing notifications. Your executor can run on a VPS, inside a private network connected through a tunnel, or on a managed application platform. This guide covers the complete network path. Read Customer actions for action definitions, request signatures, payloads, results, confirmations, and idempotency.

How the connection works

127.0.0.1:8787 is not a public address. It is the loopback address of the machine running your executor. On a VPS, only processes on that VPS can connect to it. Suppio never connects to 127.0.0.1. It connects to a public URL such as:
A reverse proxy or tunnel receives that request and forwards it to the executor on the same machine.

What Suppio expects

Your configured executor URL must meet every requirement below. Before each outbound request, Suppio resolves the hostname and continues only when every returned address is public. A hostname that has no public answer or includes any private or reserved answer is rejected. Suppio uses a strictly public network path and never follows redirects.
Do not put a browser login, basic authentication prompt, CAPTCHA, or interactive access page in front of the action path. Suppio authenticates with its signature headers and cannot complete an interactive challenge.

Start with the complete Node.js executor

Use the runnable secure template before publishing the endpoint. It includes every request, signature, schema, authorization, and idempotency check required by the lifecycle below.

Choose how to publish the executor

VPS with Caddy

Point a domain at the VPS. Caddy provides TLS and proxies requests to the loopback service. The VPS public IP appears in DNS.

Cloudflare Tunnel

Create an outbound tunnel from the server. You do not need to open an inbound executor port, and the origin IP does not need to appear in public DNS.

Managed hosting

Deploy the executor to a container or application platform that supplies a stable public HTTPS URL.

Option 1: VPS with Caddy

This is the simplest direct VPS setup. The examples assume a Debian or Ubuntu server, a domain you control, and an executor application installed in /opt/customer-actions.

Before you start

You need:
  • a VPS with a public IPv4 address or properly configured public IPv6 address
  • SSH access with sudo
  • a domain or subdomain you control
  • the complete Node.js executor example, or an equivalent implementation in your stack
  • the signing secret shown by the Suppio dashboard
Do not use CUSTOMER_ACTION_ENCRYPTION_KEY in the executor. Your executor only needs the per-agent signing secret shown when you save or rotate the endpoint.
1

Bind the executor to loopback

Run the application on 127.0.0.1, not 0.0.0.0. A Node and Express application can finish its setup like this:
Test it from the VPS:
This command should return {"ok":true}. It should fail when run from another machine because port 8787 is not public.
2

Store the signing secret outside the application

Create a dedicated service account and a root-readable environment file:
Add these values in the editor. Replace the placeholder only on your server:
ALLOWED_DISCORD_USER_IDS is only the example’s temporary global allowlist. Replace it with resource-level authorization in your handler. Never commit this file, paste its contents into support messages, or include it in application logs. If your application needs other credentials, store them in the same protected secret system or your provider’s secret manager.
3

Run the executor as a service

Place package.json, its lockfile, and server.mjs in /opt/customer-actions, then install production dependencies with npm ci --omit=dev. Create /etc/systemd/system/customer-actions.service:
StateDirectory=customer-actions creates /var/lib/customer-actions and keeps that path writable even with ProtectSystem=strict. Store the durable idempotency database there. Update ExecStart if your entry file uses a different path. Then enable the service:
View recent service logs without printing environment variables or request payloads:
4

Point a hostname at the VPS

Create an A record with your DNS provider:
This produces actions.example.com. Add an AAAA record only when the VPS has working public IPv6 and its firewall is configured for IPv6.Check DNS from a machine outside the VPS:
A direct DNS record reveals the VPS public IP. Use Cloudflare Tunnel instead if you do not want the origin IP in public DNS.
5

Install and configure Caddy

Install Caddy using its official installation instructions. The official Debian and Ubuntu package runs Caddy as a system service.Replace /etc/caddy/Caddyfile with:
Validate and reload the configuration:
Caddy obtains and renews a publicly trusted TLS certificate when the hostname points to the VPS and ports 80 and 443 reach Caddy. See the Caddy HTTPS reverse proxy guide for the underlying behavior.
6

Configure the firewall

Keep SSH available, allow HTTP and HTTPS for Caddy, and do not open port 8787:
Confirm the SSH rule before enabling the firewall so you do not lock yourself out. If your VPS provider has a separate network firewall, apply the same inbound rules there. Ubuntu documents the available commands in its firewall guide.
7

Verify public HTTPS

Test the public health endpoint:
Confirm that an unrelated path returns 404:
Do not test the action path with a hand-written unsigned request and expect success. Use Test connection so Suppio sends the correctly signed customer_action.test event.
8

Connect the endpoint in Suppio

In Customer actions, enter:
Select Save endpoint, copy the signing secret into /etc/customer-actions.env, restart the executor, and then select Test connection:
Turn on the endpoint after the connection test succeeds. The endpoint switch saves immediately.

Option 2: Cloudflare Tunnel

Cloudflare Tunnel is useful when you do not want to publish the VPS origin IP or open inbound ports 80 and 443. The cloudflared service makes an outbound connection from your server to Cloudflare and forwards the public hostname to 127.0.0.1:8787. You need a domain managed in Cloudflare and the executor already running on loopback.
1

Create a production tunnel

In Cloudflare, open Networking and then Tunnels. Create a remotely managed tunnel and choose the operating system used by your server.Follow Cloudflare’s Create a tunnel guide. Run the installation command Cloudflare generates on the VPS.The installation command contains a tunnel credential. Treat it like a secret. Do not commit it, place it in documentation, or paste it into logs or support messages.
2

Publish the executor hostname

Add a published application route with:
Cloudflare forwards the original path, so Suppio can use https://actions.example.com/suppio/actions.
3

Keep the action path machine-accessible

Do not require a Cloudflare Access login, browser challenge, or CAPTCHA on /suppio/actions. If you use WAF or bot rules, create a narrow exception that allows POST requests to the action path to reach your executor. The executor must still verify every Suppio signature.
4

Check tunnel connectivity

Confirm that the tunnel is healthy in Cloudflare and that the public health endpoint works:
cloudflared uses outbound connections. If your egress firewall is restrictive, review Cloudflare’s connectivity pre-checks.
5

Test and enable the endpoint

Save https://actions.example.com/suppio/actions in Suppio, install the displayed signing secret in your executor, select Test connection, and then turn on the endpoint.
A random trycloudflare.com Quick Tunnel is appropriate for temporary development only. Its hostname can change, and Cloudflare does not provide a production uptime guarantee for Quick Tunnels. Use a remotely managed tunnel with your own stable hostname for real actions.

Option 3: Managed application hosting

A managed container, application, or serverless platform can work when it provides a stable public HTTPS hostname. Configure the platform so that:
  • the action route accepts direct HTTPS POST requests without a browser login
  • the signing secret is stored in the platform’s secret manager, not in source code
  • your framework exposes the original raw request body before JSON parsing
  • the platform does not redirect the configured action path
  • cold starts and action execution finish within 15 seconds
  • the application accepts request bodies up to 64 KB
  • the response body stays below 64 KB
  • the application has a persistent database or durable store for action_id and event_id deduplication
  • logs exclude signature headers, secrets, raw request bodies, arguments, and result data
Some serverless frameworks parse and reconstruct JSON before your handler runs. Signature verification fails if you calculate the HMAC over reconstructed JSON. Enable the platform’s raw-body mode and verify the signature before calling JSON.parse. Use the exact HTTPS URL shown by the platform, including the final action path. Custom domains are optional, but the URL must remain stable.

The complete request lifecycle

For every request, verify the shared envelope first:
  1. Receive the HTTPS request at the exact configured path.
  2. Limit how much request data the server accepts.
  3. Read the timestamp and signature headers.
  4. Reject a timestamp more than five minutes away from the executor’s clock.
  5. Calculate the HMAC over the exact raw body and compare it in constant time.
  6. Parse JSON only after the signature succeeds.
For customer_action.execute and customer_action.test:
  1. Confirm that X-Suppio-Action-Id matches action_id.
  2. Return success without mutating data when event is customer_action.test.
  3. Reject action names that are not in an explicit handler allowlist.
  4. Validate every argument again in your application.
  5. Authorize actor.user_id for the targeted account or resource.
  6. Atomically claim action_id in durable storage. Return the stored result if that ID already completed, and never repeat a result whose completion is uncertain.
  7. Perform the action once.
  8. Store and return a succeeded or failed JSON result within 15 seconds. Return both known outcomes with HTTP 2xx.
For ticket.resolved:
  1. Confirm that X-Suppio-Event-Id matches event_id.
  2. Validate source, actor, conversation, and agent.
  3. Atomically store event_id and the work your integration must perform. If the same ID arrives again, acknowledge it without creating duplicate work.
  4. Return any 2xx response after the event is durably accepted. Suppio ignores the body.
Ticket closing notifications use at-least-once delivery. Suppio retries network failures, timeouts, HTTP 408, HTTP 429, and 5xx responses. Your event_id claim prevents those retries from applying the same close operation twice. The signature proves that Suppio sent an unchanged request. It does not prove that the Discord user is authorized to modify a record in your application. Your executor remains responsible for that authorization decision.

Security checklist

  • Bind the executor to 127.0.0.1 when a reverse proxy or tunnel runs on the same machine.
  • Run the executor as a dedicated non-root user.
  • Keep the signing secret in a protected environment file or secret manager.
  • Verify signatures against raw bytes before parsing JSON.
  • Keep the server clock synchronized so timestamp checks are reliable.
  • Persist action_id and event_id values before applying side effects.
  • Use explicit action handlers instead of dynamic code, commands, URLs, SQL, or unrestricted operation names.
  • Validate and authorize arguments even though the action has a strict schema.
  • Do not log secrets, signature headers, request bodies, action arguments, or executor result data.
  • Rotate the signing secret immediately if it may have been exposed.
  • Keep the operating system, runtime, reverse proxy, tunnel client, and dependencies updated.

Troubleshooting

Use a public domain name with https://. Suppio rejects IP-address URLs, localhost, loopback addresses, private network addresses, and hostnames whose DNS includes a private or reserved address.
Check the A or AAAA record with dig +short actions.example.com. Wait for the DNS record’s time to live after a change. Remove an incorrect AAAA record if the server does not have working public IPv6.
Confirm that the hostname points to the VPS, ports 80 and 443 reach Caddy, no other service occupies those ports, and the domain’s certificate authority authorization records permit a public certificate.
The proxy or tunnel cannot reach the executor. Run curl http://127.0.0.1:8787/health on the server, inspect the executor service, and confirm the configured local port matches.
Enter the final HTTPS URL and path in Suppio. Remove application redirects such as adding or removing a trailing slash. Suppio does not follow redirects for action requests.
Use the exact signing secret shown for that agent. Verify the untouched raw body, not parsed or reformatted JSON. Confirm that the proxy preserves the request body and that the timestamp is treated as a string when constructing the signed value.
Remove interactive Access, CAPTCHA, and browser challenges from the action path. Adjust WAF rules narrowly for that path while keeping signature verification mandatory in the executor.
Turn on the executor endpoint, enable the individual action definitions, and start a new Discord ticket or forum workflow run. Existing run traces do not change retroactively.
Test connection sends customer_action.test, not ticket.resolved. Make sure your route handles ticket.resolved before it requires action_id or X-Suppio-Action-Id. Validate X-Suppio-Event-Id against event_id, accept the documented nullable fields, and return any 2xx response after durably storing the event. Run the two-envelope smoke test in the complete executor example to verify both branches.
The request was dispatched, but Suppio lost the connection or reached the 15-second timeout. The mutation may have completed. Look up the request by action_id; do not repeat it automatically.

Go-live checklist

Before enabling production actions, confirm that:
  • the public URL uses a valid HTTPS certificate
  • the URL returns no redirect
  • the executor listens only on loopback or through the intended private network
  • the action path has no interactive access challenge
  • the signing secret is stored outside source control
  • signature and timestamp checks run before parsing, and the matching action ID or event ID is validated afterward
  • every action name maps to one explicit handler
  • durable idempotency is working
  • known failures return HTTP 2xx with structured failure results
  • the executor responds in less than 15 seconds
  • Test connection succeeds
  • the signed ticket.resolved smoke test returns 2xx when you use ticket closing notifications
  • the endpoint and intended actions are enabled