# Publishers (/docs/management/publishers)



A **publisher** is a source of inbound items — a traffic source or lead vendor
that submits <Term name="Work" /> into Ductor. It is the **supply-side**
counterpart to a [recipient](/docs/management/pools-and-recipients): a recipient
is a <Term name="Worker" /> a routing decision selects, while a publisher is an
*origin* that feeds work in. Managing a publisher means controlling how much it
may submit, the quality bar its submissions must clear, what you pay for accepted
items, and whether it is currently allowed to submit at all — the entry gate where
work is admitted before it is priced and routed.

<Callout title="Where it lives">
  Publishers are served by `PublishersService` under `/api/publishers`; the
  service implementation is in `modules/services/publisher/`. Every operation is
  tenant-scoped and gated by `publisher:read`, `publisher:write`, or
  `publisher:delete`.
</Callout>

## The publisher object [#the-publisher-object]

<TypeTable
  type="{
  id: { description: &#x22;Output-only unique identifier.&#x22;, type: &#x22;string&#x22; },
  name: { description: &#x22;Required display name, unique per tenant.&#x22;, type: &#x22;string&#x22; },
  tenant_id: { description: &#x22;Owning tenant.&#x22;, type: &#x22;string&#x22; },
  status: { description: &#x22;Lifecycle status: active, paused, or disabled.&#x22;, type: &#x22;string&#x22; },
  daily_cap: { description: &#x22;Maximum items accepted per day. 0 means unlimited.&#x22;, type: &#x22;int32&#x22; },
  weekly_cap: { description: &#x22;Maximum items accepted per week. 0 means unlimited.&#x22;, type: &#x22;int32&#x22; },
  monthly_cap: { description: &#x22;Maximum items accepted per month. 0 means unlimited.&#x22;, type: &#x22;int32&#x22; },
  current_daily: { description: &#x22;Output-only. Items received today.&#x22;, type: &#x22;int32&#x22; },
  current_weekly: { description: &#x22;Output-only. Items received this week.&#x22;, type: &#x22;int32&#x22; },
  current_monthly: { description: &#x22;Output-only. Items received this month.&#x22;, type: &#x22;int32&#x22; },
  quality_threshold: { description: &#x22;Minimum quality score (0.0–1.0) required for items from this publisher.&#x22;, type: &#x22;double&#x22; },
  revenue_share: { description: &#x22;Revenue share percentage paid to this publisher (e.g. 15.0 for 15%).&#x22;, type: &#x22;double&#x22; },
  tags: { description: &#x22;Labels for categorizing and filtering publishers.&#x22;, type: &#x22;repeated string&#x22; },
}"
/>

The `current_*` counters are read-only usage against the matching `*_cap`. Caps
and the quality threshold are the two levers that shape ingestion: a cap bounds
*how much* a publisher may send, the threshold bounds *what quality* is accepted.

## Create and configure [#create-and-configure]

`CreatePublisher` — `POST /api/publishers` (`publisher:write`) — registers a
source with its caps, quality bar, and revenue share. Only `name` is required;
caps default to `0` (unlimited).

```bash
curl -s -X POST https://api.ductor.io/api/publishers \
  -H "Authorization: Bearer $DUCTOR_TOKEN" \
  -H "X-Tenant-ID: $DUCTOR_TENANT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Lead Source",
    "daily_cap": 1000,
    "weekly_cap": 5000,
    "monthly_cap": 20000,
    "quality_threshold": 0.7,
    "revenue_share": 15.0,
    "tags": ["north-america", "tier1"]
  }'
```

A duplicate name returns a conflict; an out-of-range quality threshold or other
business-rule violation returns an unprocessable-entity error rather than being
silently clamped.

`GetPublisher` (`GET /api/publishers/{publisher_id}`, `publisher:read`) returns
one publisher including its live usage counters; `ListPublishers`
(`GET /api/publishers`, `publisher:read`) paginates the tenant's publishers
newest-first with `pagination.page_size` (max 1000, default 50) and
`pagination.page_token`. `UpdatePublisher`
(`PUT /api/publishers/{publisher_id}`, `publisher:write`) edits name, caps,
threshold, revenue share, and tags.

`DeletePublisher` (`DELETE /api/publishers/{publisher_id}`, `publisher:delete`)
permanently removes a publisher. Items it has already submitted are not affected.

## Pause and resume ingestion [#pause-and-resume-ingestion]

Rather than deleting a misbehaving source, pause it. `PausePublisher` —
`POST /api/publishers/{publisher_id}/pause` (`publisher:write`) — stops the
publisher from accepting new routable items; items already in flight are
unaffected. `ResumePublisher` (`POST .../resume`, `publisher:write`) re-enables
it.

```bash
curl -s -X POST https://api.ductor.io/api/publishers/$PUBLISHER_ID/pause \
  -H "Authorization: Bearer $DUCTOR_TOKEN" \
  -H "X-Tenant-ID: $DUCTOR_TENANT_ID"
```

<Callout type="info">
  Pause and resume are guarded against no-ops: pausing an already-paused publisher
  (or resuming one that isn't paused) returns an unprocessable-entity error, so the
  status transition is always explicit.
</Callout>

## Pricing [#pricing]

Each publisher carries its own monetization settings for accepted items.
`GetPublisherPricing` (`GET /api/publishers/{publisher_id}/pricing`,
`publisher:read`) returns the current configuration; `UpdatePublisherPricing`
(`PUT .../pricing`, `publisher:write`) upserts it.

<TypeTable
  type="{
  publisher_id: { description: &#x22;The publisher this pricing belongs to.&#x22;, type: &#x22;string&#x22; },
  price_per_accepted_lead: { description: &#x22;Charge applied per accepted lead.&#x22;, type: &#x22;double&#x22; },
  currency: { description: &#x22;ISO currency code; defaults to USD.&#x22;, type: &#x22;string&#x22; },
  updated_at: { description: &#x22;Output-only. When pricing was last updated.&#x22;, type: &#x22;timestamp&#x22; },
}"
/>

```bash
curl -s -X PUT https://api.ductor.io/api/publishers/$PUBLISHER_ID/pricing \
  -H "Authorization: Bearer $DUCTOR_TOKEN" \
  -H "X-Tenant-ID: $DUCTOR_TENANT_ID" \
  -H "Content-Type: application/json" \
  -d '{ "publisher_id": "'"$PUBLISHER_ID"'", "price_per_accepted_lead": 55.0, "currency": "USD" }'
```

## Statistics [#statistics]

`GetPublisherStats` — `GET /api/publishers/{publisher_id}/stats`
(`publisher:read`) — returns volume and quality metrics for a publisher: how much
it submitted, how much was accepted or rejected, its acceptance rate, average
quality, and the revenue it generated.

<TypeTable
  type="{
  total_submitted: { description: &#x22;Total items submitted.&#x22;, type: &#x22;string&#x22; },
  total_accepted: { description: &#x22;Total items accepted.&#x22;, type: &#x22;string&#x22; },
  total_rejected: { description: &#x22;Total items rejected.&#x22;, type: &#x22;string&#x22; },
  acceptance_rate: { description: &#x22;Acceptance rate, 0.0–1.0.&#x22;, type: &#x22;double&#x22; },
  average_quality: { description: &#x22;Average quality score of submissions.&#x22;, type: &#x22;double&#x22; },
  revenue_generated: { description: &#x22;Total revenue generated.&#x22;, type: &#x22;double&#x22; },
  updated_at: { description: &#x22;When the stats were last computed.&#x22;, type: &#x22;timestamp&#x22; },
}"
/>

Read `acceptance_rate` and `average_quality` together against the publisher's
`quality_threshold` to decide whether a source is worth its caps and revenue
share, or whether to tighten the threshold, pause it, or retire it.

## Operations at a glance [#operations-at-a-glance]

| Operation        | Method & path                                | Scope              |
| ---------------- | -------------------------------------------- | ------------------ |
| List publishers  | `GET /api/publishers`                        | `publisher:read`   |
| Create publisher | `POST /api/publishers`                       | `publisher:write`  |
| Get publisher    | `GET /api/publishers/{publisher_id}`         | `publisher:read`   |
| Update publisher | `PUT /api/publishers/{publisher_id}`         | `publisher:write`  |
| Delete publisher | `DELETE /api/publishers/{publisher_id}`      | `publisher:delete` |
| Pause publisher  | `POST /api/publishers/{publisher_id}/pause`  | `publisher:write`  |
| Resume publisher | `POST /api/publishers/{publisher_id}/resume` | `publisher:write`  |
| Get pricing      | `GET /api/publishers/{publisher_id}/pricing` | `publisher:read`   |
| Update pricing   | `PUT /api/publishers/{publisher_id}/pricing` | `publisher:write`  |
| Get statistics   | `GET /api/publishers/{publisher_id}/stats`   | `publisher:read`   |

## Where to go next [#where-to-go-next]

<Cards>
  <Card title="Pools & recipients" href="/docs/management/pools-and-recipients">
    The demand-side targets that receive what publishers supply.
  </Card>

  <Card title="Managing resources" href="/docs/management">
    Shared conventions — headers, pagination, errors — across every call.
  </Card>
</Cards>
