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.
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.eligibleMake the request
{
"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] } }
]
}uristringrequiredhttporhttpsURL. Templates work here, such ashttps://api.example.com/orders/{{{steps.1.custom.orderNumberStr}}}.methodstringrequiredget,post,put,patchordelete. Anything else fails the step withMethod must be one of post, get, patch, put, or delete.headersobject- Flat object of header names and values. Set
Content-Typeyourself; the step doesn't add one. dataobject | string- The request body. With
kustomer.rest-api.jsonit's an object, sent as JSON. Withkustomer.rest-api.genericit's a string sent as-is, for form-encoded or XML bodies. Adatavalue that isn't a string is always sent as JSON. qsobject- Query string parameters, as a flat object.
jsonboolean- Parse the response body as JSON. If the body isn't valid JSON, it's left as a string.
allowNetworkRetryboolean- Retry connection resets, refusals and timeouts. See Network errors.
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:
{ "headers": { "Authorization": "Bearer {{{variables.6513b2f4a4b1e1001a6b3f21}}}" } }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)
}For services that expect HTTP Basic, encode the pair with the base64Encode block helper. Here the user name and password are two variables, referenced by their IDs:
{ "headers": { "Authorization": "Basic {{#base64Encode}}{{{variables.6513b2f4a4b1e1001a6b3f22}}}:{{{variables.6513b2f4a4b1e1001a6b3f23}}}{{/base64Encode}}" } }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:
{
"headers": {
"Content-Type": "application/json",
"X-Conversation-Id": "{{{steps.1.id}}}",
"X-Signature": "{{{authHMAC steps.1.id secretKey=variables.6513b2f4a4b1e1001a6b3f24 algorithm='sha256' encoding='hex'}}}"
}
}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:
{
"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.
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/10and 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:
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 resultYour 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:
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-Typeheader set;"json": truefor 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