TurfAITurfAI Developers
Guides

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 returned
const 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"
      }
    }
  }
}
JSON
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 it
const 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 it

3. 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; default 60). A slow upstream that exceeds it fails as a transient error.
  • Gate risky downstream steps behind a decision node 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_code or a $.data.state field) 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):

JSONPathSelects
$.data.jd_file_idjd_file_id on the (unwrapped) body
$.data.items[0].idid of the first element of an array
$._status_codethe HTTP status code
$._raw_response.meta.nexta field on the full raw envelope

Keep mappings narrow — pull out only the fields later nodes consume.

Reference

On this page