On this page
Documentation
Proxify reference
Proxify 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
- Create an account. You get a public API key straight away.
-
In the dashboard, open API keys and add the origin your page is served from, such as
https://app.example.com. While developing onlocalhost, switch the key to test instead. - Send the request through Proxify with your key:
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.
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 }),
});
| Parameter | Purpose |
|---|---|
url | The target URL. Required. |
key | Your API key, for requests that cannot send headers. See Sending the key. |
timeout | A shorter deadline in milliseconds. It can only shorten the server's limit. The Timeout header does the same. |
format, extract, select, callback | Response transforms. |
exp, sig, sn, m | Signed URLs. |
cassette | The replay cassette to use. |
Headers
Request headers are forwarded to the upstream, with these exceptions:
-
Proxify's own credentials are never forwarded:
X-API-Key,X-Proxify-Key,AuthorizationandCookie. A target you proxy to never sees your Proxify key or session. -
To authenticate to the upstream, send
X-Proxify-Upstream-Authorization. It arrives upstream asAuthorization. Put a vault reference in it rather than a real credential. -
Headers browsers forbid script to set, such as
User-AgentorReferer, can be set withX-Proxify-Set-<Name>.Host,Content-Lengthand connection headers cannot. - Hop-by-hop headers (
Connection,Transfer-Encodingand the like) apply to one connection only and are dropped.
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; Proxify 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
Proxify'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
| Limit | Value |
|---|---|
| Request body | 10 MiB. Larger bodies are refused with 413. |
| Response body | 50 MiB. Larger responses fail with 502. |
| Upstream deadline | 60 seconds, or shorter with timeout. Exceeding it returns 504. |
| Redirects | Followed, up to 5. Each hop is checked against the same address rules. |
| Destinations | Public addresses only. Private, loopback and link-local addresses are refused. |
API keys
Sending the key
Send the key in the X-API-Key header. Proxify 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.
<!-- 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. Proxify 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
Origin, but curl can. If a key copied from your bundle must not be usable from a
terminal, require signed URLs for it.
Without a key
Requests without a key are allowed for trying Proxify 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:
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%)
| Header | Meaning |
|---|---|
X-Proxify-RateLimit-Limit | Requests allowed in the current window. |
X-Proxify-RateLimit-Remaining | Requests left in it. |
X-Proxify-RateLimit-Reset | When it resets, as a Unix timestamp in seconds. |
X-Quota-Warning | Present once a window passes the warning threshold, naming the window and how much is used. |
Retry-After | On 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 Proxify itself, before or instead of the upstream call, are JSON:
HTTP/2 429
Retry-After: 1830
Content-Type: application/json
{"success":false,"error":"Quota exceeded for the hourly window","error_code":"QUOTA_EXCEEDED"}
| Status | error_code | Meaning |
|---|---|---|
| 401 | INVALID_API_KEY | The key does not exist. In a browser this shows as a CORS error, because an unknown key gets no CORS headers. |
| 403 | API_KEY_INACTIVE, API_KEY_EXPIRED | The key was rotated, disabled or has expired. |
| 403 | IP_NOT_ALLOWED | The key has an IP allowlist and this address is not on it. |
| 403 | SIGNATURE_REQUIRED, INVALID_SIGNATURE | The key requires signed URLs, and the request was unsigned, expired or signed incorrectly. |
| 402 | TRIAL_EXPIRED, SUBSCRIPTION_EXPIRED, SUBSCRIPTION_CANCELED | The account needs an active plan. |
| 429 | QUOTA_EXCEEDED | A quota window is used up. The message names it; see Retry-After. |
| 429 | RPS_LIMIT_EXCEEDED, RPM_LIMIT_EXCEEDED | Too many requests in the last second or minute. |
| 429 | ANONYMOUS_QUOTA_EXCEEDED | The keyless allowance is used up for today. |
| 503 | QUOTA_UNAVAILABLE, RATE_LIMITER_UNAVAILABLE | Proxify could not check your quota. Retry after Retry-After. |
Problems with the request or the upstream connection return a short plain-text body:
| Status | Cause |
|---|---|
| 400 | Missing or invalid url, or a scheme other than http and https. |
| 403 | The destination host is blocked, or a secret may not be sent to it. |
| 413 | The request body is over the limit. |
| 502 | The upstream could not be reached, its address is not permitted, or its response was too large. |
| 504 | The upstream did not answer within the deadline. |
Secrets vault
Keep upstream credentials out of your bundle. Store them in Proxify 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}}:
// 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.comfor 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 Proxify 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.
- On the key's page in the dashboard, create a signing secret. It is shown once.
- Turn on Require signature.
- Sign URLs on your server and hand them to the browser.
// 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}`;
}
// 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);
| Parameter | Meaning |
|---|---|
exp | Expiry as a Unix timestamp in seconds. At most 24 hours ahead; 30 seconds of clock skew is tolerated. |
sig | HMAC-SHA256 of the canonical form, keyed with the signing secret, base64url without padding. |
m | Optional. Binds the signature to one method. Without it, only GET and HEAD are allowed. |
sn | Optional nonce, covered by the signature, for single-use schemes of your own. |
key | Your 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 Proxify to reshape the response, so the browser does not need a parser for it.
# 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
| Parameter | Effect |
|---|---|
format=json | Converts CSV, XML, RSS and Atom to JSON. The source format is taken from Content-Type. |
extract=SELECTOR | Returns 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=NAME | Wraps 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.
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" }
]
}
{
"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) ortext.errormeans the upstream could not be reached;skippedmeans a dependency failed. - When the key requires signatures, each item carries its own
exp,sigand optionalsnfor 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.
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 Proxify 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 Proxify into a fixture server for your test suite.
Set a key's replay mode on the Replay page:
| Mode | Behaviour |
|---|---|
off | Proxy normally and record nothing. |
record | Always call the upstream and store the response, replacing any earlier recording. |
replay | Serve only from recordings and never call the upstream. A miss is a 404 with X-Proxify-Replay: MISS. |
auto | Replay a recording if there is one, otherwise call the upstream and record it. |
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:
(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
Proxify 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.
// 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
targetis 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.