Outbound HTTP Requests
FRL v2 can contact approved outside services from rule code. Use outbound HTTP for tightly scoped workflows such as sending a notification, verifying a callback, creating a hosted upload token, or syncing a small event to a trusted service.
version 2;
$private.notificationUrl as "Notification service URL";
endpoint.post contactForm("/contact") as "Contact Form" {
must($request.body.email, "Email is required", 422);
$context.response = http.post($private.notificationUrl, {
json: {
email: $request.body.email,
message: $request.body.message
},
timeout: 10000
});
must($context.response.ok, "The message could not be sent.", 502);
return { ok: true };
}
Helpers
| Helper | Purpose |
|---|---|
http.get(url, options?) | Send a GET request. |
http.post(url, options?) | Send a POST request. |
http.put(url, options?) | Send a PUT request. |
http.patch(url, options?) | Send a PATCH request. |
http.delete(url, options?) | Send a DELETE request. |
http.request(method, url, options?) | Send a request with a method chosen by the rule. |
http.request({ method, url, ...options }) | Send a request from an options object. |
Common options are headers, json, body, params, and timeout. When json is provided, Frontbacked sends it as JSON and sets a JSON content type if one was not already set.
$context.result = http.request({
method: "POST",
url: "https://api.example.com/messages",
headers: {
authorization: `Bearer ${$secret.NOTIFICATION_TOKEN}`
},
json: {
subject: "New form entry",
email: $request.body.email
},
timeout: 8000
});
Response Shape
HTTP helpers return a small response object:
{
ok: true,
status: 200,
statusText: "OK",
headers: { "content-type": "application/json" },
data: { accepted: true },
text: "{\"accepted\":true}"
}
data is parsed JSON when the response is JSON or looks like JSON. Otherwise, data and text are strings.
Approved Destinations
External destinations must be approved before production FRL can contact them.
When a theme version uses a literal host such as https://api.example.com/messages, Frontbacked checks that destination with the version. If the destination is not approved yet, the version waits for review before other users can install it.
Hosts coming from $private or $secret are treated as admin-controlled dynamic hosts:
$private.crmWebhookUrl as "CRM webhook URL";
post leads {
after create {
$context.sync = http.post($private.crmWebhookUrl, {
json: { leadId: $post.pending.id }
});
}
}
Dynamic hosts are still checked when the rule runs. If the resolved host is not approved, Frontbacked blocks that request. If a dynamic host is rejected, only that destination for that site/request is blocked; the whole theme version is not disabled because the value came from site data.
Site admins can also pause an individual request from their admin area. They can choose to make the request return a service error, or simulate a successful response while they investigate. Design integrations so a disabled request fails clearly and safely, and use simulated success only for responses the site admin intentionally controls.
Loop Protection
FRL v2 blocks request loops. A rule cannot call one of the same theme's endpoints through a relative path or /fb-endpoints/...; Frontbacked also blocks same-site full URLs and proxy loops.
endpoint.post submitLead("/lead") {
// This is blocked in FRL v2.
$context.loop = http.post("/fb-endpoints/lead", {
json: $request.body
});
return { ok: true };
}
Move shared logic into an FRL function instead of calling your own endpoint through HTTP.
Dynamic URL Safety
FRL v2 does not allow outbound request targets to be built from actor, request, post, payment, or other visitor-controlled data.
endpoint.post submitLead("/lead") {
// Blocked: a visitor could choose where the server sends the request.
$context.result = http.post($request.body.webhookUrl, {
json: $request.body
});
return { ok: true };
}
Use literal URLs for fixed services, or $private/$secret for admin-configured services. This keeps site owners in control of where their data can be sent.
Good Practice
Keep outbound requests narrow and easy to explain. Send only the fields the service needs, put credentials in $secret, put editable destination URLs in $private, check $context.response.ok with must(...), and use endpoint dedupe when a repeated request could duplicate work.