Skip to content

Outbox listeners ​

Fedify can route POST requests to an actor's outbox through typed listeners. This is useful when you want to accept ActivityPub client-to-server activities from your own clients without exposing a separate non-standard API.

This guide covers POST /outbox. To serve GET /outbox, use the Collections guide.

Registering an outbox listener ​

With Fedify, you can register outbox listeners per activity type, just like inbox listeners. The following shows how to register a listener for Create activities:

federation
.
setOutboxListeners
("/users/{identifier}/outbox")
.
on
(
Create
, async (
ctx
,
activity
) => {
await
savePostedActivity
(
ctx
.
identifier
,
activity
);
await
ctx
.
sendActivity
(
{
identifier
:
ctx
.
identifier
},
myKnownRecipients
,
activity
,
); }) .
authorize
(async (
ctx
,
identifier
) => {
const
session
= await
verifyAccessToken
(
ctx
.
request
.
headers
.
get
("authorization"),
); return
session
?.
identifier
===
identifier
;
});

The setOutboxListeners() method registers the outbox path, and the on() method registers a listener for a specific activity type. The authorize() hook runs before the listener and can reject unauthorized requests with 401 Unauthorized.

Fedify also rejects a posted activity if its actor does not match the local actor who owns the addressed outbox.

TIP

If you need to handle every activity type, register a listener for the Activity class. Unsupported activity types can also be left unhandled, in which case Fedify responds with 202 Accepted without dispatching a listener.

NOTE

The URI Template syntax supports different expansion types like {identifier} (simple expansion) and {+identifier} (reserved expansion). Use the plain {identifier} form for ordinary segment-bounded identifiers; {+identifier} is an advanced choice reserved for identifiers that themselves contain slashes (such as embedded URIs). The outbox is additionally restricted: both setOutboxDispatcher() and setOutboxListeners() require the strict single-segment {identifier} shape and reject {+identifier} at registration time, so a writable (or read-only) outbox cannot use reserved expansion. See the URI Template guide for details.

Looking at OutboxContext.identifier ​

The identifier property contains the identifier from the matched outbox route. Fedify does not infer anything more specific than that.

.
on
(
Create
, async (
ctx
,
activity
) => {
console
.
log
(
ctx
.
identifier
);
console
.
log
(
activity
.
id
?.
href
);
});

Federating posted activities ​

Fedify does not federate client-posted activities automatically. If you want to deliver a posted activity, call sendActivity() or forwardActivity() explicitly inside your outbox listener.

federation
.
setOutboxListeners
("/users/{identifier}/outbox")
.
on
(
Create
, async (
ctx
,
activity
) => {
await
ctx
.
sendActivity
(
{
identifier
:
ctx
.
identifier
},
recipients
,
activity
,
); });

If the client already signed the posted JSON-LD with Linked Data Signatures or Object Integrity Proofs and you want to preserve that payload verbatim, use forwardActivity() instead of round-tripping through Fedify's vocabulary objects:

federation
.
setOutboxListeners
("/users/{identifier}/outbox")
.
on
(
Activity
, async (
ctx
) => {
await
ctx
.
forwardActivity
(
{
identifier
:
ctx
.
identifier
},
recipients
,
{
skipIfUnsigned
: true },
); });

If a listener returns without calling one of these delivery methods, Fedify logs a runtime warning. The @fedify/lint package also provides a lint rule for the same mistake; see Linting for details.

TIP

Explicit delivery keeps outbox listeners symmetric with inbox listeners: Fedify never guesses the recipient list for you, so applications can reuse their own caches and delivery policies.

Handling errors ​

You can attach an error handler to outbox listeners. It receives the outbox context along with the thrown error:

federation
.
setOutboxListeners
("/users/{identifier}/outbox")
.
on
(
Activity
, async () => {
throw new
Error
("Something went wrong.");
}) .
onError
(async (
ctx
,
error
) => {
console
.
error
(
ctx
.
identifier
,
error
);
});

Current scope ​

Outbox listeners currently provide the routing and authorization surface for client-to-server posting, but the rest of the server-side behavior remains application-defined.

In particular, Fedify does not currently do the following for you:

  • Persist the posted activity in your outbox collection
  • Generate IDs or Location headers for newly posted activities
  • Wrap non-Activity objects in Create automatically
  • Federate anything unless your listener calls ctx.sendActivity() or ctx.forwardActivity()

If you need full GET /outbox support as well, combine this guide with the Collections guide.