Custom REST integration
Call your own API from a workflow, with secrets stored as credentials.
What you'll build
A workflow node that calls an external HTTP API — passing a secret token from the encrypted
credential store, and mapping fields out of the JSON response
into workflow variables you can use downstream (fetch a Drive file, send an email, branch on a
status). You'll see both a GET and a POST node, the response shape, and how failures behave.
Prerequisites
- A TurfAI account and a JWT — see Authentication.
- The base URL exported as
$BASE:
export BASE="https://apisandbox.turfai.in/api"
export TURFAI_JWT="<your-jwt>"1. Store the secret as a credential
Never hardcode tokens in a task config. Credentials are encrypted at rest with AES-256-GCM and
the API never returns secret values — only metadata and field_names. Note the numeric id in
the response: you reference it from the node as credential_id.
curl -X POST "$BASE/credentials" \
-H "Authorization: Bearer $TURFAI_JWT" \
-H "Content-Type: application/json" \
-d '{
"data": {
"name": "Acme API",
"provider": "acme",
"fields": { "api_token": "sk_live_…" }
}
}'import os, requests
base = os.environ["BASE"]
r = requests.post(
f"{base}/credentials",
headers={"Authorization": f"Bearer {os.environ['TURFAI_JWT']}"},
json={
"data": {
"name": "Acme API",
"provider": "acme",
"fields": {"api_token": "sk_live_…"},
}
},
)
cred = r.json()["data"]
print(cred["id"], cred["field_names"]) # e.g. 7 ['api_token'] — value never returnedconst base = process.env.BASE!;
const res = await fetch(`${base}/credentials`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TURFAI_JWT}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
data: {
name: "Acme API",
provider: "acme",
fields: { api_token: "sk_live_…" },
},
}),
});
const { data: cred } = await res.json();
console.log(cred.id, cred.field_names); // e.g. 7 ['api_token'] — value never returned{
"data": {
"id": 7,
"name": "Acme API",
"provider": "acme",
"field_names": ["api_token"]
}
}2. Add a rest_api node (and reference the credential)
A workflow is created/updated through the Workflow API; the node below goes in its nodes
array. Point the node at credential 7 with credential_id. At execution time the executor
decrypts the credential, merges each field into the node's input_mapping, and drops
credential_id — so the {{api_token}} template resolves and the processor never sees the
secret. {{role_id}} comes from the trigger payload or an upstream node.
# Node fragment to include in the workflow's "nodes" array.
cat <<'JSON'
{
"id": "fetch-role-config",
"type": "rest_api_task",
"data": {
"label": "Fetch Role Configuration",
"task_type": "rest_api_task",
"config": {
"credential_id": 7,
"url": "https://api.acme.com/roles/{{role_id}}",
"method": "GET",
"headers": { "Authorization": "Bearer {{api_token}}" },
"timeout": 30,
"output_mapping": {
"jd_file_id": "$.data.jd_file_id",
"hiring_manager_email": "$.data.hiring_manager_email"
}
}
}
}
JSONnode = {
"id": "fetch-role-config",
"type": "rest_api_task",
"data": {
"label": "Fetch Role Configuration",
"task_type": "rest_api_task",
"config": {
"credential_id": 7,
"url": "https://api.acme.com/roles/{{role_id}}",
"method": "GET",
"headers": {"Authorization": "Bearer {{api_token}}"},
"timeout": 30,
"output_mapping": {
"jd_file_id": "$.data.jd_file_id",
"hiring_manager_email": "$.data.hiring_manager_email",
},
},
},
}
# include `node` in the workflow's "nodes" array when you create/update itconst node = {
id: "fetch-role-config",
type: "rest_api_task",
data: {
label: "Fetch Role Configuration",
task_type: "rest_api_task",
config: {
credential_id: 7,
url: "https://api.acme.com/roles/{{role_id}}",
method: "GET",
headers: { Authorization: "Bearer {{api_token}}" },
timeout: 30,
output_mapping: {
jd_file_id: "$.data.jd_file_id",
hiring_manager_email: "$.data.hiring_manager_email",
},
},
},
};
// include `node` in the workflow's "nodes" array when you create/update it3. The response shape
The processor returns a structured result. data is the response body unwrapped — if the
upstream returns a standard { "success": true, "data": {...} } envelope, data is the inner
object so $.data.field reads intuitively; otherwise data is the raw body. _status_code is
the HTTP status, and _raw_response is the full untouched body for debugging.
{
"data": { "jd_file_id": "1ABC123xyz", "hiring_manager_email": "manager@acme.com" },
"_status_code": 200,
"_raw_response": { "success": true, "data": { "jd_file_id": "1ABC123xyz", "hiring_manager_email": "manager@acme.com" } },
"jd_file_id": "1ABC123xyz",
"hiring_manager_email": "manager@acme.com"
}Top-level dict fields from data are also spread onto the result for direct access. Your
output_mapping fields (jd_file_id, hiring_manager_email) are now workflow variables you
can reference downstream — e.g. a google_drive_fetch_task on {{jd_file_id}} or an
email_send_task to {{hiring_manager_email}}.
Very large response bodies are kept in _raw_response but can be heavy to pass between nodes.
Prefer narrow output_mapping JSONPaths so downstream nodes carry only the fields they need.
4. POST / PUT bodies
For write methods include a templated body; it's sent as JSON for POST/PUT/PATCH. Mix
credential fields and upstream variables freely.
{
"id": "create-candidate",
"type": "rest_api_task",
"data": {
"task_type": "rest_api_task",
"config": {
"credential_id": 7,
"url": "https://api.acme.com/candidates",
"method": "POST",
"headers": {
"Authorization": "Bearer {{api_token}}",
"Content-Type": "application/json"
},
"body": { "name": "{{candidate_name}}", "email": "{{email}}" },
"output_mapping": { "candidate_id": "$.data.id" }
}
}
}5. Error handling
Any non-2xx response fails the task and the execution (status: "failed", with
failed_node naming this node). The same happens on a timeout or a network/DNS error. The
error message carries the upstream status and body:
{
"status": "failed",
"failed_node": "fetch-role-config",
"error": "HTTP 404: {\"success\": false, \"message\": \"role not found\"}"
}To make a step robust:
- Set a realistic
timeout(seconds; default60). A slow upstream that exceeds it fails as a transient error. - Gate risky downstream steps behind a
decisionnode when you'd rather branch than fail. Because a hard non-2xx fails the whole run, branch on a status the API returns in a 2xx body (e.g.$._status_codeor a$.data.statefield) rather than expecting to catch the 404 itself. - Blocked URLs fail fast. TurfAI rejects requests to private/reserved IPs and non-HTTP(S) schemes (SSRF protection) — these surface as a config error, not a retryable one.
JSONPath in output_mapping
output_mapping maps a workflow variable name → a JSONPath into the response. Paths are
evaluated against the result described in step 3 (so $.data.… reads the unwrapped body):
| JSONPath | Selects |
|---|---|
$.data.jd_file_id | jd_file_id on the (unwrapped) body |
$.data.items[0].id | id of the first element of an array |
$._status_code | the HTTP status code |
$._raw_response.meta.next | a field on the full raw envelope |
Keep mappings narrow — pull out only the fields later nodes consume.
Reference
- Integrations overview, credentials, and OAuth: Integrations concept.
rest_api_taskconfig and output fields: Task types reference.- Credential endpoints: Credential API.