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.
| Path | HTTP status | Who it's for |
|---|---|---|
| WordPress plugin | 301 | WordPress sites. No code, installed from the admin panel. |
| PrestaShop module | 301 | PrestaShop 8 and 9 stores. No code, installed from a zip. |
| Laravel package | 301 | Laravel 12 and 13 applications. Installed with Composer, no integration code to write. |
| Node.js package | 301 | Express, Fastify, Hono and Next.js applications. Installed with npm, no integration code to write. |
| Server-side | 301 | Anyone writing their own code. We hand you a ready specification. |
| Browser snippet | stays 404 | No server access, or you just want to try it in five minutes. |
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
- In your WordPress admin, go to Plugins → Add New and search for no404.
- Click Install Now and activate it.
- Paste the API key from your dashboard into the plugin settings.
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
- Download the zip that matches your store: PrestaShop 9 → 2.x, PrestaShop 8 → 1.x.
- Upload it, without unpacking, under Modules → Module Manager → Upload a module.
- Open no404 → Configure, paste the API key from your dashboard and test the connection.
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/laravelAdd the API key from your dashboard to .env:
NO404_API_KEY=YOUR_API_KEYCheck the connection, then confirm it with Integration → Test in your dashboard:
php artisan no404:testcomposer 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/nodeAdd 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.comIn 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-102npm 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.
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>;
}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.
Parameters
| apiKey | path | Your site's API key. Required unless you send the Authorization header. This path also requires an Origin header. |
| path | query / body | The path that returned 404, e.g. /missing-page (required). |
| ref | query / body | Where the visitor came from (document.referrer). Optional; empty = direct visit. |
| ad | query / body | Ad 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). |
| Authorization | header | Bearer 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
| found | boolean | Whether a confident match was found (can be false on a fallback redirect). |
| redirect | string | null | The full URL to redirect to. If set, send the visitor there (fallbacks included). |
| score | number | Match score between 0 and 1 (1 = a manual redirect). |
| source | enum | REDIRECT (manual) · CATALOG (automatic) · FALLBACK (below threshold: parent category or fallback address) · NONE (no match). |
| redirectStatus | 301 | 302 | null | The 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
| 200 | Success (found may be true or false). |
| 403 | Origin missing or not allowed, or tracking is paused. |
| 404 | Invalid API key. |
| 422 | Invalid or missing 'path'. |
| 429 | Rate 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.
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.