Security

Real offensive depth

Testing and defence led by a published security researcher with five CVEs, including a CVSS 9.1 Mirai botnet kill-switch.

All security →
Compliance

Audit-ready, fixed scope

SOC 2, ISO, and the Canadian privacy stack, run end to end with an independent auditor.

All frameworks →
Resources

Learn the space

Original research, free tools, and plain-language guides on security and compliance, from a published security researcher.

Read the blog →
Security

API Security Best Practices for SaaS Startups

Your API is not just how your frontend talks to your backend. It is how partners integrate with your platform, how mobile apps access your data, and how attackers probe for vulnerabilities. In a SaaS application, the API is the primary attack surface, and securing it requires deliberate effort.

Authentication: Get the basics right

Use OAuth 2.0 or API keys, not session cookies, for API authentication. Session-based auth is fine for browser-based applications, but API clients need stateless authentication. For user-facing APIs, implement OAuth 2.0 with short-lived access tokens (15 to 60 minutes) and refresh tokens. For server-to-server APIs, use API keys with scoped permissions.

Never pass credentials in URL parameters. URLs are logged by proxies, browsers, and server access logs. Always pass API keys and tokens in HTTP headers (the Authorization header for tokens, a custom header like X-API-Key for API keys).

Implement rate limiting on authentication endpoints. Brute-force attacks against login and token endpoints are trivially easy to automate. Rate limit by IP address and by account. After 10 failed attempts, introduce a progressive delay or temporary lockout.

Authorization: The most common vulnerability

Broken access control is the #1 web application vulnerability according to OWASP. In API terms, this means users accessing data that does not belong to them. Because the flaw lives in your business logic rather than in a known CVE, it is the kind of issue a hands-on security review finds and an automated scanner usually does not.

Check authorization on every request. Do not rely on the frontend to hide resources the user should not see. Every API endpoint must verify that the authenticated user has permission to access the requested resource. This means checking tenant ownership, not just authentication.

Use indirect object references. Instead of exposing auto-incrementing database IDs in your API (GET /api/invoices/1234), use UUIDs or other non-guessable identifiers. Sequential IDs make it trivial for an attacker to enumerate resources by incrementing the ID.

Implement field-level authorization. Not every user should see every field on a resource. An admin might see a customer's billing information, but a read-only user should not. Filter response fields based on the user's role and permissions.

Want this handled? Tell us what your buyer is asking for and we will tell you what the work involves, what it costs, and what you can do yourself. Talk to us

Input validation

Validate and sanitize every input. Never trust data from the client. Validate data types, lengths, ranges, and formats on every request. Use an allowlist approach (define what is valid) rather than a blocklist approach (define what is invalid).

Prevent SQL injection. Use parameterized queries or an ORM. Never concatenate user input into SQL strings. This is basic, but we still find SQL injection vulnerabilities in production SaaS APIs regularly.

Prevent mass assignment. If your API accepts JSON bodies and maps them directly to database models, an attacker can set fields they should not have access to (like role or is_admin). Explicitly define which fields are allowed on each endpoint.

Transport security

TLS everywhere. Every API endpoint must use HTTPS. No exceptions. Redirect HTTP to HTTPS. Enable HSTS with a long max-age. Use TLS 1.2 minimum, TLS 1.3 preferred. Test your TLS configuration with SSL Labs.

Pin certificates for mobile apps. If you have a mobile app that talks to your API, implement certificate pinning to prevent man-in-the-middle attacks. This adds complexity to certificate rotation, so plan your rotation process before implementing pinning.

Logging and monitoring

Log every API request. Include the timestamp, IP address, user ID, endpoint, HTTP method, response code, and response time. Do not log request or response bodies by default (they may contain sensitive data), but have the ability to enable body logging for debugging.

Alert on anomalies. Set up alerts for unusual patterns: a spike in 401/403 responses (someone probing for access), a sudden increase in requests from a single IP (potential DDoS or scraping), or a user accessing an unusual number of resources (potential data exfiltration).

Implement an audit trail. For sensitive operations (deleting data, changing permissions, accessing PII), create an immutable audit log. This is a compliance requirement for SOC 2 and many enterprise customers.

Versioning and deprecation

Version your API from day one. Use URL-based versioning (/api/v1/) or header-based versioning. When you need to make breaking changes, release a new version and maintain the old one for at least 6 months. Communicate deprecation timelines clearly to your API consumers.

Document your API. Use OpenAPI/Swagger to document every endpoint, its parameters, and its response format. Keep the documentation up to date. Your API documentation is a security tool: it helps developers use your API correctly instead of guessing and making mistakes.

API security is not a one-time project. It is an ongoing practice. Review your API security quarterly, run penetration tests annually, and stay current with OWASP guidance. The cost of securing your API is a fraction of the cost of a breach.

Enforce tenancy at the data layer, not at the endpoint

Checking authorization on every request is the correct instruction and it fails in practice for a structural reason: it relies on every developer remembering, on every endpoint, forever. One hurried route added during a customer escalation is enough, and that route will be the one a tester finds.

The durable version pushes tenant scoping below the handler. Three patterns work. A repository layer where every query is constructed from a session object carrying the tenant identifier, so a query without a tenant filter is not expressible in the codebase. Database row-level security, where the policy lives in the engine and the application sets the tenant on the connection, which is strong precisely because it survives a developer writing raw SQL. Or a database per tenant, which is the simplest to reason about and the most operationally expensive.

Whichever you pick, back it with a test that runs in the build. Create two tenants in a test fixture, then for every route in your inventory, call it with tenant A's credentials against tenant B's resource identifier and assert a 404 or a 403. This is the single highest-value security test a SaaS product can have, it takes a day to build once you have a route inventory, and it catches the class of bug that turns into a breach notification under PIPEDA rather than a line in a report.

Prefer 404 over 403 for cross-tenant resources, incidentally. A 403 confirms the resource exists, which is a small disclosure that helps an attacker map your customer base.

You cannot secure endpoints you do not know about

Ask an engineering team for a list of their public endpoints and you will usually get the documented ones. The gap between that list and reality is where trouble lives: an internal admin API exposed through the same ingress, a legacy version kept alive for one customer, a debug route added during an incident, a webhook receiver written by someone who has since left.

Build the inventory from evidence rather than memory. Generate it from your framework's route table at build time, reconcile against what your gateway or load balancer actually accepts, and reconcile again against observed traffic over thirty days. The three lists will disagree, and each disagreement is worth explaining. Routes in traffic but not in code usually mean an old deployment still running. Routes in code but never called are candidates for deletion, and deletion is the cheapest security control available.

Treat that inventory as an artifact with an owner and a review date. Auditors increasingly ask for it, and enterprise security reviewers ask a version of it in every questionnaire.

Token and key mechanics

The advice to use short-lived access tokens is right and incomplete. Several details decide whether the implementation holds.

Validate the claims you think you are validating. Check the issuer, check the audience, and reject tokens whose audience is another service. A token minted for your analytics service should not open your billing API. Validate the algorithm against an allowlist rather than trusting the header, which is an old attack that still lands on hand-rolled verification code.

Plan revocation before you need it. Stateless tokens are not revocable by design, so decide now what happens when an employee is terminated at 09:00 and their token is valid until 09:45. The workable answers are a short expiry with a token version or session identifier checked against a fast cache, or a deny list of revoked identifiers with a lifetime equal to the maximum token expiry.

Rotate signing keys on a schedule. Publish a key set with more than one active key so rotation does not require simultaneous deployment, and actually run a rotation once before you need to run one urgently.

Store API keys hashed. A long-lived API key in your database is a credential, and a database dump should not hand over working credentials for every customer integration. Hash them, show the plaintext once at creation, and give each key a readable non-secret prefix so support can identify a key without asking for it. That prefix also lets secret-scanning tools recognize your keys in public repositories, which is how most leaked keys are actually discovered.

Give keys scopes, expiry and last-used timestamps. A key with no expiry and no usage record is impossible to retire safely, and the usual result is a set of keys nobody dares revoke.

GraphQL changes the rate limiting problem

If your API is GraphQL, request-count rate limiting is close to meaningless. One request can ask for a nested tree that fans out into thousands of database queries, and a batched array of operations arrives as a single HTTP request that most limiters count once.

The controls that work are query depth limits, complexity scoring where each field carries a cost and the total is budgeted per caller, and a cap on batch size. Disable introspection in production if your API is not public, though treat that as inconvenience for an attacker rather than protection, since your schema is inferable from the client bundle.

The authorization point is sharper too. Resolvers are composable, so a field that is safe at the top level can be reached through an unexpected path when nested inside another type. Authorization belongs on the resolver for each sensitive field, not on the entry point.

Webhooks are an API you forgot to secure, in both directions

Webhooks you receive. Verify a signature computed over the raw request body with a shared secret, using a constant-time comparison, and reject anything outside a short timestamp window so captured requests cannot be replayed. Verifying the source IP alone is weak, and verifying nothing at all means anyone who guesses the URL can post events into your system. Parse defensively as well, because a webhook receiver is an unauthenticated parser exposed to the internet.

Webhooks you send. Your outbound webhook feature lets a customer specify a URL that your server will then request, which is server-side request forgery offered as a product feature. Resolve the hostname, reject private and link-local address ranges, re-check after resolution to defeat rebinding, disable redirect following or revalidate every hop, and send from an egress path with no access to internal services or the cloud metadata endpoint. Sign your outbound payloads too, so your customers can verify them, and document the scheme.

Business logic abuse, which no scanner will find

The vulnerabilities that cost SaaS companies money are frequently not memory corruption or injection. They are ordinary features used at a rate or in an order the designer did not consider.

Pagination endpoints that accept an unbounded page size turn a legitimate read permission into a full database export. Bulk endpoints that accept an array of identifiers become an enumeration oracle. Trial signups without any friction become a compute subsidy for someone else's workload. Coupon and referral logic applied without atomic checks gets applied twice under concurrent requests. Password reset flows that respond differently for known and unknown addresses become a user enumeration tool for a phishing campaign.

Two controls generalize well. Put per-tenant quotas on expensive operations, expressed in units of work rather than requests, and return 429 with a Retry-After header so well-behaved clients back off instead of retrying into your incident. And write the abuse cases into your design reviews explicitly: for each new endpoint, ask what happens if it is called a thousand times a minute, called concurrently with itself, or called with the largest values the types allow.

Testing the authorization matrix in your build

Security testing that runs once a year finds regressions eleven months late. The parts worth automating are cheap.

Keep a matrix of roles against sensitive routes and generate a test per cell asserting the expected outcome, including the negative cases. Run your OpenAPI specification against the live service in continuous integration so undocumented routes and drifted response shapes fail the build. Add a dependency scan and a secret scan on every commit, with the secret scan covering the whole history the first time, because a key removed in a later commit is still in the objects. Run a lightweight dynamic scan against a seeded environment nightly, accepting that it will find only the obvious things.

None of this replaces a human review of the business logic, which is where manual testing earns its fee, and the point of automating the mechanical checks is that the manual time is spent on the hard part rather than on findings a script could have caught.

When a key leaks, which it will

Decide the runbook while nothing is happening. A customer will paste an API key into a public repository, or a contractor's laptop will be stolen, or a log aggregator will turn out to have been capturing an Authorization header.

The runbook has five moves: revoke the credential, which requires that revocation is a single operation and not a code change; determine what that credential could reach, which requires scopes to have been narrow enough for the answer to be useful; determine what it actually did, which requires request logs with the key identifier attached and retained long enough to matter; notify the customer, which requires knowing which customer the key belonged to; and assess whether personal information was accessed, which is the question that decides whether you have a notification obligation rather than an inconvenience.

Every one of those depends on design decisions made months earlier. Logging the key identifier rather than the key, scoping keys per integration rather than per account, and keeping access logs for a period that matches your obligations are the three that most often turn a two-week investigation into a two-hour one.

What to skip, at least for now

Some of the standard advice is genuinely premature for a small team, and buying it early wastes money that should have gone somewhere else.

A bug bounty program. Running one before you can triage reliably produces a queue of low-severity reports, arguments about severity, and researchers who are correctly annoyed at being ignored. Start with a security.txt file and a plain vulnerability disclosure page committing to a response time you can meet. Add bounties when someone owns triage as part of their job.

An expensive API gateway bought for security. Gateways are excellent at routing, quota enforcement and observability. They are poor at authorization, because authorization depends on your data model and the gateway does not have it. A gateway in front of an application with broken object level authorization is a well-monitored path to the same bug.

Certificate pinning on a young mobile app. Pinning protects against a narrow threat and creates a real risk of bricking every installed client during a certificate change. If you have not yet rehearsed a rotation, the pin is more likely to cause your outage than to prevent your breach.

A payments-grade control set when you are not in scope. If a payment processor handles cards and your systems never touch primary account numbers, your obligations are far narrower than the checklists suggest, and the scoping question is worth answering before the remediation plan. We would rather tell you that your scope is smaller than you feared than sell you the larger version.

If you are unsure which of these applies to your architecture, describe the API and the buyer asking about it. Frequently the honest answer is that two specific fixes plus a test in your pipeline settles the question, and no engagement is needed at all.

Want this handled? Tell us what your buyer is asking for and we will tell you what the work involves, what it costs, and what you can do yourself.

Talk to usOr talk about a retainer

Before you go

Want the rest of this by email?

If this was useful, I send a few short notes on security posture. Unsubscribe in one click, and replies reach me directly.

From Jacob Masse, principal of traztech. No spam, unsubscribe in one click.

Want a second opinion on where you stand?

We run SOC 2, ISO 27001 and the rest of the compliance stack for startups and SMEs, and the security testing that sits behind it. The first call is free, and we will tell you if you are not ready to start yet.

Book a free call

Track record

Who is actually doing the work

5
Published CVEs, including a CVSS 9.1
76
Controls taken from nothing to a passed SOC 2 Type II
Zero
Exceptions on that Type II report
20+
Penetration testing engagements delivered

Published vulnerability research

Five published CVEs. CVE-2024-45163 (CVSS 9.1) is a flaw in the Mirai botnet itself, which gave defenders a way to shut down attacker infrastructure. CVE-2026-42626 takes HP ENVY 5000 printers offline from any unauthenticated device on the same network.

A SOC 2 Type II built from nothing

At Humera, a venture-backed US security company, Jacob built the compliance programme in-house from nothing: no report, no policies, no documented controls. It ended in a Type II attestation across 76 controls with zero exceptions, on a team of 15.