Skip to content
On this page

Documentation

ProxifyEdge reference

ProxifyEdge fetches a URL on your page's behalf and returns the response with the CORS headers your browser needs. Everything here is relative to https://api.proxifyedge.com.

Quick start

  1. Create an account. You get a public API key straight away.
  2. In the dashboard, open API keys and add the origin your page is served from, such as https://app.example.com. While developing on localhost, switch the key to test instead.
  3. Send the request through ProxifyEdge with your key:
fetch.js
const target = 'https://api.github.com/repos/withastro/astro';

const response = await fetch(
  `https://api.proxifyedge.com/proxy?url=${encodeURIComponent(target)}`,
  { headers: { 'X-API-Key': 'pk_your_public_key' } },
);

if (!response.ok) throw new Error(`Request failed: ${response.status}`);
const repo = await response.json();

The upstream status, headers and body come back to your script. Try it without writing code in the playground.

Making requests

The endpoint

Every request goes to https://api.proxifyedge.com/proxy with the target in the url query parameter. Encode it with encodeURIComponent so its own query string survives. The target must be an absolute http or https URL.

Use any of GET, HEAD, POST, PUT, PATCH and DELETE. The method and body you send are the ones the upstream receives.

post.js
await fetch(`https://api.proxifyedge.com/proxy?url=${encodeURIComponent('https://api.example.com/orders')}`, {
  method: 'POST',
  headers: {
    'X-API-Key': 'pk_your_public_key',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ sku: 'A-100', quantity: 2 }),
});
Query parameters
ParameterPurpose
urlThe target URL. Required.
keyYour API key, for requests that cannot send headers. See Sending the key.
timeoutA shorter deadline in milliseconds. It can only shorten the server's limit. The Timeout header does the same.
format, extract, select, callbackResponse transforms.
exp, sig, sn, mSigned URLs.
cassetteThe replay cassette to use.

Headers

Request headers are forwarded to the upstream, with these exceptions:

  • ProxifyEdge's own credentials are never forwarded: X-API-Key, X-Proxify-Key, Authorization and Cookie. A target you proxy to never sees your ProxifyEdge key or session.
  • To authenticate to the upstream, send X-Proxify-Upstream-Authorization. It arrives upstream as Authorization. Put a vault reference in it rather than a real credential.
  • Headers browsers forbid script to set, such as User-Agent or Referer, can be set with X-Proxify-Set-<Name>. Host, Content-Length and connection headers cannot.
  • Hop-by-hop headers (Connection, Transfer-Encoding and the like) apply to one connection only and are dropped.
set-header.js
await fetch(`https://api.proxifyedge.com/proxy?url=${encodeURIComponent('https://api.example.com/feed')}`, {
  headers: {
    'X-API-Key': 'pk_your_public_key',
    // Browsers will not let script set User-Agent; ProxifyEdge sets it upstream.
    'X-Proxify-Set-User-Agent': 'my-app/1.0',
  },
});

On the way back, upstream Set-Cookie and Access-Control-* headers are not relayed: cookies would be scoped to ProxifyEdge's domain, and CORS is decided by your key's policy. Script can read Content-Type, Content-Length, X-Request-ID, the rate-limit headers, Retry-After and X-Quota-Warning.

Limits

Request limits
LimitValue
Request body10 MiB. Larger bodies are refused with 413.
Response body50 MiB. Larger responses fail with 502.
Upstream deadline60 seconds, or shorter with timeout. Exceeding it returns 504.
RedirectsFollowed, up to 5. Each hop is checked against the same address rules.
DestinationsPublic addresses only. Private, loopback and link-local addresses are refused.

API keys

Sending the key

Send the key in the X-API-Key header. ProxifyEdge also accepts X-Proxify-Key, Authorization: Bearer and the key query parameter. Use the query parameter where you cannot set headers: an <img>, a <script>, a stylesheet, or a WebSocket.

index.html
<!-- An <img>, <script> or stylesheet cannot send headers: use ?key= -->
<img src="https://api.proxifyedge.com/proxy?key=pk_your_public_key&url=https%3A%2F%2Fexample.com%2Fchart.png" alt="Chart">

A request with a custom header is preflighted by the browser. ProxifyEdge answers preflight itself, without counting it against your quota.

Live and test keys

A live key only answers the origins you list in the dashboard, so it is safe to ship in a browser bundle: another site cannot read responses made with it. Origins are exact, in the form scheme://host[:port], up to 50 per key, and a wildcard is not allowed on a live key. A new key is live with no origins, so it works from curl but not from a browser until you add one.

A test key answers any origin, without credentials, for development on localhost, CodePen or preview deployments. Switch to live before you ship.

Origin locking is a browser guarantee

Without a key

Requests without a key are allowed for trying ProxifyEdge out, under a small allowance per IP address that resets daily (10 requests by default). They get a public CORS policy with no credentials. Past the allowance, the response is 429 with ANONYMOUS_QUOTA_EXCEEDED.

Quotas and rate limits

Your plan sets a monthly request quota, and may also set daily and hourly windows and a requests-per-second limit. A key can carry its own requests-per-minute limit. Every proxied response reports where you stand:

response headers
HTTP/2 200
X-Proxify-RateLimit-Limit: 10000
X-Proxify-RateLimit-Remaining: 9876
X-Proxify-RateLimit-Reset: 1793577600
X-Quota-Warning: 84.0% of the hourly quota consumed (threshold 80.0%)
Rate limit headers
HeaderMeaning
X-Proxify-RateLimit-LimitRequests allowed in the current window.
X-Proxify-RateLimit-RemainingRequests left in it.
X-Proxify-RateLimit-ResetWhen it resets, as a Unix timestamp in seconds.
X-Quota-WarningPresent once a window passes the warning threshold, naming the window and how much is used.
Retry-AfterOn a 429, the seconds to wait.

The headers carry the X-Proxify- prefix because X-RateLimit-* on a proxied response belong to the upstream API, and are passed through untouched.

Errors

An upstream error is returned as the upstream sent it. Errors from ProxifyEdge itself, before or instead of the upstream call, are JSON:

response
HTTP/2 429
Retry-After: 1830
Content-Type: application/json

{"success":false,"error":"Quota exceeded for the hourly window","error_code":"QUOTA_EXCEEDED"}
Error codes
Statuserror_codeMeaning
401INVALID_API_KEYThe key does not exist. In a browser this shows as a CORS error, because an unknown key gets no CORS headers.
403API_KEY_INACTIVE, API_KEY_EXPIREDThe key was rotated, disabled or has expired.
403IP_NOT_ALLOWEDThe key has an IP allowlist and this address is not on it.
403SIGNATURE_REQUIRED, INVALID_SIGNATUREThe key requires signed URLs, and the request was unsigned, expired or signed incorrectly.
402TRIAL_EXPIRED, SUBSCRIPTION_EXPIRED, SUBSCRIPTION_CANCELEDThe account needs an active plan.
429QUOTA_EXCEEDEDA quota window is used up. The message names it; see Retry-After.
429RPS_LIMIT_EXCEEDED, RPM_LIMIT_EXCEEDEDToo many requests in the last second or minute.
429ANONYMOUS_QUOTA_EXCEEDEDThe keyless allowance is used up for today.
503QUOTA_UNAVAILABLE, RATE_LIMITER_UNAVAILABLEProxifyEdge could not check your quota. Retry after Retry-After.

Problems with the request or the upstream connection return a short plain-text body:

Proxy errors
StatusCause
400Missing or invalid url, or a scheme other than http and https.
403The destination host is blocked, or a secret may not be sent to it.
413The request body is over the limit.
502The upstream could not be reached, its address is not permitted, or its response was too large.
504The upstream did not answer within the deadline.

Secrets vault

Keep upstream credentials out of your bundle. Store them in ProxifyEdge and reference them by name; the real value is added on the way out.

In the dashboard, open Secrets, choose the key, and add a secret with a name (a letter followed by up to 63 letters, digits or underscores) and the hosts it may be sent to. Then reference it in any request header as {{secret.NAME}}:

secrets.js
// STRIPE_KEY is stored in the dashboard, bound to api.stripe.com.
await fetch(`https://api.proxifyedge.com/proxy?url=${encodeURIComponent('https://api.stripe.com/v1/products')}`, {
  headers: {
    'X-API-Key': 'pk_your_public_key',
    // Arrives at Stripe as "Authorization: Bearer sk_live_…".
    'X-Proxify-Upstream-Authorization': 'Bearer {{secret.STRIPE_KEY}}',
  },
});
  • References are expanded in request headers only, never in the URL or body.
  • Each secret is bound to hosts: exact names such as api.stripe.com, or a wildcard such as *.example.com for subdomains. A request to any other host is refused with 403 before anything is sent, so a reference cannot be redirected to a site that would log it.
  • A reference to a secret that does not exist fails with 400 naming it, rather than sending a blank credential.
  • Secrets belong to a key, so they need a key on the request. Values are encrypted at rest and never shown again after saving.

Signed URLs

A signed URL is a short-lived capability for one target. Turn signing on for a key and ProxifyEdge refuses its unsigned requests.

A public key can be lifted from your bundle and used from a terminal, because origin locking relies on the browser. With signing, your own server, which already holds a real secret, mints a URL that names one target and expires in seconds. The browser receives a capability, not a credential.

  1. On the key's page in the dashboard, create a signing secret. It is shown once.
  2. Turn on Require signature.
  3. Sign URLs on your server and hand them to the browser.
sign.js
// On your server. SIGNING_SECRET is the key's signing secret from the dashboard.
import { createHmac } from 'node:crypto';

export function signProxyUrl({ target, key, ttlSeconds = 60, method, nonce }) {
  const params = new URLSearchParams();
  params.set('url', target);
  params.set('exp', String(Math.floor(Date.now() / 1000) + ttlSeconds));
  if (nonce) params.set('sn', nonce);
  if (method) params.set('m', method.toUpperCase());

  // Canonical form: every parameter except sig and key, names sorted, each
  // value written as <byte length>:<name><byte length>:<value>.
  const names = [...new Set(params.keys())].filter((n) => n !== 'sig' && n !== 'key').sort();
  let canonical = '';
  for (const name of names) {
    for (const value of params.getAll(name).sort()) {
      canonical += `${Buffer.byteLength(name)}:${name}${Buffer.byteLength(value)}:${value}`;
    }
  }
  params.set('sig', createHmac('sha256', process.env.SIGNING_SECRET).update(canonical).digest('base64url'));
  params.set('key', key);
  return `https://api.proxifyedge.com/proxy?${params}`;
}
client.js
// In the browser: ask your server for a URL, then use it as-is.
const { url } = await fetch('/api/signed-proxy-url?target=' + encodeURIComponent(target)).then((r) => r.json());
const response = await fetch(url);
Signing parameters
ParameterMeaning
expExpiry as a Unix timestamp in seconds. At most 24 hours ahead; 30 seconds of clock skew is tolerated.
sigHMAC-SHA256 of the canonical form, keyed with the signing secret, base64url without padding.
mOptional. Binds the signature to one method. Without it, only GET and HEAD are allowed.
snOptional nonce, covered by the signature, for single-use schemes of your own.
keyYour key. Identifies the signing secret; not part of the signature.

The signature covers every query parameter except sig and key, including url and any transform or timeout parameter, so a URL signed for one purpose cannot be changed into another. Sign exactly the parameters you will send.

Response transforms

Ask ProxifyEdge to reshape the response, so the browser does not need a parser for it.

examples
# CSV, XML, RSS or Atom converted to JSON
https://api.proxifyedge.com/proxy?url=https%3A%2F%2Fexample.com%2Fprices.csv&format=json

# Only the matching HTML elements
https://api.proxifyedge.com/proxy?url=https%3A%2F%2Fexample.com%2F&extract=article%20h2

# Only some fields of a JSON response
https://api.proxifyedge.com/proxy?url=https%3A%2F%2Fapi.github.com%2Frepos%2Fwithastro%2Fastro&select=full_name,stargazers_count%20as%20stars

# JSONP, for environments that can only load scripts
https://api.proxifyedge.com/proxy?url=https%3A%2F%2Fapi.example.com%2Fdata&callback=handleData
Transform parameters
ParameterEffect
format=jsonConverts CSV, XML, RSS and Atom to JSON. The source format is taken from Content-Type.
extract=SELECTORReturns only the HTML elements matching a CSS-like selector, including descendant chains such as article .title.
select=PATHS Projects a JSON body to comma-separated paths: name, a.b.c, items[], items[].title, items[0].title, and path as label to rename. It is not JSONPath.
callback=NAMEWraps the result as JSONP.

Very large bodies are passed through untransformed rather than refused.

Batch requests

Send up to 20 upstream requests in one round trip.

POST a JSON body to https://api.proxifyedge.com/proxy/batch with your key. Each item needs a unique id and a url, and may set method, headers, body, the transform fields format, extract and select, and depends_on to run after another item succeeds.

request
POST /proxy/batch
X-API-Key: pk_your_public_key
Content-Type: application/json

{
  "requests": [
    { "id": "user", "url": "https://api.github.com/users/octocat" },
    { "id": "repos", "url": "https://api.github.com/users/octocat/repos", "select": "name,stargazers_count" },
    { "id": "create", "url": "https://api.example.com/items", "method": "POST",
      "headers": { "Content-Type": "application/json" }, "body": "{\"name\":\"demo\"}",
      "depends_on": "user" }
  ]
}
response
{
  "results": [
    { "id": "user", "status": 200, "headers": { "content-type": "application/json; charset=utf-8" },
      "body": { "login": "octocat" }, "duration_ms": 142 },
    { "id": "repos", "status": 200, "body": [{ "name": "hello-world", "stargazers_count": 2800 }], "duration_ms": 188 },
    { "id": "create", "status": 201, "text": "created", "duration_ms": 96 }
  ],
  "summary": { "total": 3, "succeeded": 3, "failed": 0, "skipped": 0, "duration_ms": 290, "saved_round_trips": 2 }
}
  • Up to 20 items and a 1 MiB envelope. At most 6 run at once; the whole batch has 30 seconds.
  • Each item counts as one request against your quota. The envelope itself does not.
  • Results carry either body (JSON) or text. error means the upstream could not be reached; skipped means a dependency failed.
  • When the key requires signatures, each item carries its own exp, sig and optional sn for its own target.

WebSocket tunnel

Connect to wss://api.proxifyedge.com/proxy/ws with a ws or wss target in url. Browsers cannot set headers on a WebSocket, so pass the key as key.

socket.js
const target = 'wss://stream.example.com/ticker';
const socket = new WebSocket(
  `wss://api.proxifyedge.com/proxy/ws?key=pk_your_public_key&url=${encodeURIComponent(target)}`,
);
socket.addEventListener('message', (event) => console.log(event.data));

Browsers do not apply CORS to WebSockets, so ProxifyEdge checks the page's Origin against the key's policy before connecting. Messages are limited to 10 MiB, like request bodies, and a tunnel idle in both directions for 5 minutes is closed.

Record and replay

Turn ProxifyEdge into a fixture server for your test suite.

Set a key's replay mode on the Replay page:

Replay modes
ModeBehaviour
offProxy normally and record nothing.
recordAlways call the upstream and store the response, replacing any earlier recording.
replayServe only from recordings and never call the upstream. A miss is a 404 with X-Proxify-Replay: MISS.
autoReplay a recording if there is one, otherwise call the upstream and record it.
test-setup.js
await fetch(`https://api.proxifyedge.com/proxy?url=${encodeURIComponent('https://api.example.com/catalog')}`, {
  headers: {
    'X-API-Key': 'pk_your_test_key',
    // Recordings are grouped into named cassettes; "default" when omitted.
    'X-Proxify-Cassette': 'checkout-tests',
  },
});

A request is matched on its method, target (in any parameter order), body, and its Accept, Content-Type and Accept-Language headers. Served recordings carry X-Proxify-Replay: HIT. Credentials, including substituted secrets, are stripped before anything is stored. To re-record, delete the cassette.

Edge modules

Upload a WebAssembly module that rewrites requests and responses for one key, inline, without a server of your own.

Enabled by the operator

A module exports a small ABI and imports nothing. It has no filesystem, network, clock or randomness:

ABI
(import)  none
(export)  memory
(export)  alloc(size: i32) -> i32
(export)  on_request(ptr: i32, len: i32)  -> i64   ; optional
(export)  on_response(ptr: i32, len: i32) -> i64   ; optional

ProxifyEdge writes a JSON document into the module's memory through alloc and calls the hook. The hook returns (ptr << 32) | len naming JSON it wrote, or 0 for no change.

payloads
// on_request receives
{ "method": "GET", "url": "…", "target": "https://api.example.com/x", "headers": { "accept": "…" }, "body": "…" }
// and may return (all fields optional)
{ "set_headers": { "x-tenant": "acme" }, "remove_headers": ["cookie"],
  "target": "https://api.example.com/v2/x", "body": "…", "reject": { "status": 403, "body": "not allowed" } }

// on_response receives
{ "status": 200, "headers": { "content-type": "application/json" }, "body": "…", "target": "…" }
// and may return (all fields optional)
{ "status": 200, "set_headers": { "cache-control": "no-store" }, "remove_headers": ["set-cookie"], "body": "…" }
  • Modules up to 8 MiB, 32 MiB of memory, 4 MiB of JSON in or out, and a 100 ms budget per hook by default.
  • Each request gets a fresh instance, so no state is shared between requests or tenants.
  • A rewritten target is checked against the same address rules as any other.
  • By default a failing module lets the request through unchanged. Turn that off when the module enforces policy.

Ready to try it?

Send a request in the playground, or create a key in minutes.

Are you sure?