Cloud support
Submit Cloud support requests, follow private account tickets, and manage the operator queue.
https://openship.io/support and /contact use the same ticket intake. The
website forwards to POST /api/cloud/support on the Cloud API; its default is
https://api.openship.io. A development website can select a different Cloud API
with OPENSHIP_CLOUD_API_URL. The website needs no SMTP credentials or operator
token, and never forwards browser cookies or claimed client IPs.
The Cloud API stores support tickets and starts the delivery worker only when
CLOUD_MODE=true. Local installations expose a customer-only relay through the
existing Cloud connection. Authenticated tickets are private to their creator and Cloud
operators. Anonymous requests remain operator-only. All support records are
excluded from customer, project, and self-hosted instance exports.
Signed-in ticket center
https://app.openship.io/support lists the signed-in customer's tickets, with
topic, Open/Resolved status, subject/reference search, pagination and conversation
history. Support is the final sidebar entry on Cloud and Cloud-connected self-hosted
or desktop installations. It also remains visible for existing Cloud projects when
the connection needs attention. Checkout availability requests
created in the Cloud dashboard appear in the same history.
Browser requests require a session cookie and the usual trusted-origin checks for writes. The server-side Cloud connection uses its verified user session as a Bearer; this is a session credential, not an API token. Identity and contact information come from that session. API tokens, organization ownership and matching an email address do not grant access. Changing the selected team does not change ticket ownership. Existing anonymous website submissions are never automatically assigned to an account.
| Method | Path under /api/cloud/support | Result |
|---|---|---|
| GET | /session | The current support account, or account: null on local installations without a personal Cloud link |
| GET | /mine?status=open&search=build&limit=25 | Own tickets and nextCursor; pass it as before to load more |
| POST | /mine | Save a ticket with requestId (UUID), category, subject and message; return a receipt |
| GET | /mine/<reference> | Own report and customer/support replies |
| POST | /mine/<reference>/replies | Save a reply with requestId and message; return the conversation |
| PATCH | /mine/<reference> | Set status to open or resolved; return the conversation |
Categories are deployment, billing, account and general. Subjects allow
200 characters and messages 12,000. New tickets are limited to five per account
per 15 minutes; new replies to 30. Retry the same UUID and content after an
uncertain response. Confirmed retries do not create another message or consume
another quota slot. A new customer reply reopens a resolved ticket; retrying an
older reply does not undo a later resolution.
Replies use the existing durable mail outbox: customer replies notify the support mailbox, while operator replies appear in the conversation and are emailed to the customer. The dashboard does not expose SMTP attempts, errors, receipts or internal addresses. Inbound email replies still require the operator's mailbox workflow; they are not automatically imported into the ticket conversation.
Connected self-hosted and desktop installations
The local inbox uses the signed-in person's own verified Cloud link. Tickets stay
in Cloud, with the same history and email delivery as app.openship.io/support.
The shared organization connection does not give teammates access to its owner's
private tickets. A teammate without a personal link can open Support in Cloud and
sign in there. Disconnected installations offer Settings and Cloud Support links.
The local client reads /session first, then sends the returned account.key in
X-Openship-Support-Account on every ticket request. This value identifies the
connection and is not a credential. The relay rejects old or missing bindings with
409 SUPPORT_ACCOUNT_CHANGED and discards responses from a replaced connection.
The UI clears private state when the account changes and keeps drafts after transient
failures. Cloud session credentials stay on the server; only safe customer responses
and rate-limit retry guidance reach the browser.
The local relay validates input and exposes only the six customer endpoints above. Public intake and operator endpoints remain Cloud-only. Support failures do not clear the Cloud connection used by deployments. Install the updated Cloud API before updating connected installations so the session and customer endpoints are available.
Delivery and mailbox configuration
Provision [email protected] to receive mail. Configure the Cloud API's existing
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, and SMTP_FROM with a working
sender. For example, after that mailbox is provisioned:
SMTP_HOST=mail.oblien.com
SMTP_PORT=587
SMTP_USER=[email protected]
SMTP_PASS=<mailbox-password>
SMTP_FROM=Openship Support <[email protected]>Use credentials appropriate to the actual mail server; keep them in deployment secrets. This is an operator configuration example, not a credential requirement for the website. Creating a ticket does not depend on SMTP being available.
Each submission atomically saves a cloud_support_ticket and two
cloud_support_message deliveries: a receipt for the customer, and a notification
to [email protected]. The customer receipt contains a stable reference, no
reflected message text, and Reply-To: [email protected]. The team notification
includes the report and sets Reply-To to the requester. Ordinary inbound email
and email replies arrive in the support mailbox; this API does not ingest IMAP.
The client reuses its request UUID if the response is lost. Identical retries return the original receipt; different content under the same key returns 409. Intake validates bounded input and rate-limits new submissions by hashed contact address across Cloud replicas, in addition to the public per-IP ceiling.
Delivery begins after the database commit. The existing system scheduler runs
cloud-support:deliver every minute and resumes pending deliveries after restart.
SQL leases prevent concurrent workers from sending the same claimed message.
Failures back off from one minute to one hour and stop after ten attempts. The
ticket and error state remain available, and an operator can retry delivery.
Accepted mail is not resent during ordinary retries. Like other SMTP outboxes,
delivery is at least once across a crash after SMTP acceptance but before the DB
acknowledgement; a stable Message-ID is reused in that case.
Operator API
All routes below require X-Internal-Token matching the Cloud API's INTERNAL_TOKEN.
An organization owner, customer cookie, PAT, or arbitrary ticket reference is
insufficient. Responses are not cached. Do not put this token in client code.
| Method | Path under /api/cloud/support | Result |
|---|---|---|
| GET | /tickets?status=open&limit=25 | Queue, with nextCursor; use before=<nextCursor> for the next page |
| GET | /tickets/<reference> | Report, replies, delivery attempts, errors and next attempt times |
| POST | /tickets/<reference>/replies | Save and queue a reply; optionally resolve the ticket atomically |
| PATCH | /tickets/<reference> | Set status to open or resolved |
| POST | /tickets/<reference>/retry | Retry undelivered mail without stealing active sends or resending accepted mail |
Example listing:
curl --fail-with-body 'https://api.openship.io/api/cloud/support/tickets?status=open&limit=25' \
-H "X-Internal-Token: $OPENSHIP_INTERNAL_TOKEN"For replies, send JSON with a fresh UUID in requestId, the reply in message,
and resolve: true to close the ticket. Reuse the UUID when retrying an uncertain
response. A 202 response means the reply is saved and queued, not that SMTP has
delivered it. Read the ticket to check deliveredAt, lastError, and
nextAttemptAt for each message.
Deploy the API (including migrations 0152_cloud_support.sql and
0167_cloud_support_customers.sql) before the dashboard.
Then submit a test report from /support, check its reference through the operator
API, and verify receipt and team notification in the configured mailboxes.
Verification
apps/api/test/modules/cloud-support covers the website-to-API HTTP request, real
SQL persistence, the shared SMTP sender, recipient rejection and retry, concurrent
submission deduplication, worker lease recovery, operator access, replies,
resolution, pagination, input validation, cross-account isolation, anonymous-ticket
privacy, idempotent customer replies and the local relay. The connection integration
test exercises a real Cloud session through the existing transport and auth middleware,
and rejects PAT access. Dashboard tests cover connected and disconnected navigation,
desktop access, personal versus shared accounts, draft preservation, connection changes
and stale responses after navigation. SMTP tests use
a loopback server; they do not send mail to real customers.
Operations
| Operation | REST API |
|---|---|
| Save a Cloud support ticket and queue acknowledgement and team email. Public intake returns only a receipt and deduplicates retries. | POST /api/cloud/supportHandler authentication |
| List Cloud support tickets with status filtering and cursor pagination. Requires internal operator authority. | GET /api/cloud/support/ticketsInternal operator |
| Read a Cloud ticket, its replies, and email delivery attempts. Requires internal operator authority. | GET /api/cloud/support/tickets/:idInternal operator |
| Open or resolve a Cloud support ticket. Requires internal operator authority. | PATCH /api/cloud/support/tickets/:idInternal operator |
| Save and queue an idempotent support reply, optionally resolving the ticket in the same transaction. Requires internal operator authority. | POST /api/cloud/support/tickets/:id/repliesInternal operator |
| Retry undelivered support email while preserving active delivery leases and already accepted messages. Requires internal operator authority. | POST /api/cloud/support/tickets/:id/retryInternal operator |
| List the signed-in customer’s tickets with status, search and cursor pagination. Uses the personal Cloud session, including on connected local installations. | GET /api/cloud/support/mineHandler authentication |
| Read the private support account and connection binding. Local installations require the caller’s own verified Cloud link; shared organization credentials never grant private ticket access. | GET /api/cloud/support/sessionHandler authentication |
| Save an account-owned Cloud support ticket and queue the existing receipt and team notification. | POST /api/cloud/support/mineHandler authentication |
| Read the customer’s own conversation without internal delivery metadata. | GET /api/cloud/support/mine/:idHandler authentication |
| Save an idempotent customer reply, notify support and reopen the conversation. | POST /api/cloud/support/mine/:id/repliesHandler authentication |
| Open or resolve the signed-in customer’s own ticket. | PATCH /api/cloud/support/mine/:idHandler authentication |