API
Three endpoints. JSON in, JSON out. No auth on shorten and revoke; the revocation key is the authentication.
POST /shorten
Submit a URL, receive a short URL and a one-time revocation key.
curl -X POST https://api.fwd2.app/shorten \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com/an-honest-path"}'
Response:
{
"short_url": "https://fwd2.app/K7m-9Qw",
"code": "K7m-9Qw",
"revocation_key": "fwd2_wH8h9Bk7vGqL2pYz...",
"warning": "Save the revocation key now. It is shown once."
}
DELETE /links/<code>
Delete a link given its code and revocation key. The key goes in the body or the X-Revocation-Key header.
curl -X DELETE https://api.fwd2.app/links/K7m-9Qw \
-H 'Content-Type: application/json' \
-d '{"revocation_key":"fwd2_wH8h9Bk7vGqL2pYz..."}'
On success: 204 No Content. On failure (unknown code or wrong key): 403 with the same message either way.
POST /revoke
Equivalent to DELETE for callers that cannot send DELETE.
curl -X POST https://api.fwd2.app/revoke \
-H 'Content-Type: application/json' \
-d '{"code":"K7m-9Qw","revocation_key":"fwd2_wH8h9Bk7vGqL2pYz..."}'
GET /<code>
302 to the target URL. 404 if the code is not stored. Available on both api.fwd2.app and fwd2.app; the public short URL uses fwd2.app.
GET /health, /health/live, /health/ready
/health and /health/live are cheap liveness checks: 200 means the process is up.
/health/ready probes the database and the rate-limit store; 200 means the service can take traffic, 503 means it cannot yet.
All three are no-cache, no-store.
Errors
Errors return {"error":"<code>","message":"<human>"} with a meaningful HTTP status.
Unknown code and wrong key both return 403 revoke_failed so a guesser cannot enumerate codes.
415 unsupported_media_type if a body is sent without Content-Type: application/json.
413 payload_too_large if the body is over the configured cap.
Rate limits
POST /shorten and the revoke endpoints are rate limited per client. When the limit trips the response is
429 rate_limited with a Retry-After header in seconds. Revoke is also throttled per-code so a
brute-force grinder cannot hammer one short link from many IPs. The defaults are tuned for humans using the site; scripted use
within the limits is fine.
Target URL safety
fwd2 refuses targets that point back at fwd2 itself, hosts that resolve to private, loopback, or link-local IPs, and any
URL that carries credentials in the userinfo. Operators can extend the rejection set through FWD2_DENYLIST_HOSTS
and FWD2_DENYLIST_SUFFIXES. These rules cut off the easy misuse paths; they do not promise that every linked
site is safe.
Canary links
Add canary: true and a https:// webhook_url to POST /shorten and fwd2 will POST a signed
JSON message to that URL each time someone follows the link. The visitor sees a normal redirect.
See the canary page for the why.
curl -sX POST https://api.fwd2.app/shorten \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com/leaked-internal.pdf",
"canary":true,
"webhook_url":"https://your-server.example/canary"}'
The fire delivery carries three headers and a five-field body.
The values below are the golden-vector payload and signature from docs/WEBHOOKS.md.
POST /canary HTTP/1.1
Host: your-server.example
Content-Type: application/json
X-Fwd2-Signature: 2b452eaa29d147b4a7c34596497ad3534a49664de21c81429977f8324e5904e5
X-Fwd2-Key-Id: k1
X-Fwd2-Timestamp: 1781041044
{"event":"canary_hit","code":"A2c-9Xz","fired_at":"2026-06-15T22:17:24+00:00",
"timestamp":1781041044,"nonce":"AAECAwQFBgcICQoLDA0ODw"}
Verify a webhook
Verify the signature with HMAC-SHA256 over the canonical body, then check the timestamp is within five minutes of now,
then check the nonce has not been seen recently. The byte-exact canonical encoding, a Python verifier, and a golden
vector live in the project's docs/WEBHOOKS.md. If your webhook ever misses a delivery, the same revocation key can pull the fire status:
curl -sX POST https://api.fwd2.app/canary/status \
-H 'Content-Type: application/json' \
-d '{"code":"A2c-9Xz","revocation_key":"fwd2_..."}'
A wrong key, a non-canary code, and an unknown code all return the same 404. That is on purpose. See
the canary page for the model and the limits.
Worked example
Shorten a URL, then delete it. The plaintext key only ever appears in the shorten response.
# 1. shorten
$ curl -sX POST https://api.fwd2.app/shorten \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com/draft"}'
{"short_url":"https://fwd2.app/K7m-9Qw","code":"K7m-9Qw",
"revocation_key":"fwd2_example_revocation_key...","warning":"..."}
# 2. revoke with the key
$ curl -sX DELETE https://api.fwd2.app/links/K7m-9Qw \
-H 'Content-Type: application/json' \
-d '{"revocation_key":"fwd2_example_revocation_key..."}' \
-w '%{http_code}\n'
204
# 3. the redirect now 404s
$ curl -sI https://fwd2.app/K7m-9Qw | head -1
HTTP/2 404