Developer docs

Integration

When a visitor hits a 404 you ask no404, get the best matching URL back and send them there. There are three ways to do it — and the difference is not what the visitor sees, it's the HTTP status the search engine sees.

Which path should I pick?

All three get the visitor to the right page. The difference is the HTTP status your server returns, and that difference decides the SEO outcome: an address that returns 301 carries its accumulated value to the new one, an address that stays 404 does not.

PathHTTP statusWho it's for
WordPress plugin301WordPress sites. No code, installed from the admin panel.
PrestaShop module301PrestaShop 8 and 9 stores. No code, installed from a zip.
Laravel package301Laravel 12 and 13 applications. Installed with Composer, no integration code to write.
Node.js package301Express, Fastify, Hono and Next.js applications. Installed with npm, no integration code to write.
Server-side301Anyone writing their own code. We hand you a ready specification.
Browser snippetstays 404No server access, or you just want to try it in five minutes.
The snippet is the easiest path, but the one thing it cannot do is change the HTTP status: the page moves for the visitor and stays dead for the search engine. If you have server access, pick one of the paths that return a 301.

WordPress pluginPublished on WordPress.org

Catches the 404 on the server, asks no404 and returns a real 301. It doesn't touch your theme files and requires no code.

  • WordPress 6.0+ · PHP 7.4+
  • Caches results locally — protecting your quota and the rate limit.
  • If no404 doesn't answer, the page falls back to its normal 404 (fail-open); the plugin never keeps your site waiting.

Installation

  1. In your WordPress admin, go to Plugins → Add New and search for no404.
  2. Click Install Now and activate it.
  3. Paste the API key from your dashboard into the plugin settings.
The plugin is published in the official WordPress directory and is open source (source code). Install it from the directory: the .zip can be uploaded by hand, but that install gets no automatic updates — when a security patch ships, your site stays on the old version.

PrestaShop module

Catches the 404 on the server — deleted products and categories included —, asks no404 and returns a real 301. No overrides, and it doesn't touch your theme files.

  • PrestaShop 9.0+ → the 2.x line (PHP 8.1+) · PrestaShop 8.0–8.2 → the 1.x line (PHP 7.2+) · the cURL extension and friendly (SEO) URLs are required
  • Caches results locally per shop; in multistore, each shop can carry its own API key.
  • If no404 doesn't answer, your store's own 404 page is shown; a 1.5 s timeout and a circuit breaker mean your store never waits.

Installation

  1. Download the zip that matches your store: PrestaShop 9 → 2.x, PrestaShop 8 → 1.x.
  2. Upload it, without unpacking, under Modules → Module Manager → Upload a module.
  3. Open no404 → Configure, paste the API key from your dashboard and test the connection.
The module is open source (source code). It isn't on the PrestaShop Addons marketplace yet, and a manually uploaded module gets no automatic updates — when a new version ships, upload the zip again; your settings are kept. When you upgrade your store from PrestaShop 8 to 9, upload the 2.x zip: it installs as a module upgrade.

Laravel package

When your application answers 404 (an unmatched route, abort(404), findOrFail, Route::fallback()), it asks no404 and returns a real 301 or 302. The middleware registers itself; you don't touch your exception handler.

  • Laravel 12 · 13 · PHP 8.2+ (8.3+ for Laravel 13) · a cache store that survives between requests (redis, database, file…)
  • Caches answers per path, misses included. Static files, /storage, /livewire, /api and admin paths are never sent.
  • If no404 is slow or unreachable, the visitor sees your application's own 404 page within 1.5 s at most, and lookups pause for a minute.

Installation

Install the package:

composer require no404/laravel

Add the API key from your dashboard to .env:

NO404_API_KEY=YOUR_API_KEY

Check the connection, then confirm it with Integration → Test in your dashboard:

php artisan no404:test
The package is open source (source code) and distributed through Packagist; updates arrive with composer update. APP_URL must be your site's real address, because redirect targets are only accepted on that host. Laravel 11 and older are not supported; use the server-side integration there.

Node.js package

When your application is about to answer 404, it asks no404 and returns a real 301 or 302. On the Next.js App Router the page layer can only issue 308 and 307, so 301 becomes 308 and 302 becomes 307; Google treats them the same.

  • Node.js 22.12+ · Express · Fastify · Hono · Next.js · no runtime dependencies
  • Answers are cached per path, misses included; simultaneous requests for one dead link share a single lookup. On serverless platforms (Vercel, Lambda, Workers) use a shared cache: Redis or Cloudflare KV. Clicks from an ad or an AI assistant skip the cache so no404 counts each one; if no404 can't be reached, the cached answer is still used.
  • If no404 is slow or unreachable, the visitor sees your application's own 404 page within 1.5 seconds at most, and lookups pause for a minute.

Installation

Install the package:

npm install @no404/node

Add the API key from your dashboard and your site's address to the server's environment (NOT with a NEXT_PUBLIC_ prefix — the key must never reach the browser):

NO404_API_KEY=YOUR_API_KEY
NO404_SITE_URL=https://your-store.com

In Express, register it after all your routes and before your own 404 handler:

import express from "express";
import { createNo404 } from "@no404/node";
import { no404Express } from "@no404/node/express";

const app = express();
// … your routes …
app.use(no404Express(createNo404()));

On the Next.js App Router two small files are enough: proxy.ts (middleware.ts on Next 15) carries the original URL and app/[...rest]/page.tsx, which catches unmatched URLs, asks. Do NOT put the lookup in app/not-found.tsx: Next embeds that file in every page's response, so the lookup also runs on pages that return 200 and eats your allowance for nothing. Already have a proxy.ts? Pass withNo404Headers(request) as the request headers of the response you return. On deleted-product pages use notFoundOrRedirect() instead of notFound():

// proxy.ts
import { NextResponse, type NextRequest } from "next/server";
import { withNo404Headers } from "@no404/node/next/proxy";

export function proxy(request: NextRequest) {
  return NextResponse.next({ request: { headers: withNo404Headers(request) } });
}

export const config = { matcher: ["/((?!_next/|api/|favicon.ico).*)"] };

// app/[...rest]/page.tsx
import { createNo404 } from "@no404/node";
import { resolveNotFound } from "@no404/node/next";
import { notFound } from "next/navigation";

const no404 = createNo404();

export default async function UnmatchedPath() {
  await resolveNotFound(no404);
  notFound();
}

Check the environment and try the key with a single lookup, then confirm it with Integration → Test in your dashboard:

npx @no404/node doctor
npx @no404/node resolve /14g-gold-ring-102
The package is open source (source code) and distributed through npm; updates arrive with npm update. Fastify, Hono and Pages Router examples are in the package README. A static export (output: export) runs no server, so it isn't supported.

Server-side integration

If you write your own code, you can ask no404 wherever you produce a 404 and return a real 301. You don't have to design this from scratch: the dashboard generates a ready integration specification for your site.

The specification is not a snippet, it's an engineering document: timeouts, mandatory caching, a blocklist, the 301/302 rule, plus loop and open-redirect protection. Hand the file to your developer, or straight to an AI coding agent.

Download it from the dashboard → site detail → Integration tab. The specification is generated from your site's address and settings; we don't keep a copy here — a document kept in two places eventually becomes two documents.

Caching is not optional

In a server-to-server call all of your traffic counts as coming from a single IP, and the rate limit (120 requests per minute) applies to that IP. If you don't cache responses locally, a busy site will hit the limit and redirects will stop silently. The specification includes this as a mandatory item.

The minimal version (Node.js)

const res = await fetch(
  "https://www.no404.tr/api/v1/resolve?path=" + encodeURIComponent(path),
  { headers: { Authorization: "Bearer YOUR_API_KEY" } }
);
const data = await res.json();

// redirectStatus is 301 or 302, following the 301 threshold you chose in the dashboard.
if (data.redirect) {
  const status = data.redirectStatus === 301 ? 301 : 302;
  // Express: res.redirect(status, data.redirect);
}

Browser snippet

This path leaves the HTTP status at 404

The redirect happens in JavaScript, so the server still returned a 404. The visitor reaches the right page, but the search engine keeps treating the old address as dead and no link value is passed on. If you can reach your server, pick one of the two paths above.

The key is public: the key in the snippet shows up in your page source. Every request made with your key, including bots and malicious traffic, counts toward your monthly allowance (Terms of Service §2). With a server-side integration the key stays on your server.

Add the code below to your site's 404 page. Replace YOUR_API_KEY with the API key from your dashboard.

<!-- no404: add this to your 404 page -->
<script>
(function () {
  var p = location.pathname + location.search;
  var ref = document.referrer;
  fetch("https://www.no404.tr/api/v1/resolve/YOUR_API_KEY?path=" + encodeURIComponent(p) + "&ref=" + encodeURIComponent(ref))
    .then(function (r) { return r.json(); })
    .then(function (d) { if (d && d.redirect) location.replace(d.redirect); })
    .catch(function () {});
})();
</script>

Next.js (App Router)

Inside app/not-found.tsx:

"use client";
import { useEffect } from "react";

export default function NotFound() {
  useEffect(() => {
    const p = location.pathname + location.search;
    fetch(`https://www.no404.tr/api/v1/resolve/YOUR_API_KEY?path=${encodeURIComponent(p)}&ref=${encodeURIComponent(document.referrer)}`)
      .then((r) => r.json())
      .then((d) => { if (d.redirect) location.replace(d.redirect); })
      .catch(() => {});
  }, []);
  return <p>Page not found, redirecting you…</p>;
}
You manage your API key and allowed origins from dashboard → site detail.

Endpoint reference

A single endpoint used by all three paths; it accepts both GET and POST.

The key can travel two ways, and the rules differ. In the address (/api/v1/resolve/KEY) it is the browser path: a page is making the request, so an Origin header is required and it has to be one of your allowed origins. In an Authorization: Bearer header (/api/v1/resolve) the request counts as coming from your server, and no Origin is expected. If you are calling from a server, use the header: a key in the address ends up in server and proxy logs, a header does not.

GETPOST/api/v1/resolve/{apiKey}

Parameters

apiKeypathYour site's API key. Required unless you send the Authorization header. This path also requires an Origin header.
pathquery / bodyThe path that returned 404, e.g. /missing-page (required).
refquery / bodyWhere the visitor came from (document.referrer). Optional; empty = direct visit.
adquery / bodyAd category: google, microsoft, meta or other. Optional; a server-side integration that doesn't send the query string works out the category itself and reports it. Never send the raw click ID (gclid).
AuthorizationheaderBearer KEY. With the header, the address is /api/v1/resolve, the key is not part of it, and no Origin is expected.

GET example

curl "https://www.no404.tr/api/v1/resolve/YOUR_API_KEY?path=/14g-gold-ring-102" \
  -H "Origin: https://yoursite.com"

POST example

curl -X POST "https://www.no404.tr/api/v1/resolve/YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Origin: https://yoursite.com" \
  -d '{"path":"/14g-gold-ring-102"}'

With a header (recommended server-side)

curl "https://www.no404.tr/api/v1/resolve?path=/14g-gold-ring-102" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response & status codes

A successful response returns JSON:

{
  "success": true,
  "found": true,
  "redirect": "https://yoursite.com/14g-gold-ring",
  "score": 0.92,
  "source": "CATALOG",
  "redirectStatus": 301
}

Response fields

foundbooleanWhether a confident match was found (can be false on a fallback redirect).
redirectstring | nullThe full URL to redirect to. If set, send the visitor there (fallbacks included).
scorenumberMatch score between 0 and 1 (1 = a manual redirect).
sourceenumREDIRECT (manual) · CATALOG (automatic) · FALLBACK (below threshold: parent category or fallback address) · NONE (no match).
redirectStatus301 | 302 | nullThe HTTP status to return server-side, computed from the 301 threshold the site owner picked in the dashboard: manual redirects and catalog matches above the threshold get 301, everything else 302; null when there is no redirect. Don't hard-code your own threshold — use this field.

Status codes

200Success (found may be true or false).
403Origin missing or not allowed, or tracking is paused.
404Invalid API key.
422Invalid or missing 'path'.
429Rate limit exceeded (too many requests).

Origin lock & security

Your API key is protected by an origin lock. When the key travels in the address, requests are only accepted from the allowed origins you define in the dashboard, and a request that carries no Origin header at all is rejected too. Even if your key leaks, a request from another domain gets a 403 and no CORS response.

  • When you add your site, your domain is added as an allowed origin automatically (with and without www).
  • You can add further domains from the dashboard.
  • If a key leaks, regenerate it from the dashboard; the old key becomes invalid immediately.
The lock constrains browser requests. A server-to-server call sends no Origin header, so on the Authorization: Bearer path the lock does not engage and protection comes down to the secrecy of the key and the rate limit. Keep the key on your server and never embed it in the client.

Best practices

  • Prefer the server side: it's the only path that produces a 301; the snippet carries no SEO value.
  • Cache the responses: in a server-side integration this is not optional — all traffic counts as one IP.
  • Connect your sitemap: match quality depends on how current your catalog is.
  • Review the threshold: if you don't want overly aggressive redirects, raise it (0.3 recommended).
  • Define manual redirects for critical paths; they override automatic matching.
  • Rate limit: 120 requests per minute per key + IP.

Ready?

Add your site, grab your key, and be live within minutes.

Sign up free