One endpoint, documented exactly
There is no public REST API, no key issuance and no outbound webhook — those are on the roadmap and this page will not pretend otherwise. What exists is the inbound endpoint, and it is enough to put your own systems in front of Systeni today.
Send a message in
Anything that can make an HTTPS request can hand work to Systeni. The run it starts is inspectable in Operations, governed by your policies, and stops for a person where you have said it should.
POST /api/inbound
curl -X POST https://systeni.com/api/inbound \
-H "content-type: application/json" \
-H "x-systeni-secret: $SYSTENI_SECRET" \
-d '{
"organizationId": "org_...",
"channel": "web",
"from": "anna@example.com",
"name": "Anna Reuter",
"body": "Do you fit flat roofs? Around 180 square metres, Hamburg.",
"externalId": "thread-4471",
"messageId": "msg-9930"
}'The secret is per workspace and is shown once, in Operations, when it is issued. Issuing a new one retires the old immediately. It is stored as a peppered digest, so a database dump does not disclose it — which also means we cannot show it to you again.
The body
| Field | Required | What it is |
|---|---|---|
| organizationId | required | Your workspace id. Shown with the secret in Operations. |
| from | required | Whatever the channel knows the person by: an address, a number, a handle. |
| body | required | The message. Anything past 10,000 characters is cut. |
| channel | optional | web, email, whatsapp, sms, voice or instagram. Defaults to web. |
| name | optional | The person's name, if the channel has it. |
| company | optional | Their company. |
| phone | optional | A number, when it is not already the `from`. |
| subject | optional | A subject line, where the channel has one. |
| locale | optional | A language tag, so the reply is written in theirs. |
| externalId | optional | The channel's id for the thread. Two messages with the same one continue a conversation instead of starting a second. |
| messageId | optional | The channel's id for this message. A redelivery with the same one is ignored rather than processed twice. |
What comes back
Status codes a retry policy can be written against.
| 202 | Accepted. The run may still be executing or waiting for a person — which is why this is not a 200. |
| 400 | The body was not JSON, or a required field is missing, or the channel is not one of the six. |
| 401 | No secret, or the wrong one. The same answer is given for an organisation that does not exist, because the difference is what an attacker enumerating ids wants. |
| 405 | Anything other than POST. |
| 413 | The body is larger than 32 KB. That is not an enquiry. |
| 429 | A limit was reached. `Retry-After` says when. |
| 500 | Intake failed on our side. Retry — every step carries an idempotency key, which is what makes inviting the retry safe. |
Limits
60 a minute, per origin
Counted before the secret is read, so guessing it costs the guesser time rather than costing us a database lookup per attempt.
120 a minute, per workspace
An inbound message starts a model-billed chain. The monthly ceiling bounds the bill; this bounds the day.
Not built
Listed because a developer needs to know what is missing before they design around it, not after.
- No public REST API. The application's server functions are internal and are not a product surface.
- No API keys, scopes or versioning. The inbound secret is per workspace and does one thing.
- No outbound webhooks and no event catalogue. Events are recorded and readable in the console; nothing is pushed to you.
- No SDK. The endpoint is one POST and a client library would be a wrapper around curl.
- No sandbox. Send a test message from Operations, which runs against your own workspace and appears in the same execution record.
Try it against your own workspace
Operations issues the secret, sends a test enquiry and shows the run it starts — every step, every decision, and the point where it stopped for a person.