External Api Snippets
Custom Code Snippets — External APIs
Copy-paste examples for calling external HTTP APIs from a Custom Code workflow step using fetch (or the shorthand http.get/post/put/patch/delete). Every example follows the same shape — call the API, check the response, push results out via returnData.
Allowlist note: every host you call must be on the workspace's SANDBOX_HTTP_ALLOWLIST. If fetch throws "Host not allowlisted", ask your workspace admin to add the domain.
Secrets: read API keys from App Variables (getAppVariable("slack_webhook")) — never hard-code them. Code is stored in the workflow config and visible to anyone with builder access.
What you can and can't do with fetch
| Rule | Behavior |
|---|---|
| Allowed methods | GET, POST, PUT, PATCH, DELETE — anything else returns { ok: false, status: 0, error: "HTTP method not allowed" }. |
| HTTPS only in production | Local dev allows HTTP; production enforces HTTPS via x-forwarded-proto. |
| Allowlisted hosts | Hosts not on SANDBOX_HTTP_ALLOWLIST throw "Host not allowlisted". |
| Private IPs blocked | By default. Throws "Private IP blocked" or "Resolved to private IP". |
| Redirects | Not followed (maxRedirects: 0). Your script sees the 3xx response. |
| Stripped headers | host, cookie, x-api-key in the wrong place, x-forwarded-*, and hop-by-hop headers are silently removed. |
POST to a Slack incoming webhook
// Inputs: message (custom_val or record_val)
const url = getAppVariable("slack_webhook", "");
if (!url) {
returnData("ok", false);
returnData("error", "slack_webhook not configured in App Variables");
return;
}
const r = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: params["message"] || "(empty)" })
});
returnData("ok", r.ok);
returnData("status", r.status);
Slack message with formatting + record link
const url = getAppVariable("slack_webhook", "");
const link = `${getAppDetails("base_url", "")}/r/${record.id}`;
const payload = {
text: `New ticket: ${record.subject || "(no subject)"}`,
blocks: [
{
type: "section",
text: { type: "mrkdwn", text: `*New ticket from ${record.customer_name}*\n>${record.subject}` }
},
{
type: "actions",
elements: [{ type: "button", text: { type: "plain_text", text: "Open" }, url: link }]
}
]
};
const r = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
returnData("ok", r.ok);
returnData("status", r.status);
GET JSON and extract a nested field
// Inputs: city
const r = await fetch(
`https://api.weather.example/v1/current?q=${encodeURIComponent(params["city"])}`,
{ headers: { "Authorization": `Bearer ${getAppVariable("weather_api_key", "")}` } }
);
if (!r.ok) {
console.error("Weather API failed", r.status);
returnData("ok", false);
returnData("error", `status ${r.status}`);
return;
}
const data = await r.json();
returnData("ok", true);
returnData("temperature_f", Obj.get(data, "current.temp_f"));
returnData("condition", Obj.get(data, "current.condition.text"));
POST with retry + backoff
async function postWithRetry(url, body, attempts = 3) {
for (let i = 0; i < attempts; i++) {
const r = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
});
if (r.ok) return r;
await new Promise(res => setTimeout(res, 250 * (i + 1)));
}
throw new Error("exhausted retries");
}
try {
const r = await postWithRetry("https://api.example.com/events", {
type: "record.created",
id: record.id
});
returnData("ok", true);
returnData("status", r.status);
} catch (e) {
returnData("ok", false);
returnData("error", e.message);
}
Build a URL with query params
const u = new URL("https://api.example.com/search");
u.searchParams.set("q", params["query"] || "");
u.searchParams.set("limit", "25");
u.searchParams.set("user_id", record.id);
const r = await fetch(u.toString(), { method: "GET" });
const data = r.ok ? await r.json() : {};
returnData("results", data.items || []);
returnData("count", (data.items || []).length);
Parse and forward an incoming webhook payload
// Inputs: raw_body (custom_val), forward_url (custom_val)
let payload;
try {
payload = JSON.parse(params["raw_body"] || "{}");
} catch (e) {
returnData("ok", false);
returnData("error", "Invalid JSON in raw_body");
return;
}
const r = await fetch(params["forward_url"], {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
received_at: DateTime.format(DateTime.now(), "YYYY-MM-DD HH:mm:ss"),
record_id: record.id,
...payload
})
});
returnData("ok", r.ok);
returnData("forwarded_status", r.status);
Call the Tadabase REST API to update a record
// Use this when you need an action a built-in step doesn't expose.
// Allowlist your *.tadabase.io API host first.
const appId = getAppDetails("id", "");
const tableId = params["table_id"]; // e.g. "abc123"
const recId = params["record_id"];
const r = await fetch(`https://api.tadabase.io/api/v1/data-tables/${tableId}/records/${recId}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Tadabase-App-id": appId,
"X-Tadabase-App-Key": getAppVariable("td_app_key", ""),
"X-Tadabase-App-Secret": getAppVariable("td_app_secret", "")
},
body: JSON.stringify({
field_5: params["new_value"]
})
});
returnData("ok", r.ok);
returnData("status", r.status);
if (!r.ok) returnData("error", await r.text());
POST as application/x-www-form-urlencoded
// Some legacy APIs require form-encoded bodies, not JSON.
const body = new URLSearchParams({
email: params["email"],
list_id: getAppVariable("mailchimp_list_id", "")
}).toString();
const r = await fetch("https://api.example.com/subscribe", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body
});
returnData("ok", r.ok);
returnData("status", r.status);
Shorthand http.get / http.post
// `http` is a thin wrapper around fetch that returns the parsed body
// for OK responses, and throws for non-2xx.
try {
const data = await http.get("https://api.example.com/lookup?q=" + encodeURIComponent(record.email));
returnData("hit", !!data);
returnData("data", data);
} catch (e) {
returnData("hit", false);
returnData("error", e.message);
}
Skip the call when a cache field is fresh
// If we fetched within the last hour, reuse the cached value.
const lastRun = record.api_last_run_at;
const oldEnough = !lastRun || DateTime.diffDays(lastRun, DateTime.now()) >= 1
|| (Date.now() - new Date(lastRun).getTime()) > 3_600_000;
if (!oldEnough) {
returnData("from_cache", true);
returnData("data", record.api_cached_data);
return;
}
const r = await fetch("https://api.example.com/data?id=" + record.id);
const data = r.ok ? await r.json() : null;
returnData("from_cache", false);
returnData("data", data);
returnData("api_last_run_at", DateTime.format(DateTime.now(), "YYYY-MM-DD HH:mm:ss"));
Create a Stripe charge (PaymentIntent)
const secret = getAppVariable("stripe_secret_key", "");
if (!secret) { returnData("ok", false); returnData("error", "stripe_secret_key missing"); return; }
const body = new URLSearchParams({
amount: String(Math.round(Number(record.total) * 100)), // cents
currency: "usd",
customer: record.stripe_customer_id,
confirm: "true",
"automatic_payment_methods[enabled]": "true"
}).toString();
const r = await fetch("https://api.stripe.com/v1/payment_intents", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": `Bearer ${secret}`,
"Idempotency-Key": `pi-${record.id}-${DateTime.format(DateTime.now(), "YYYYMMDD")}`
},
body
});
const data = await r.json();
returnData("ok", r.ok);
returnData("intent_id", data.id || null);
returnData("status", data.status || null);
if (!r.ok) returnData("error", Obj.get(data, "error.message"));
Send an SMS via Twilio
const sid = getAppVariable("twilio_account_sid", "");
const token = getAppVariable("twilio_auth_token", "");
const from = getAppVariable("twilio_from_number", "");
const body = new URLSearchParams({
To: params["to"] || record.phone,
From: from,
Body: params["message"] || "Hello from Tadabase"
}).toString();
const r = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${sid}/Messages.json`, {
method: "POST",
headers: {
"Authorization": `Basic ${btoa(`${sid}:${token}`)}`,
"Content-Type": "application/x-www-form-urlencoded"
},
body
});
const data = await r.json();
returnData("ok", r.ok);
returnData("sid", data.sid || null);
returnData("status", data.status || null);
if (!r.ok) returnData("error", data.message || "Send failed");
Try a primary API, fall back to a secondary one
async function tryFetch(url) {
try {
const r = await fetch(url);
return r.ok ? await r.json() : null;
} catch (_) { return null; }
}
const data = (await tryFetch("https://primary.example.com/lookup"))
|| (await tryFetch("https://secondary.example.com/lookup"));
returnData("ok", !!data);
returnData("data", data);
returnData("source", data ? "primary_or_secondary" : "neither");
Honor a 429 Retry-After header
const r = await fetch("https://api.example.com/data");
if (r.status === 429) {
const retry = Number(r.headers.get("Retry-After")) || 5;
console.warn(`Rate limited — would retry in ${retry}s`);
returnData("ok", false);
returnData("rate_limited", true);
returnData("retry_after_seconds", retry);
return;
}
returnData("ok", r.ok);
returnData("status", r.status);
returnData("data", r.ok ? await r.json() : null);
We'd love to hear your feedback.