Call your own code

Run your own logic from a Kustomer workflow with REST steps. Covers building requests, authentication and signing, branching on status codes, network retries, URL restrictions, timeouts, long-running work, and getting results back into Kustomer.

Workflows don't run arbitrary code. When you need logic the built-in actions can't express, such as pricing rules, a lookup in your database or a call to a partner API, put it in a small HTTP service you own and call it with a REST step. Your service can use any language, libraries and data. The workflow passes it the context it needs and branches on what it returns.

Kustomer APIYour serviceWorkflowKustomer APIYour serviceWorkflowopt[Need more context]POST /kustomer/refund-eligibility (JSON + auth header)1Your logic, your database2GET /v1/customers/{id}3200 { "eligible": true, "maxAmount": 40 }4Branch on /5
Diagram source
sequenceDiagram
  autonumber
  participant W as Workflow
  participant S as Your service
  participant K as Kustomer API
  W->>S: POST /kustomer/refund-eligibility (JSON + auth header)
  S->>S: Your logic, your database
  opt Need more context
    S->>K: GET /v1/customers/{id}
  end
  S-->>W: 200 { "eligible": true, "maxAmount": 40 }
  W->>W: Branch on /#steps.check.body.eligible

Make the request

REST step
{
  "id": "check",
  "action": "kustomer.rest-api.json",
  "params": {
    "method": "post",
    "uri": "https://hooks.example.com/kustomer/refund-eligibility",
    "headers": {
      "Content-Type": "application/json",
      "Authorization": "Bearer {{{variables.6513b2f4a4b1e1001a6b3f21}}}"
    },
    "data": {
      "conversationId": "/#steps.1.id",
      "customerId": "/#steps.1.customer",
      "orderNumber": "/#steps.1.custom.orderNumberStr",
      "channel": "/#steps.1.channels.0"
    },
    "json": true,
    "allowNetworkRetry": true
  },
  "transitions": [
    { "target": "approve", "condition": { "op": "and", "values": [
      { "op": "eq", "values": ["/#steps.check.response.statusCode", 200] },
      { "op": "true", "values": ["/#steps.check.body.eligible"] }
    ] } },
    { "target": "escalate", "condition": { "op": "true", "values": [true] } }
  ]
}
Params
uristringrequired
methodstringrequired
headersobject
dataobject | string
qsobject
jsonboolean
allowNetworkRetryboolean

Use kustomer.rest-api.json for almost everything, and always set "json": true when you expect JSON back, so later steps can reference fields like /#steps.check.body.eligible.

6513b2f4a4b1e1001a6b3f21 in the Authorization header is the ID of a workflow variable holding the token. Workflows refer to variables by ID, not by name; the builder's variable picker inserts the ID for you.

Authenticate

Your endpoint is on the public internet, so it must reject requests that don't come from your workflow. Store the credential in a workflow variable, never in params.

The simplest option, and enough over HTTPS:

JSON
{ "headers": { "Authorization": "Bearer {{{variables.6513b2f4a4b1e1001a6b3f21}}}" } }
Express
import { timingSafeEqual } from 'node:crypto'

function fromKustomer(req: Request) {
  const expected = Buffer.from(`Bearer ${process.env.KUSTOMER_WORKFLOW_TOKEN}`)
  const actual = Buffer.from(req.get('authorization') ?? '')
  return actual.length === expected.length && timingSafeEqual(actual, expected)
}

Sign your requests

When a partner requires a signature, compute an HMAC with the authHMAC helper. Sign a value the receiver can reconstruct, such as the ID of the record the request is about:

JSON
{
  "headers": {
    "Content-Type": "application/json",
    "X-Conversation-Id": "{{{steps.1.id}}}",
    "X-Signature": "{{{authHMAC steps.1.id secretKey=variables.6513b2f4a4b1e1001a6b3f24 algorithm='sha256' encoding='hex'}}}"
  }
}
Verify (Python)
import hashlib, hmac, os

def valid(request) -> bool:
    conversation_id = request.headers["X-Conversation-Id"]
    expected = hmac.new(os.environ["KUSTOMER_SIGNING_KEY"].encode(), conversation_id.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, request.headers.get("X-Signature", ""))

The step serializes data itself, so a template can't sign the exact body bytes. Treat the signed ID as the trustworthy part, and if the body drives anything important, re-read the record from the Kustomer API before acting on it.

Handle responses and failures

The REST step doesn't fail on HTTP status codes. A 404 or 503 from your service is a successful step whose output says what happened:

Step output
{
  "response": {
    "statusCode": 503,
    "headers": { "content-type": "text/plain", "retry-after": "30" }
  },
  "body": "Service Unavailable"
}

So every REST step needs transitions for the outcomes you care about, as in the example above, with a final always-true branch to catch everything else.

ReferenceValue
/#steps.<id>.response.statusCodeThe HTTP status, as a number
/#steps.<id>.response.headers.<name>A response header. Names are lowercase
/#steps.<id>.bodyThe body: an object with "json": true and a JSON response, otherwise a string
/#steps.<id>.body.<field>A field of a JSON body

Network errors

When the request doesn't produce a response at all (DNS failure, connection refused or reset, or no response within 60 seconds), the step fails and the run stops with an error. Don't count on error cases to recover from a connection failure: they're designed around Kustomer action errors, where /#error is the API's error object. Plan for the run to end at that step.

Network retries are off by default. With "allowNetworkRetry": true, the step first retries connection resets, refusals and timeouts: up to 3 attempts in total, 5 and then 10 seconds apart. Only enable it if your endpoint is safe to call twice, because a timed-out request may have reached your service. Use an idempotency key such as the conversation ID.

Calls can repeat

Your endpoint can receive the same request more than once even without allowNetworkRetry. If a later Kustomer action in the same run gets a 5xx from the API, Kustomer runs the whole workflow again from step 1, and your REST step sends its request again. Make your endpoint idempotent: key the work on a stable value such as the conversation ID plus what happened, and return the earlier result for a repeat.

Where you can send requests

The URL must use http or https and resolve to a public IPv4 address. The step rejects:

  • Private, loopback, link-local, carrier-grade NAT, multicast, reserved and documentation ranges (10.0.0.0/8, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10 and others)
  • IPv6 literals such as https://[2001:db8::1]/
  • Hostnames that don't resolve

Expose services in a private network through a public gateway or reverse proxy with authentication. Use https for anything that carries customer data or credentials.

Keep it fast

A REST step waits up to 60 seconds, and the whole run must finish within 5 minutes. The run limit is checked between steps, so the run ends after the step that crosses it. For anything slower, such as an LLM call, a batch job or a human approval, reply immediately and finish asynchronously:

Second workflowInbound web hookYour serviceWorkflowSecond workflowInbound web hookYour serviceWorkflowWork for as long as it takesPOST /jobs { conversationId }202 { "jobId": "j_81" }POST { jobId, conversationId, result }kustomer.hooks.web.<id>.receiveUpdate the conversation with the result
Diagram source
sequenceDiagram
  participant W as Workflow
  participant S as Your service
  participant H as Inbound web hook
  participant W2 as Second workflow
  W->>S: POST /jobs { conversationId }
  S-->>W: 202 { "jobId": "j_81" }
  Note over S: Work for as long as it takes
  S->>H: POST { jobId, conversationId, result }
  H->>W2: kustomer.hooks.web.<id>.receive
  W2->>W2: Update the conversation with the result

Your service can also write results directly with the Kustomer API, for example by adding a note or updating custom attributes. Posting to an inbound web hook instead keeps the "what to do with the result" logic in a workflow your admins can see and change.

Run your code from outside a workflow

The reverse direction, your system starting a workflow, also uses an inbound web hook. Workflows can't be called directly over REST. Create a web hook, use it as the trigger of a workflow, and post to it from your code:

Shell
curl -X POST "$KUSTOMER_HOOK_URL" \
  -H "Authorization: Bearer $KUSTOMER_HOOK_KEY" -H "Content-Type: application/json" \
  -d '{ "conversationId": "6512f0c2a4b1e1001a6b2c90", "action": "escalate" }'

In apps

An app can package a REST call as a named action with its own inputs, so admins drag "Look up order" into a workflow instead of configuring a URL. App actions can read the installing org's settings, such as an API base URL or key, and can authenticate with an app account using OAuth 2.0 or a custom header, so the credential never appears in a workflow.

Checklist

  • Endpoint is HTTPS, public, and rejects unauthenticated requests
  • Credentials are in workflow variables, not params
  • Content-Type header set; "json": true for JSON responses
  • Variables referenced by ID, such as {{{variables.6513b2f4a4b1e1001a6b3f21}}}
  • Transitions for success, expected failures and a catch-all
  • Acceptable behavior when a network failure ends the run
  • Endpoint responds in well under 60 seconds and is idempotent, because the run can repeat