Security & threat model
1. What we protect
| Asset | Why it matters |
|---|---|
| Your organization’s data | Tenants, contracts, garages, documents and emails may only be seen by your integrations, never by another organization. |
| Tokens | A token is the key to your data within the scope of its permissions. |
| Webhook secrets | With the secret, a receiver can verify that a message comes from us; whoever knows it could forge messages. |
| Booking and payment data | Financial data is particularly sensitive; bank and payer details are only available with an additional permission. |
| Availability | The API and webhook delivery should stay usable for everyone, even with misuse or mass requests. |
2. Who might attack, and what we assume
- Another organization (a foreign tenant) trying to reach data or ids of your organization.
- Whoever holds a token — stolen, expired or already revoked — and misuses it.
- A malicious webhook receiver, or someone who tries to make us contact internal systems through a webhook address (SSRF), or who forges or replays messages.
- A network attacker who reads or alters traffic.
- Abuse through mass requests, deliberate or caused by a bug in an integration.
- Operational mistakes, such as a wrong setting or a secret passed on by accident.
We assume an attacker knows the public documentation and the OpenAPI description, but neither your tokens nor your webhook secrets. Not covered is an attacker who already has access to your own systems or to your account in the management app: whoever holds a valid token may do what its permissions allow.
3. Threats and protections
The tables list for each area the threat, the protection and the kind of check. Figures and limits are on the respective pages of the documentation.
Authentication and tokens
| Threat | Protection | How we check it |
|---|---|---|
| Token is stolen from the database | Only a keyed hash of the secret is stored, never the secret itself; the key needed for it is not in the database. The token is shown only once. | Automated tests, database checks |
| Token is too powerful | Each token carries only the permissions (scopes) you granted; the endpoints with data require the matching scope. The token only works as long as its creator still has the API access permission in the management app. | Automated tests, database checks |
| Stolen or expired token | Tokens have an expiry date. Revocation takes effect from the next request; the same applies when the organization’s access is switched off. Creating and revoking a token are recorded in the audit log. | Automated tests, database checks |
| Guessing tokens | The secret is random and very long. In addition, limits per address slow down failed sign-in attempts; if the counter needed for that fails, the API refuses instead of answering unthrottled. | Automated tests |
| Token in the browser or in links | A token in the address (query string) is rejected; cookies are not evaluated. The API is intended for server-to-server calls. | Automated tests |
Tenant isolation and responses
| Threat | Protection | How we check it |
|---|---|---|
| Access to another organization’s data | The organization comes from the token, not from details in the request. Queries run through the API’s database functions, which verify the token and the organization themselves (design rule); this check is the decisive boundary. Row-level security (RLS) of the tables protects the other access paths, such as the management app, but is not a second protection layer of the API. Where the API provides files (document and DATEV download links), a short-lived link for a path of your own organization is only created after the check in the database. | Automated tenant-isolation check during development, database checks |
| Trying ids of foreign objects | For single resources, unknown and foreign ids are answered the same way with 404, not with “no permission”. For the bulk lookups via by-ids, the API answers 200 and simply leaves out unknown, foreign and invisible identifiers. In both cases the API does not reveal whether an id exists elsewhere. | Automated tests |
| Internals in error messages | Errors have a fixed format with an error code and request_id. An internal error (500) gives no details; responses are not cached. | Automated tests |
Abuse and repetition
| Threat | Protection | How we check it |
|---|---|---|
| Mass requests | Limits per address, per token and per organization; the token limit counts reading and writing calls separately; the sandbox has tighter limits. See the “Limits” page. | Automated tests |
| Duplicate bookings through retries | Writing calls usually take an Idempotency-Key: if you repeat the same request with the same key, the result is returned instead of creating something twice. How long the key is kept and what it is bound to depends on the endpoint: for most it is the token (after a token change a new operation can be created), for some (such as imports, appointments, DATEV exports) the organization; taking over an import needs no key because it can safely be repeated by itself. After the retention period a late retry can start a new operation. The binding table per endpoint is under idempotency. | Automated tests |
| Traceability | Calls are logged (without the token and without contents), including rejected sign-ins with a known token identifier; you can see the history per token in the management app. The log is a best-effort aid and can be incomplete (for example with very many rejected sign-ins or during faults of the log). | Automated tests, database checks |
Webhooks
| Threat | Protection | How we check it |
|---|---|---|
| Forged message | Every message is signed (HMAC-SHA256 over the timestamp and the unchanged raw body). The receiver should check the signature with a constant-time comparison. Example code is in the webhooks handbook. | Shared test vectors for all examples, automated tests |
| Replayed message | We deliver a signed timestamp and a constant event id. The receiver protects against replay: it must reject messages older than 5 minutes and store processed event ids. The server does not enforce this. | Automated tests |
| Secret becomes known or is lost | The secret is stored only in encrypted form and shown only once. Rotation is possible at any time; the previous secret remains valid for another 24 hours so you can switch over without an outage. Only the immediately preceding secret stays valid: if you rotate a second time within these 24 hours, the transition period of the first secret ends immediately. | Automated tests |
| Webhook address as a lever against internal systems (SSRF) | Only https on port 443, only public targets. The address is checked on creation and on every send; the connection goes to exactly the checked addresses so that a later DNS change (DNS rebinding) changes nothing. Redirects are not followed, the receiver’s response content is not stored, and no internal tracing headers are sent along. | Automated tests |
| Lost or duplicate messages | Retries with a constant event id, detection of duplicate events, resending (replay) of failed deliveries, pausing instead of discarding when switched off, reconciliation through the list endpoints. This is a reliability topic and one of data integrity at the same time — see the limits below. | Automated tests, database checks |
Sandbox, status and management
| Threat | Protection | How we check it |
|---|---|---|
| Test and real data get mixed | The sandbox is its own organization with invented sample data. A test token only sees this data, and a real token never sees the sandbox. The assignment is enforced in the database; if it does not match, the token is rejected. Responses to test tokens carry the X-M2H-Sandbox header. | Automated tests, database checks |
| Customer data on the status page | The status page only shows aggregated values (state, availability, incidents, a few metrics) without details of individual customers, organizations or tokens. | Automated tests, database checks |
| Abusing token management from outside | Creating and revoking tokens works only when signed in to the management app and only with the API access permission. The management app is separate from the API host: the API host has no management pages. | Automated tests |
| Secrets in logs | The call log contains neither tokens nor contents; network error messages are mapped to fixed codes. Error monitoring masks known fields that carry credentials. | Automated tests |
4. Known limits and accepted residual risks
- Webhooks are not “exactly once”. Messages can arrive more than once, in a different order or late, and in rare cases an event can get lost. Process idempotently and reconcile regularly through the lists (see known limits of webhooks).
- The status page measures inside the same service it observes. If the whole service fails, the page itself may be affected. It does not replace monitoring of your own integration.
- A valid token is a key. Whoever holds it may do everything its permissions allow until it is revoked or expires. We cannot tell whether the caller is “really” your integration.
- No independent attestation. There is currently no independent penetration-test certificate and no certification (such as ISO 27001 or SOC 2) for the API. The checks listed below are our own automated and AI-assisted checks — they do not replace an independent audit.
- The organization check of the database functions is a design rule. Whether all functions of the API call it is verified in review; an automated test for it is still missing.
- Not every check targets the API specifically. Some of the general security checks run for the whole application, not specifically against the external API. The API’s database is additionally covered by its own checks.
5. What you should do
- Give each token only the permissions the integration really needs, and create a separate token per integration.
- Keep tokens and webhook secrets in a password manager or secret store — never in websites, browser apps, emails, chats or Git.
- Always verify the webhook signature, over the unchanged raw body, not over re-serialized JSON. Compare in constant time.
- Reject messages with a timestamp that is too old (5 minutes) and process each event id only once.
- Rotate webhook secrets regularly and immediately if one may have become known; revoke tokens you no longer need.
- Operate webhook receivers only over https on port 443 with a valid certificate and a publicly reachable address.
More under Security & support, Authentication and Webhooks.
6. How we check
We use several methods, each covering part of the threats above. They are our own checks and not a seal of approval from an independent body:
- Automated tests for tokens, permissions, signature, address checking, delivery and documentation (run on every change).
- An automated tenant-isolation check that tries to read one organization’s data from another.
- Database checks (“probes”) against the API’s database, also on a schedule.
- Static code analysis and dependency, secret and container scans in automated quality assurance (including Sonar, Semgrep, Trivy, gitleaks).
- Security reviews of the code, also AI-assisted, for larger changes to the API.
- General automated attack tests of the application (including SQL injection, API fuzzing, web scanners).
7. Reporting a vulnerability
Please report security concerns and vulnerabilities by email to security@manage2hero.com. The address is also in our security.txt (/.well-known/security.txt). We review your report and get back to you with any questions.
A report should contain:
- the affected endpoint or function
- the steps to reproduce the problem
- the suspected impact
- the
request_idof the response concerned, if there is one
Please do not:
- send a token or secret — not even to demonstrate the problem
- send or collect customer data
- access other organizations’ data to prove the vulnerability
- run load tests or denial-of-service attempts against production
If you have lost a token, first revoke it immediately in the management app (see Security & support).
Which changes we announce and how long old versions keep running is described under Versioning; changes to the security description appear in the changelog.