---
title: "Callbacks"
description: "Pause a run until a webhook, a person or another system posts to a URL, with a timeout."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.ezghcloud.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Callbacks

A callback is a step that waits for someone to post to a URL: a webhook from another system, an
approval link in an email, a person finishing a task. The run suspends while it waits, for as long
as the timeout allows.

### TypeScript

```ts
import { CallbackTimeout } from "@ezgh/workflows"

const cb = await step.createCallback<{ approved: boolean }>("approval", { timeoutMs: 3 * 86_400_000 })
await step.run("ask", () => email.send(manager, `Approve? POST to ${cb.url}`))
try {
  const { approved } = await cb.wait()
} catch (e) {
  if (e instanceof CallbackTimeout) { /* nobody answered in time */ }
}
```
### Go

```go
cb, err := workflows.CreateCallback(ctx, "approval", 3*24*time.Hour)
_, err = workflows.Run(ctx, "ask", func(sc workflows.StepContext) (bool, error) {
	return true, mail.Send(sc, manager, "Approve? POST to "+cb.URL)
})
decision, err := workflows.WaitCallback[Decision](ctx, cb)
if errors.Is(err, workflows.ErrCallbackTimeout) { /* nobody answered in time */ }
```
### Python

```python
from ezgh_workflows import CallbackTimeoutError

cb = await ctx.step.create_callback("approval", timeout="3d")
await ctx.step.run("ask", lambda: send_email(manager, f"Approve? POST to {cb.url}"))
try:
    decision = await cb.wait()
except CallbackTimeoutError:
    ...  # nobody answered in time
```

- **The URL** is `https://workflows-api.ezghcloud.com/public/v1/callbacks/<token>`. It's recorded
  before your code sees it, so hand it out from a step, and a replay rebuilds the same URL.
- **The timeout** is 1 second to a year, 7 days by default, and starts when the callback is
  created. When it passes, waiting throws a callback timeout error (code
  `workflows.callback_timeout`), which the workflow can catch. It isn't retried.
- **Cancelling the run** ends its waiting callbacks at once.

## Complete a callback

**By its URL,** with no credential: the token in the URL is the only one, so treat the URL as a
secret. Post a JSON body, at most 1 MiB, with `Content-Type: application/json`:

```sh
curl -X POST "$CALLBACK_URL" -H "Content-Type: application/json" -d '{"approved": true}'
```

The body becomes the callback step's result. The SDKs have a helper that posts to a URL:
`completeCallbackURL(url, value)` in TypeScript, `workflows.CompleteCallbackURL(ctx, httpClient,
url, value)` in Go, and `complete_callback_url(url, value)` in Python.

**As an authenticated caller,** by run ID and step ID, with
[CompleteCallback](/workflows-api/CompleteCallback/) and `workflows.callbacks.complete` on
the workflow. It answers the run.

| Status | Meaning |
| --- | --- |
| `200` | Completed. The first completion wins; a repeat with the same body also answers `200` |
| `400` | The body isn't valid JSON |
| `404` | No such callback |
| `409 callback_completed` | Already completed with a different body |
| `410 callback_expired` | The callback timed out, or its run was cancelled or ended |
| `413` | The body is over 1 MiB |
| `415` | The body isn't `application/json` |
| `429` | Too many completions from your address: completing by URL allows 20 a second per client IP address, in bursts of up to 60 |

Completions by URL aren't recorded in [Trails](/trails); authenticated completions are recorded
when they're refused. Callback bodies never appear in Trails.

Source: https://docs.ezghcloud.com/workflows/callbacks/index.mdx
