Use Resend with a scoped key and a request cap
Point the Resend SDK at https://relay.keylayer.dev and use your Keylayer key as the API key; Keylayer forwards each call to api.resend.com with your real key. Resend keys take request caps, a per-minute rate and a concurrency limit but no spend cap, the API key, domain and webhook routes are blocked, and a request can run for up to 120 seconds.
Set up
Add a Resend API key with Sending access, limited to the one domain you send from, as a credential. Then mint a key as in the quickstart and put it in an environment variable:
export KEYLAYER_KEY=kl_k_YOUR_KEYimport os
import resend
resend.api_key = os.environ["KEYLAYER_KEY"]
resend.api_url = "https://relay.keylayer.dev"
email = resend.Emails.send({
"from": "Acme <onboarding@resend.dev>",
"to": ["delivered@resend.dev"],
"subject": "Hello from Keylayer",
"html": "<p>Sent through Keylayer.</p>",
})
print(email["id"])import { Resend } from "resend";
const resend = new Resend(process.env.KEYLAYER_KEY, {
baseUrl: "https://relay.keylayer.dev",
});
const { data, error, headers } = await resend.emails.send({
from: "Acme <onboarding@resend.dev>",
to: ["delivered@resend.dev"],
subject: "Hello from Keylayer",
html: "<p>Sent through Keylayer.</p>",
});
if (error) {
const code = headers?.["keylayer-error"];
console.log(code ? `Keylayer refused the request: ${code}` : error.message);
} else {
console.log(data.id);
}curl https://relay.keylayer.dev/emails \
-H "Authorization: Bearer $KEYLAYER_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <onboarding@resend.dev>",
"to": ["delivered@resend.dev"],
"subject": "Hello from Keylayer",
"html": "<p>Sent through Keylayer.</p>"
}'Instead of setting the URL in code, you can set RESEND_API_URL for Python or RESEND_BASE_URL for Node.js to https://relay.keylayer.dev.
Request caps on Resend
Resend calls have no price, so a Resend key can't have a spend cap and minting one with a spend cap is refused. Limit it with Requests per hour, day, week or month, Requests a minute and Requests at once; see request caps and rate.
A request cap counts API calls, not emails. One call to /emails sends one email to up to 50 recipients, one call to /emails/batch sends up to 100 emails, and sending a broadcast mails a whole audience. To make the cap count single sends, set the key's Only methods to POST and its Only paths to /emails; see methods and paths.
Resend's own rate limit, 10 requests per second per team by default, still applies on top of the key's limits.
Blocked routes
Keylayer refuses these routes with 403 path_not_allowed, in any letter case, so a key can't create Resend keys or change where and how you send:
| Route | What it does |
|---|---|
| /api-keys/** | Creates, lists and deletes API keys |
| /domains/** | Adds, verifies, changes and deletes sending domains |
| /webhooks/** | Creates, changes and deletes webhooks |
Escape hatches lift the block for one key; see admin routes and escape hatches.
Errors
A Keylayer refusal has a keylayer-error header and a Resend-shaped body:
{"statusCode": 403, "message": "Keylayer path_not_allowed: this key does not allow this path.", "name": "path_not_allowed"}Resend's own refusals, such as 429 daily_quota_exceeded and monthly_quota_exceeded, come without the keylayer-error header.
The Node.js SDK doesn't throw on an HTTP error. It returns data, error and headers, and the example above reads the code from headers. The Python SDK raises ResendError, which keeps the headers:
import os
import resend
from resend.exceptions import ResendError
resend.api_key = os.environ["KEYLAYER_KEY"]
resend.api_url = "https://relay.keylayer.dev"
try:
resend.Emails.send({
"from": "Acme <onboarding@resend.dev>",
"to": ["delivered@resend.dev"],
"subject": "Hello from Keylayer",
"html": "<p>Sent through Keylayer.</p>",
})
except ResendError as err:
code = err.headers.get("keylayer-error")
if code is None:
raise
print(f"Keylayer refused the request: {err.code} {code}")The error reference lists every code.
What doesn't work
- Managing API keys, domains and webhooks, as above.
- Spend caps. Use request caps, and Only paths if you want to count emails.
Next steps
- Make your first request: add a credential and mint a key.
- Set limits on a key: every limit a key can have and how request caps count.
- Handle Keylayer errors: every code Keylayer returns and what to do about it.
Updated