Using Outbox¶
Outbox is required by the specification, but most implementations recognize an Actor even if it doesn't have an Outbox.
This guide explains how to implement a simple Outbox.
Define Empty Outbox¶
First, you must define the Outbox in order to use it:
app.outbox("/users/{identifier}/outbox")
The Outbox is handled by subscribing to the special Outbox class from apkit.types using the app.on decorator:
from apkit.models import OrderedCollection, Person
from apkit.server.types import Context
from apkit.server.responses import ActivityResponse
...
person = Person(
...
outbox="https://example.com/users/1/outbox"
)
@app.on(Outbox)
async def listen_outbox(ctx: Context):
identifier = ctx.request.path_params.get("identifier")
col = OrderedCollection(
id=f"https://example.com/users/{identifier}/outbox",
total_items=0,
ordered_items=[]
)
return ActivityResponse(col)
Returning real data¶
In most cases, you don't actually need to return the contents. (However, some implementations use the contents of the outbox to count the number of posts.)
However, this time let us retrieve and return actual data.
from datetime import datetime
from apkit.models import Announce, Create, Delete, Note, Tombstone, Person, OrderedCollection, OrderedCollectionPage
from fastapi.responses import JSONResponse
person = Person(id="https://example.com/users/alice")
PAGE_SIZE = 20
posts = [
Announce(
id="https://example.com/users/alice/activities/4",
actor=person.id,
published=datetime(2026, 1, 3),
to=["https://www.w3.org/ns/activitystreams#Public"],
object="https://example.net/users/bob/notes/2",
),
Delete(
id="https://example.com/users/alice/activities/3",
actor=person.id,
published=datetime(2026, 1, 2),
to=["https://www.w3.org/ns/activitystreams#Public"],
object=Tombstone(
id="https://example.com/users/alice/notes/2",
),
),
Create(
id="https://example.com/users/alice/activities/1",
actor=person.id,
published=datetime(2026, 1, 1),
to=["https://www.w3.org/ns/activitystreams#Public"],
object=Note(
id="https://example.com/users/alice/notes/1",
attributedTo="https://example.com/users/alice",
content="<p>Hello World!</p>",
published=datetime(2026, 1, 1),
to=["https://www.w3.org/ns/activitystreams#Public"]
),
)
]
@app.on(Outbox)
async def listen_outbox(ctx: Context):
identifier = ctx.request.path_params.get("identifier")
if identifier != "alice":
return JSONResponse({"message": "Not Found"}, status_code=404)
outbox_url = f"https://example.com/users/{identifier}/outbox"
is_page = ctx.request.query_params.get("page") == "true"
max_id = ctx.request.query_params.get("max_id")
if not is_page:
col = OrderedCollection(
id=outbox_url,
total_items=len(posts),
first=f"{outbox_url}?page=true",
last=f"{outbox_url}?page=true&min_id={posts[-1].id}" if posts else None
)
return ActivityResponse(col)
start_index = 0
if max_id:
for i, p in enumerate(posts):
if p.id == max_id:
start_index = i + 1
break
page_items = posts[start_index : start_index + PAGE_SIZE]
next_url = None
if start_index + PAGE_SIZE < len(posts):
last_item_id = page_items[-1].id
next_url = f"{outbox_url}?page=true&max_id={last_item_id}"
page = OrderedCollectionPage(
id=f"{outbox_url}?page=true" + (f"&max_id={max_id}" if max_id else ""),
part_of=outbox_url,
ordered_items=page_items,
next=next_url
)
return ActivityResponse(page)
What is Tombstone?
The Tombstone type indicates content that existed in the past but has now been deleted. By returning this object instead of completely removing the item from the Outbox, you can explicitly communicate to the remote server that "this post has been deleted."