# Guide to Workato Webhooks: Features and Best Practices

Workato is an enterprise automation platform. Teams build recipes that react to events in hundreds of business apps, such as a new Salesforce opportunity, a NetSuite order or a scheduled timer, and then act on other apps. When one of those actions needs to reach your own application, the recipe calls it over HTTP, which makes Workato a webhook sender you design yourself.

This guide covers how the HTTP connector sends requests, how to shape them so one endpoint can serve several recipes, how to authenticate them with HTTP Basic credentials, how Workato handles timeouts, errors and retries, and the practices that keep a receiver reliable in production.

## What are Workato webhooks?

Workato has no outbound webhook feature with fixed events. Instead, a recipe step uses the HTTP connector's **Send request via HTTP** action. The request goes through an **HTTP connection**, which holds the authentication settings and a **Base URL**, and the action adds the method, path, parameters, headers and body.

That means the method, body and any event name are all chosen by whoever builds the recipe. Workato's own webhook triggers, where other services call into a recipe, are the opposite direction and are not covered here.

Workato does not sign requests. The HTTP connection supports many authentication types, and **Basic** is the one Hookdeck's Workato source checks.

## Workato webhook features

| Feature | Details |
| --- | --- |
| Mechanism | **Send request via HTTP** action in a recipe, using an HTTP connection |
| Base URL | Set on the connection. Recipes can't override it, and the action adds a relative path |
| Method | The recipe author's choice, such as `POST`, `PUT`, `PATCH`, `GET` or `DELETE` |
| Body | The recipe author's choice of request content type, such as JSON or raw JSON |
| Event types | None built in. Add your own `event` field |
| Authentication | None, Basic, Header auth, Query params, Custom, OAuth 2, AWS and Azure options. No signature |
| Timeout | Up to 120 seconds, or 11 to 3600 seconds with **Wait for response** |
| Errors | Non-2xx responses fail the step and stop the job unless handled |
| Retries | Implicit retry on timeouts for `GET`, `HEAD`, `PUT` and `DELETE`, unless **Disable retries** is set |

## Events

Because there are no built-in event types, the body is whatever the recipe builds. If several recipes call the same endpoint, agree on a small envelope so your handler can route and deduplicate:

```json
{
  "event": "order.created",
  "id": "a1b2c3d4-job-or-record-id",
  "data": {
    "order_id": "ord_123",
    "email": "jane@example.com"
  }
}

```

| Field | Purpose |
| --- | --- |
| `event` | A name you choose, such as `order.created` or `record.updated`, used for routing |
| `id` | A unique value from the recipe, such as the record ID plus its updated time, used for deduplication |
| `data` | The fields mapped from earlier recipe steps |

In the action, set **Request content type** to a JSON option. **JSON request body** builds input fields from a sample JSON object, and **Raw JSON request body** lets you type the structure directly.

> **Inspect Workato webhooks as they arrive.** Set a [Hookdeck Inspect](https://inspect.hookdeck.com) URL as the connection's Base URL and run the recipe step to see the exact method, headers and body Workato sends, with no account or setup required.

## Setting up Workato webhooks

Pick a username and generate a long random password, for example with `openssl rand -hex 32`, and store both on your receiver.

1. In Workato, create a connection for the **HTTP** connector and give it a name.
2. Set **Authentication type** to **Basic**.
3. Enter the username in **Basic auth user** and the password in **Basic auth password**.
4. Enter your HTTPS endpoint in **Base URL**, then connect.
5. In your recipe, add the **Send request via HTTP** action using that connection.
6. Choose the **Method**, a relative **Request URL** such as `webhooks/workato`, and a JSON request content type, then map the body.
7. Test the step and check that your endpoint receives the request.

The credentials must arrive on the first request. Workato's documentation says Basic credentials are Base64-encoded in transit, but doesn't say whether they are sent straight away or only after a `401` challenge with a `WWW-Authenticate` header. A sender that waits for a challenge will never authenticate against a receiver that doesn't issue one, so check the first test request for an `Authorization` header.

To put Hookdeck in front of your service, create a Hookdeck source with the **Workato** type and use its URL as the connection's Base URL. Enter the same **Username** and **Password** on the source. Hookdeck does not send a `401` challenge, so it needs the credentials on the first request. Hookdeck still answers with a `200`, but a request with missing or wrong credentials is recorded as failed verification and never reaches your destination. The source has `POST` selected. If your recipe uses `PUT`, `PATCH`, `GET` or `DELETE`, allow that method on the Hookdeck source as well.

If the recipe uses the response, for example to read an ID your service returns in a later step, don't put Hookdeck in between: Workato would receive Hookdeck's acknowledgement, not your service's response. Hookdeck fits steps that only need to know the request was accepted.

## Securing Workato webhooks

With Basic authentication, each request carries `Authorization: Basic base64(username:password)`. Decode it, split on the first colon, and compare the username and password with your stored values in constant time.

```javascript
const crypto = require('crypto');
const express = require('express');

function safeEqual(a, b) {
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

function verifyBasicAuth(header, username, password) {
  if (!username || !password) return false; // never accept when unconfigured
  const match = /^basic\s+([A-Za-z0-9+/]+={0,2})\s*$/i.exec(header || '');
  if (!match) return false;
  const decoded = Buffer.from(match[1], 'base64');
  const colon = decoded.indexOf(0x3a); // split on the first colon: passwords may contain ':'
  if (colon === -1) return false;
  const userOk = safeEqual(decoded.subarray(0, colon), Buffer.from(username, 'utf8'));
  const passOk = safeEqual(decoded.subarray(colon + 1), Buffer.from(password, 'utf8'));
  return userOk && passOk; // evaluate both before deciding
}

const app = express();

// The method is the recipe's choice, so accept any.
app.all('/webhooks/workato', express.raw({ type: '*/*' }), (req, res) => {
  const ok = verifyBasicAuth(
    req.get('authorization'),
    process.env.WORKATO_WEBHOOK_USERNAME,
    process.env.WORKATO_WEBHOOK_PASSWORD
  );
  if (!ok) return res.sendStatus(401);

  let payload = {};
  if (req.body && req.body.length > 0) { // a GET has no body
    try {
      payload = JSON.parse(req.body.toString('utf8'));
    } catch {
      return res.sendStatus(400);
    }
  }

  // Skip payload.id if you have seen it, then route on payload.event.
  switch (payload.event) {
    case 'order.created':
    case 'record.updated':
      break;
    default:
      // Log and acknowledge anything else.
  }

  res.sendStatus(200);
});

```

The same check in Python:

```python
import base64
import binascii
import hmac
import os
import re

BASIC_PATTERN = re.compile(r"^basic\s+([A-Za-z0-9+/]+={0,2})\s*$", re.IGNORECASE)

def verify_basic_auth(header, username, password) -> bool:
    if not username or not password:
        return False  # never accept when unconfigured
    match = BASIC_PATTERN.match(header or "")
    if not match:
        return False
    try:
        decoded = base64.b64decode(match.group(1), validate=True)
    except (binascii.Error, ValueError):
        return False
    received_user, sep, received_password = decoded.partition(b":")  # first colon only
    if not sep:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    user_ok = hmac.compare_digest(received_user, username.encode("utf-8"))
    password_ok = hmac.compare_digest(received_password, password.encode("utf-8"))
    return user_ok and password_ok

# verify_basic_auth(request.headers.get("authorization"),
#                   os.environ.get("WORKATO_WEBHOOK_USERNAME"),
#                   os.environ.get("WORKATO_WEBHOOK_PASSWORD"))

```

Basic credentials prove the sender knows the password. They do not cover the body and carry no timestamp, so a modified or replayed request still passes. Use an HTTPS Base URL, a long random password, and rotate it on the connection if it might have leaked. Anyone who can edit the recipe or connection controls what is sent, so limit who has that access in Workato.

The HTTP connection's **Header auth** type, which sends a custom header such as a bearer token, is another option, but Hookdeck's Workato source checks Basic credentials only. To use a header token through Hookdeck, use a generic source with API key authentication instead.

> **Make Workato webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the Basic credentials, acknowledges Workato straight away, and delivers each request to your service with retries you control.

## Workato webhook limitations and pain points

### Credentials have to arrive without a challenge

**The Problem:** Every request fails authentication even though the credentials are right.

**Why It Happens:** Some HTTP clients only send Basic credentials after a server answers `401` with a `WWW-Authenticate` challenge. Workato's docs don't say which way the HTTP connector works, and a receiver that verifies the request as it arrives, as Hookdeck does, never issues that challenge.

**Workarounds:**

* Test the step against a Hookdeck Inspect URL first and check that the `Authorization` header is there.
* If it isn't, use a header token with a source that checks it.

**How Hookdeck Can Help:** Hookdeck keeps every request, including those that fail verification, with full headers, so you can see straight away whether `Authorization` arrived.

### A failed request stops the job

**The Problem:** A short outage or slow deploy on your side fails Workato jobs, and someone has to rerun them.

**Why It Happens:** The HTTP connector treats non-2xx responses as errors, which stop the job unless the step is set to **Mark non-2xx response codes as success** or a **Handle errors** step catches them. Implicit retries only cover timeouts on `GET`, `HEAD`, `PUT` and `DELETE`, so a failed `POST` isn't retried.

**Workarounds:**

* Wrap the step in a **Handle errors** block with retries.
* Keep your endpoint fast, and acknowledge before slow work.

**How Hookdeck Can Help:** Hookdeck acknowledges the request as soon as it arrives and stores it, so the Workato job finishes. Delivery to your service then uses [retries](/docs/retries) you configure, and [issues](/docs/issues) alert you when deliveries fail.

### Retries can send the same request twice

**The Problem:** One recipe run creates two records in your system.

**Why It Happens:** Implicit retries on timeouts, **Handle errors** retries and manual job reruns all resend the request, and a timeout doesn't mean your service didn't process it.

**Workarounds:**

* Send a unique `id` in the body and deduplicate on it.
* Set **Disable retries** on steps that must never repeat, and handle failures explicitly.

**How Hookdeck Can Help:** Hookdeck [deduplication](/docs/deduplication) can drop repeats based on a body field such as `id` before they reach your service.

### No standard payload

**The Problem:** Each recipe sends a different shape, and your handler grows a branch per recipe.

**Why It Happens:** Workato has no built-in events, so every recipe author designs their own body.

**Workarounds:**

* Agree on the `event`, `id` and `data` envelope and use it in every recipe.
* Or give each recipe its own path under the Base URL.

**How Hookdeck Can Help:** [Transformations](/docs/transformations) can reshape requests from older recipes into your standard envelope, and [filters](/docs/filters) can route by `event` or path to different connections.

## Best practices

### Use one envelope for every recipe

`event`, `id` and `data` in every body make routing, logging and deduplication the same for all recipes. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Use `POST` for notifications

Sending events with `POST` keeps them out of URL logs and means a body is always present. If a recipe uses another method, accept it on your endpoint and on the Hookdeck source.

### Keep the response small and fast

Return `200` quickly once the request is authenticated and process it from a queue. Workato waits for your response and fails the step on a timeout. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Limit who can change the connection

The connection holds the credentials and the Base URL. Restrict edit access to it, and rotate the password when people with access leave.

## Conclusion

Workato sends webhooks through the HTTP connector, so the method, body and event names are yours to design. Use Basic credentials over HTTPS that arrive on the first request, compare them in constant time, send an `event` and `id` in a common envelope, and deduplicate because retries and reruns can repeat a request.

[Hookdeck Event Gateway](https://hookdeck.com) checks the Basic credentials, acknowledges Workato immediately, and delivers each request to your service with retries and deduplication.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Workato webhooks reliably in minutes.