---
title: "Listening locally"
url: https://services.dev/docs/cli/listen
---
# Listening locally

URL: https://services.dev/docs/cli/listen

> Watch your brand's events as they happen, forward them to your own endpoint signed the way your webhooks are, and fire any event yourself with trigger.

While `dev` runs, every job writes events to your brand's feed, in order. `listen` prints them as they land, `--forward-to` delivers them to your endpoint as signed webhooks, and `trigger` sends one in. [Your events](https://services.dev/docs/api/events.md) covers who receives which.

## Forward to your endpoint

`--forward-to` makes a watch for your URL. `dev` delivers each event there as a real webhook: signed with the watch's `whsec_` secret in `services-signature`, and retried when your endpoint fails. Stopping `listen` deletes the watch. `--events` narrows it to the events you name.

Here a Filing waits on the client's signature and on the registry's receipt, and the endpoint gets both:

```txt
magic values: client.email = signs@example.test

$ npx services.dev listen --forward-to http://localhost:4242/hook --events job.ordered,ask.opened,receipt.accepted,job.delivered
Ready! Forwarding to http://localhost:4242/hook as wh_1rKV5VS8V3. Its signing secret is whsec_88PiGzGqWulKhuHAUGldLH9GSl5J8tTF (^C to quit)

# in another terminal
$ npx services.dev filing "client=Ada Lovelace <signs@example.test>" document=lease.pdf --max 49
job_7Hq2 · working · $49.00 held
job_7Hq2 · delivered · lease.filing.pdf · $49.00 charged
  record  prepare · ai | sign · client · sign | review · ai · pass | file · the registry · accepted · rcp_7Hq2
  ledger  +$49.00 · AI $0.84 · you $48.16
  next    npx services.dev accept job_7Hq2

# listen
2026-10-25 14:02:11   --> job.ordered [evt_5gX1X2B5swzY61]
2026-10-25 14:02:11  <--  [200] POST http://localhost:4242/hook [evt_5gX1X2B5swzY61]
2026-10-25 14:02:11   --> ask.opened [evt_RpMfNTkcGvEOR4]
2026-10-25 14:02:11  <--  [200] POST http://localhost:4242/hook [evt_RpMfNTkcGvEOR4]
2026-10-25 14:02:11   --> receipt.accepted [evt_0NJwixDHOuv1kX]
2026-10-25 14:02:11  <--  [200] POST http://localhost:4242/hook [evt_0NJwixDHOuv1kX]
2026-10-25 14:02:11   --> job.delivered [evt_xdJmAitpoYURo6]
2026-10-25 14:02:11  <--  [200] POST http://localhost:4242/hook [evt_xdJmAitpoYURo6]

# the receiver at http://localhost:4242/hook, checking each delivery with webhooks.verify(body, header, 'whsec_88PiGzGqWulKhuHAUGldLH9GSl5J8tTF')
job.ordered [evt_5gX1X2B5swzY61] seq 1
  services-signature: t=1792936931,v1=833af4d63e571d303c476dbc60e03a836d4227bcfc4fd5da0efcf430d33aa7b4
  verified
ask.opened [evt_RpMfNTkcGvEOR4] seq 6
  services-signature: t=1792936931,v1=c1ca4269f41dc5d56686625f4acdb3558274e7736d56c2ce5d7caf9fd5306a22
  verified
receipt.accepted [evt_0NJwixDHOuv1kX] seq 15
  services-signature: t=1792936931,v1=da74c709784e4e583c68d48607c8ee9ccbd0acdc36d57c3228c6779ff7e8ad26
  verified
job.delivered [evt_xdJmAitpoYURo6] seq 19
  services-signature: t=1792936931,v1=1baa3b792d459db5a8dd9b4000def0cf0e0d2c8360e258d130bd49940b4727e7
  verified
```

Your endpoint checks each delivery with `webhooks.verify()` from `services.dev/agent`, and gets the typed event back. To see a retry, forward to a magic host such as `https://fails-once.example.test/hook`: its first delivery of each event fails. [Magic values](https://services.dev/docs/reference/magic-values.md) lists the hosts.

## Print the feed

Without `--forward-to`, `listen` prints what a buyer's key receives. Run it in one terminal, and work in another:

```txt
magic values: none

$ npx services.dev listen
Ready! Listening at http://localhost:8787/events/stream (^C to quit)

# in another terminal
$ npx services.dev trigger job.delivered
evt_5gX1X2B5swzY61 · seq 1 · job.delivered · synthetic

$ npx services.dev translation document=https://example.com/lease.pdf to=fr-CA --max 89
job_7Hq2 · working · $89.00 held
job_7Hq2 · waiting · on signature · $89.00 held
  record  translate · ai | harden · ai · adversarial · 2 lenses · pass
  next    npx services.dev approve job_7Hq2

$ npx services.dev trigger revised job=job_7Hq2
evt_w2jh2w27S71PvA · seq 13 · signal.raised · revised · goto translate

$ npx services.dev trigger revisde job=job_7Hq2
revisde isn't an inbound signal of Translation: it takes revised.
  docs    http://localhost:8787/docs/errors/input_invalid
$ echo $?
3

$ npx services.dev get job_7Hq2
job_7Hq2 · waiting · on signature · $89.00 held
  record  translate · ai | harden · ai · adversarial · 2 lenses · pass | revised · signal · goto translate | translate · ai · redo | harden · ai · adversarial · 2 lenses · pass
  next    npx services.dev approve job_7Hq2

# listen
2026-10-25 14:02:11   --> job.delivered · synthetic [evt_5gX1X2B5swzY61]
2026-10-25 14:02:11   --> job.ordered [evt_88cLb4PC0MfpT4]
2026-10-25 14:02:11   --> charge.held · $89.00 [evt_OhpD96gteDbiYF]
2026-10-25 14:02:11   --> step.committed · translate [evt_RpMfNTkcGvEOR4]
2026-10-25 14:02:11   --> step.committed · harden [evt_YH12iYAJIRWMeT]
2026-10-25 14:02:11   --> job.paused · on signature [evt_Lk7ARAO561bg4w]
2026-10-25 14:02:11   --> signal.raised · revised · goto translate [evt_w2jh2w27S71PvA]
2026-10-25 14:02:11   --> step.committed · translate [evt_OiiY1lICHzowCP]
2026-10-25 14:02:11   --> step.committed · harden [evt_GvS7pwKqxmgxvT]
2026-10-25 14:02:11   --> job.paused · on signature [evt_TtVPvhLLGG1KVz]
```

## Fire an event

`trigger <type>` is `events send`. Name an event of the catalog and it fires as a synthetic event, to its audience like any other, so you can test your endpoint for an event no job has reached yet. Name a job too, and it raises that job's inbound signal. This Translation takes one, `revised`, for when the buyer's source document changes:

```ts
import { Service } from 'services.dev'

export default Service({
  name: 'Translation',
  description: 'A document, translated and signed.',
  input: { document: 'file', to: 'language: a BCP 47 tag, such as fr-CA' },
  output: { document: 'file' },
  signals: { revised: { inbound: true, goto: 'translate' } },
  deliver: {
    translate: 'ai',
    harden: { review: 'adversarial', lenses: ['meaning', 'terms'], rounds: 2 },
  },
  human: { sign: 'sworn-translator', stamp: true },
  price: { usd: 89, payout: 30 },
})
```

Above, `revised` moved the waiting job back to translate, and the record shows the redo. A name the Service doesn't take is refused with exit code 3, `invalid_request`, and the job is untouched.

[Signals](https://services.dev/docs/service/signals.md) covers how a Service declares them, and [Events](https://services.dev/docs/reference/events.md) lists the catalog.

---

- Up: [The CLIs](https://services.dev/docs/cli.md)
- Previous: [In CI](https://services.dev/docs/cli/ci.md)
- Next: [Your MCP server](https://services.dev/docs/mcp.md)
