https://atwork.place
XRPC API Documentation
Programmatic access to job listings on at://work
Overview
at://work provides a public XRPC API for querying job listings. XRPC (Cross-Protocol RPC) is the standard API protocol used in the ATProtocol ecosystem. All endpoints are read-only and do not require authentication.
All responses return job listings as place.atwork.listing
records with metadata
including the AT-URI and CID for creating strong references.
API Endpoints
place.atwork.getListings
Get job listings, optionally filtered by tag or identity
GET /xrpc/place.atwork.getListings
Parameters
Parameter | Type | Required | Description |
---|---|---|---|
tag |
string | No | Filter listings by hashtag (without the # symbol) |
identity |
string | No | Filter listings by creator DID (e.g., did:plc:abc123) |
Example Response
{
"listings": [
{
"uri": "at://did:plc:abc123/place.atwork.listing/xyz789",
"cid": "bafyreib2rxk3rybk6zafezkrbzl3vq7hfqx6hs6whdb7wq4qvxpggxvvga",
"value": {
"title": "Senior Software Engineer",
"description": "We're looking for an experienced engineer...",
"notBefore": "2024-01-15T00:00:00Z",
"notAfter": "2024-03-15T23:59:59Z",
"applyLink": "https://example.com/apply",
"locations": [
{
"type": "remote",
"description": "Remote"
}
],
"facets": [...]
}
}
]
}
Code Examples
cURL
# Get all listings
curl "https://atwork.place/xrpc/place.atwork.getListings"
# Get listings filtered by tag
curl "https://atwork.place/xrpc/place.atwork.getListings?tag=engineering"
# Get listings filtered by identity (creator DID)
curl "https://atwork.place/xrpc/place.atwork.getListings?identity=did:plc:abc123"
# Combine filters
curl "https://atwork.place/xrpc/place.atwork.getListings?tag=engineering&identity=did:plc:abc123"
Python
import requests
# Get all listings
response = requests.get("https://atwork.place/xrpc/place.atwork.getListings")
listings = response.json()["listings"]
# Get listings filtered by tag
response = requests.get(
"https://atwork.place/xrpc/place.atwork.getListings",
params={"tag": "engineering"}
)
listings = response.json()["listings"]
# Get listings filtered by identity (creator DID)
response = requests.get(
"https://atwork.place/xrpc/place.atwork.getListings",
params={"identity": "did:plc:abc123"}
)
listings = response.json()["listings"]
# Combine filters
response = requests.get(
"https://atwork.place/xrpc/place.atwork.getListings",
params={"tag": "engineering", "identity": "did:plc:abc123"}
)
listings = response.json()["listings"]
for listing in listings:
print(f"{listing['value']['title']} - {listing['uri']}")
TypeScript
// Get all listings
const response = await fetch(
"https://atwork.place/xrpc/place.atwork.getListings"
);
const data = await response.json();
const listings = data.listings;
// Get listings filtered by tag
const tagResponse = await fetch(
"https://atwork.place/xrpc/place.atwork.getListings?tag=engineering"
);
const tagData = await tagResponse.json();
// Get listings filtered by identity (creator DID)
const identityResponse = await fetch(
"https://atwork.place/xrpc/place.atwork.getListings?identity=did:plc:abc123"
);
const identityData = await identityResponse.json();
// Combine filters
const combinedResponse = await fetch(
"https://atwork.place/xrpc/place.atwork.getListings?tag=engineering&identity=did:plc:abc123"
);
const combinedData = await combinedResponse.json();
interface ListingRecord {
uri: string;
cid: string;
value: {
title: string;
description: string;
notBefore: string;
notAfter: string;
applyLink?: string;
locations: Array<{type: string; description: string}>;
facets?: any[];
};
}
listings.forEach((listing: ListingRecord) => {
console.log(`${listing.value.title} - ${listing.uri}`);
});
place.atwork.getListing
Get a specific job listing by DID and record key
GET /xrpc/place.atwork.getListing
Parameters
Parameter | Type | Required | Description |
---|---|---|---|
repo |
string | Yes | The DID of the repository (e.g., did:plc:abc123) |
rkey |
string | Yes | The record key (e.g., xyz789) |
Errors
Status | Error | Description |
---|---|---|
404 | ListingNotFound | The specified listing does not exist |
500 | ListingFetchFailed | Internal error retrieving the listing |
Example Response
{
"uri": "at://did:plc:abc123/place.atwork.listing/xyz789",
"cid": "bafyreib2rxk3rybk6zafezkrbzl3vq7hfqx6hs6whdb7wq4qvxpggxvvga",
"value": {
"title": "Senior Software Engineer",
"description": "We're looking for an experienced engineer...",
"notBefore": "2024-01-15T00:00:00Z",
"notAfter": "2024-03-15T23:59:59Z",
"applyLink": "https://example.com/apply",
"locations": [...]
}
}
Code Examples
cURL
curl "https://atwork.place/xrpc/place.atwork.getListing?repo=did:plc:abc123&rkey=xyz789"
Python
import requests
response = requests.get(
"https://atwork.place/xrpc/place.atwork.getListing",
params={
"repo": "did:plc:abc123",
"rkey": "xyz789"
}
)
if response.status_code == 200:
listing = response.json()
print(f"Title: {listing['value']['title']}")
print(f"URI: {listing['uri']}")
print(f"CID: {listing['cid']}")
elif response.status_code == 404:
print("Listing not found")
else:
error = response.json()
print(f"Error: {error['error']} - {error['message']}")
TypeScript
const params = new URLSearchParams({
repo: "did:plc:abc123",
rkey: "xyz789"
});
const response = await fetch(
`https://atwork.place/xrpc/place.atwork.getListing?${params}`
);
if (response.ok) {
const listing = await response.json();
console.log(`Title: ${listing.value.title}`);
console.log(`URI: ${listing.uri}`);
console.log(`CID: ${listing.cid}`);
} else if (response.status === 404) {
console.log("Listing not found");
} else {
const error = await response.json();
console.error(`Error: ${error.error} - ${error.message}`);
}
place.atwork.searchListings
Search job listings with full-text query
GET /xrpc/place.atwork.searchListings
Parameters
Parameter | Type | Required | Description |
---|---|---|---|
query |
string | Yes | Search query (searches title, description, facets) |
Example Response
{
"listings": [
{
"uri": "at://did:plc:abc123/place.atwork.listing/xyz789",
"cid": "bafyreib2rxk3rybk6zafezkrbzl3vq7hfqx6hs6whdb7wq4qvxpggxvvga",
"value": {
"title": "Senior Software Engineer",
"description": "We're looking for an experienced engineer...",
...
}
}
]
}
Code Examples
cURL
curl "https://atwork.place/xrpc/place.atwork.searchListings?query=rust+engineer"
Python
import requests
response = requests.get(
"https://atwork.place/xrpc/place.atwork.searchListings",
params={"query": "rust engineer"}
)
listings = response.json()["listings"]
for listing in listings:
print(f"ā {listing['value']['title']}")
print(f" {listing['uri']}")
print()
TypeScript
const searchQuery = "rust engineer";
const params = new URLSearchParams({ query: searchQuery });
const response = await fetch(
`https://atwork.place/xrpc/place.atwork.searchListings?${params}`
);
const data = await response.json();
const listings = data.listings;
listings.forEach((listing: ListingRecord) => {
console.log(`ā ${listing.value.title}`);
console.log(` ${listing.uri}`);
});
Interactive API Testing
Test the XRPC endpoints directly from your browser.
Lexicon: place.atwork.listing
Job listings in at://work are stored as ATProto records using the place.atwork.listing
lexicon.
This section provides detailed documentation about the record schema, how to retrieve the lexicon definition,
and examples of valid records.
Lexicon Overview
Lexicons are the schema definition language used in ATProto. They define the structure, validation rules,
and semantics of records. The place.atwork.listing
lexicon defines job listing records that
can be created, read, updated, and deleted through the ATProto network.
Property | Value |
---|---|
Lexicon ID | place.atwork.listing |
Type | record |
Key Type | TID (timestamp identifier) |
Collection NSID | place.atwork.listing |
Resolving the Lexicon Authority
ATProto uses DNS TXT records to discover lexicon authorities. You can resolve the lexicon authority
using the dig
command:
DNS Resolution with dig
# Query DNS for the lexicon authority
dig _lexicon.atwork.place TXT +short
# Alternative: Query specific nameserver
dig @8.8.8.8 _lexicon.atwork.place TXT +short
# Expected output format:
# "did=did:web:atwork.place"
The TXT record indicates which DID has authority over this lexicon namespace. This DID can publish the canonical lexicon definition and any updates to the schema.
Retrieving the Lexicon Schema
You can retrieve the lexicon schema definition programmatically using HTTP:
HTTP Retrieval
# Retrieve the lexicon schema
goat get at://did:web:atwork.place/com.atproto.lexicon.schema/place.atwork.listing
curl 'https://pds.cauda.cloud/xrpc/com.atproto.repo.getRecord?repo=did:web:atwork.place&collection=com.atproto.lexicon.schema&rkey=place.atwork.listing'
# Pretty-print the JSON output
curl 'https://pds.cauda.cloud/xrpc/com.atproto.repo.getRecord?repo=did:web:atwork.place&collection=com.atproto.lexicon.schema&rkey=place.atwork.listing' | jq '.'
Complete Lexicon Schema
{
"$type": "com.atproto.lexicon.schema",
"defs": {
"main": {
"description": "A job listing",
"key": "tid",
"record": {
"properties": {
"applyLink": {
"description": "URL where applicants can apply for the job.",
"format": "uri",
"type": "string"
},
"description": {
"description": "The description of the job listing.",
"maxGraphemes": 10000,
"maxLength": 10000,
"type": "string"
},
"facets": {
"description": "Annotations of text (mentions, URLs, hashtags, etc).",
"items": {
"ref": "app.bsky.richtext.facet",
"type": "ref"
},
"type": "array"
},
"locations": {
"description": "Locations that are relevant to the job listing.",
"items": {
"refs": [
"community.lexicon.location.hthree"
],
"type": "union"
},
"type": "array"
},
"notAfter": {
"description": "Client-declared timestamp when the job listing expires.",
"format": "datetime",
"type": "string"
},
"notBefore": {
"description": "Client-declared timestamp when the job listing becomes visible.",
"format": "datetime",
"type": "string"
},
"title": {
"description": "The title of the job listing.",
"maxLength": 200,
"type": "string"
}
},
"required": [
"title",
"notBefore",
"notAfter",
"description"
],
"type": "object"
},
"type": "record"
}
},
"id": "place.atwork.listing",
"lexicon": 1
}
Field Reference
Detailed breakdown of all fields in the place.atwork.listing
record:
Required Fields
Field | Type | Constraints | Description |
---|---|---|---|
title |
string | max 200 chars |
The job title. Should be clear and descriptive.
Example: "Senior Rust Engineer"
|
notBefore |
datetime | ISO 8601 format |
When the listing becomes visible. Must be UTC timestamp.
Example: "2025-01-15T00:00:00.000Z"
|
notAfter |
datetime | ISO 8601 format, after notBefore |
When the listing expires and becomes hidden.
Example: "2025-04-15T23:59:59.999Z"
|
description |
string | max 10,000 chars/graphemes |
Full job description with details, requirements, and benefits. Supports Unicode.
Can include hashtags, mentions, and URLs which will be automatically linked via facets.
Example: "We're seeking an experienced Rust developer to join our backend team..."
|
Optional Fields
Field | Type | Constraints | Description |
---|---|---|---|
applyLink |
string (URI) | valid HTTPS URL |
Direct link to application form or job posting page.
Example: "https://example.com/careers/apply/123"
|
facets |
array | app.bsky.richtext.facet[] |
Rich text annotations for mentions (@handle), links (https://...), and hashtags (#tag).
Automatically extracted from the description text.
See example below for structure |
locations |
array | H3 location refs[] |
Geographic locations relevant to the job using H3 geospatial indexing.
Each location is represented by an H3 cell index at resolution 5.
Example: [{"$type": "community.lexicon.location.hthree", "h3": "852a1073fffffff"}]
|
Facets Structure
Facets enable rich text features in the description field. They define byte ranges in the UTF-8 encoded description text and associate them with semantic meaning (hashtags, mentions, links).
Facet Example: Hashtag
{
"index": {
"byteStart": 45,
"byteEnd": 57
},
"features": [
{
"$type": "app.bsky.richtext.facet#tag",
"tag": "engineering"
}
]
}
This facet marks the text #engineering
at bytes 45-57 in the description as a hashtag.
The tag value excludes the # symbol.
Facet Example: Mention
{
"index": {
"byteStart": 12,
"byteEnd": 27
},
"features": [
{
"$type": "app.bsky.richtext.facet#mention",
"did": "did:plc:abc123xyz456"
}
]
}
This facet marks @company.bsky.social
as a mention, linking to the specified DID.
Facet Example: Link
{
"index": {
"byteStart": 100,
"byteEnd": 130
},
"features": [
{
"$type": "app.bsky.richtext.facet#link",
"uri": "https://example.com/careers"
}
]
}
This facet marks https://example.com/careers
as a clickable link.
Complete Example Record
Here's a complete, valid place.atwork.listing
record with all fields:
Full Job Listing Record
{
"$type": "place.atwork.listing",
"title": "Senior Rust Engineer - Remote",
"notBefore": "2025-10-02T00:00:00.000Z",
"notAfter": "2025-12-31T23:59:59.999Z",
"description": "Join @acme.bsky.social as a Senior Rust Engineer!\n\nWe're building the next generation of decentralized applications using Rust, ATProto, and modern web technologies.\n\nš§ Requirements:\n- 5+ years of Rust development\n- Experience with async/await patterns\n- Strong understanding of concurrent systems\n- ATProto knowledge is a plus\n\nš° Benefits:\n- Competitive salary ($150k-$200k)\n- Full remote work\n- Health/dental/vision insurance\n- 401k matching\n- Unlimited PTO\n\nš Location: Remote (US/EU timezones preferred)\n\nApply at https://careers.acme.com/rust-engineer or email jobs@acme.com\n\n#rust #engineering #remote #atproto",
"applyLink": "https://careers.acme.com/rust-engineer",
"facets": [
{
"index": {
"byteStart": 5,
"byteEnd": 23
},
"features": [
{
"$type": "app.bsky.richtext.facet#mention",
"did": "did:plc:acme123xyz456"
}
]
},
{
"index": {
"byteStart": 412,
"byteEnd": 450
},
"features": [
{
"$type": "app.bsky.richtext.facet#link",
"uri": "https://careers.acme.com/rust-engineer"
}
]
},
{
"index": {
"byteStart": 472,
"byteEnd": 477
},
"features": [
{
"$type": "app.bsky.richtext.facet#tag",
"tag": "rust"
}
]
},
{
"index": {
"byteStart": 478,
"byteEnd": 490
},
"features": [
{
"$type": "app.bsky.richtext.facet#tag",
"tag": "engineering"
}
]
},
{
"index": {
"byteStart": 491,
"byteEnd": 498
},
"features": [
{
"$type": "app.bsky.richtext.facet#tag",
"tag": "remote"
}
]
},
{
"index": {
"byteStart": 499,
"byteEnd": 507
},
"features": [
{
"$type": "app.bsky.richtext.facet#tag",
"tag": "atproto"
}
]
}
],
"locations": [
{
"$type": "community.lexicon.location.hthree",
"h3": "8528308dfffffff",
"description": "San Francisco, California"
},
{
"$type": "community.lexicon.location.hthree",
"h3": "85283473fffffff",
"description": "New York, New York"
}
]
}
Notes on this example:
- The
$type
field identifies the record type - All required fields are present with valid data
- The description includes hashtags, mentions, and URLs
- Facets accurately mark byte positions for rich text features
- Locations use H3 resolution 5 cells for San Francisco and New York
- The applyLink provides a direct application URL
- Date range spans 3 months (October through December 2025)
Record Validation Rules
When creating or updating job listing records, the following validation rules apply:
- Title: Must be 3-200 characters, non-empty after trimming whitespace
- Description: Must be 50-10,000 characters, supports Unicode graphemes
- Date Range:
notBefore
can be up to 14 days in the pastnotAfter
must be afternotBefore
- Duration between dates must be 1 hour to 91 days
- Apply Link: If provided, must be a valid HTTPS URL (max 500 characters)
- Facets: Byte indices must be valid UTF-8 boundaries within the description
- Locations: Maximum of 3 locations per listing
- Tags: Maximum of 10 unique tags extracted from facets
Working with Records
Job listing records are stored in the ATProto repository under the collection NSID
place.atwork.listing
. The full AT-URI format is:
at://[did]/place.atwork.listing/[rkey]
Example:
at://did:plc:abc123xyz456/place.atwork.listing/3kvxyz789abc
Where 3kvxyz789abc
is a TID (timestamp identifier) automatically generated when the
record is created. TIDs are lexicographically sortable and encode the creation timestamp.
Rate Limiting
The XRPC API is intended for reasonable programmatic access. Please be respectful of server resources. If you need high-volume access, consider running your own instance of the listings service.