Security
You are about to give a scheduler a URL and, usually, a credential that opens it. This page says exactly what happens to that credential, how your endpoint can prove a request came from us, and which addresses we refuse to call. It names mechanisms and parameters rather than adjectives, so you can check it rather than trust it.
Credentials are encrypted at rest
Every secret you give a job — a basic-auth password, a bearer token, an API key, an HMAC signing secret, or the value of any request header you mark secret — is encrypted before it is written to the database, with AES-256-GCM.
A fresh 96-bit initialisation vector is generated per encryption, and the authentication tag is stored alongside the ciphertext, so a tampered record fails to decrypt rather than decrypting to something else. Stored form is v1:<iv>:<tag>:<ciphertext>, base64url. The 32-byte key lives in the deployment environment, never in the database, so a dump of the database on its own decrypts nothing.
Secrets are write-only from the API’s point of view. No endpoint returns them: responses are built from an explicit allow-list of fields rather than by serialising the stored record, and a secret header’s value comes back as an empty string. The edit form works without ever receiving your current secrets — which is why leaving a secret field blank there means “keep the stored one”.
Your endpoint can verify the request came from us
Any job can be configured to sign its request body. getNodi computes an HMAC-SHA256 over <timestamp>.<body> using a secret only you and we hold, and sends it in a header — X-Nodi-Signature by default — in the form t=1771243800,v1=<hex>.
This is the same scheme Stripe uses for its webhooks, chosen so the verification code you may already have written is the code that works here. Because the timestamp is inside the signed string, a captured request cannot be replayed later without the signature failing — provided you reject old timestamps, which is the one part your side has to do.
Every request also carries X-Nodi-Job-Id, X-Nodi-Run-Id and X-Nodi-Attempt, so a retry is distinguishable from a first delivery and a duplicate is distinguishable from a new run.
We refuse to call addresses we should not
A cron service accepts arbitrary URLs from users and then fetches them from its own servers. Without a guard that is a server-side request forgery engine: it would let anyone reach our internal network, our database and queue hosts, or the cloud instance-metadata endpoint at 169.254.169.254, which on many providers hands out credentials to whoever asks.
Validation runs against the resolved addresses, not the hostname. A DNS record you control that points at 127.0.0.1 is still refused, because the check happens after resolution and every address a name resolves to is examined, not just the first.
These IPv4 ranges are refused outright:
- 0.0.0.0/8
- 10.0.0.0/8
- 100.64.0.0/10
- 127.0.0.0/8
- 169.254.0.0/16
- 172.16.0.0/12
- 192.0.0.0/24
- 192.0.2.0/24
- 192.168.0.0/16
- 198.18.0.0/15
- 198.51.100.0/24
- 203.0.113.0/24
- 224.0.0.0/4
- 240.0.0.0/4
On IPv6 only global unicast (2000::/3) is allowed, so unique-local, link-local and multicast are refused, as are the documentation range 2001:db8::/32 and the NAT64 prefix 64:ff9b::/96. IPv4-mapped addresses are unwrapped and judged as IPv4, so ::ffff:127.0.0.1 does not slip through.
Also refused: any scheme that is not http or https, credentials embedded in the URL (they would end up in logs and execution records), and the hostnames localhost, *.localhost and *.internal. Anything that cannot be parsed or resolved fails closed — the ambiguous case is a refusal, not an attempt.
API keys are stored as hashes
An API key is shown to you once, at creation, and never again — what we store is its SHA-256 digest plus a short non-secret prefix so you can tell your keys apart in the list. A stolen database gives an attacker hashes, not keys. Comparison at authentication time is constant-time, so a key cannot be recovered by timing the endpoint.
Keys carry explicit scopes and an optional expiry, so a key minted for a read-only dashboard cannot create or trigger jobs.
Accounts and sessions
Passwords are held by Amazon Cognito, not by us — we never see or store one. Sessions are JWTs with a 30-day life.
Because a session can outlive a decision made about the account, suspension is re-checked against the database on every authenticated request rather than read from the token, and operator privileges are re-read from the database on every use. Suspending or demoting someone takes effect immediately, not at their next sign-in.
Payments are processed by Razorpay on their own hosted page. Card, UPI and netbanking details never reach getNodi.
What we keep, and for how long
We record every run: status, HTTP code, latency, response headers and the first few kilobytes of the response body. How much of the body is kept is a per-plan limit, from 2 KB to 128 KB, and there is a hard ceiling above every plan so one pathological response cannot bloat the store.
Individual run records are deleted automatically once they pass your project’s retention window — enforced by the database itself with a TTL index, not by a cleanup job that can quietly stop running. Aggregate hourly counts are kept beyond that so long-range charts still draw; they contain no response content.
Request headers are redacted in our own operational logs, and stored response snippets are truncated before they are written.
What this page does not claim
We are not certified against SOC 2, ISO 27001 or any similar standard, and we are not going to imply otherwise with a badge. What is described above is what is implemented; it is not an audit and it is not a warranty.
A signed DPA, a security review and an uptime SLA are things we do on the Enterprise plan, individually and in writing, rather than features that flip on. If you need them, talk to us and we will tell you honestly whether we can meet your requirement.