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.

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