# 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.

Source: https://developer.kustomer.com/docs/workflows/code-steps

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.

```mermaid
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

```json title="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**

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](https://developer.kustomer.com/docs/workflows/conditions-and-expressions#variables-and-secrets) holding the token. Workflows refer to variables by ID, not by name; the builder's variable picker inserts the ID for you.

> **Use Kustomer actions for Kustomer data:** Don't call the Kustomer API from a REST step. The built-in [actions](https://developer.kustomer.com/docs/workflows/triggers-and-actions#built-in-actions) are already authenticated, return flattened output, and handle not-found for you. Call the Kustomer API from your service instead, where you can use a scoped API key.

## 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](https://developer.kustomer.com/docs/workflows/conditions-and-expressions#variables-and-secrets), never in params.

**Bearer token**

The simplest option, and enough over HTTPS:

```json
{ "headers": { "Authorization": "Bearer {{{variables.6513b2f4a4b1e1001a6b3f21}}}" } }
```

```ts title="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)
}
```

**Basic auth**

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:

```json
{ "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:

```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'}}}"
  }
}
```

```python title="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:

```json title="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.

| Reference                              | Value                                                                           |
| -------------------------------------- | ------------------------------------------------------------------------------- |
| `/#steps.<id>.response.statusCode`     | The HTTP status, as a number                                                    |
| `/#steps.<id>.response.headers.<name>` | A response header. Names are lowercase                                          |
| `/#steps.<id>.body`                    | The 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](https://developer.kustomer.com/docs/workflows/conditions-and-expressions#handle-errors) 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](https://developer.kustomer.com/docs/workflows#the-life-of-a-run). 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:

```mermaid
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](https://developer.kustomer.com/api/core-resources/create-note-within-conversation) or updating custom attributes. Posting to an [inbound web hook](https://developer.kustomer.com/docs/events/inbound-webhooks) 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:

```bash
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](https://developer.kustomer.com/docs/apps/workflows-actions-triggers) 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
