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:

typescript
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). If your identifiers contain URIs or special characters, you may need to use {+identifier} to avoid double-encoding issues. 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.

typescript
.
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.

typescript
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:

typescript
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:

typescript
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.