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.

Base URL: https://atwork.place

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.

Request URL: https://atwork.place/xrpc/place.atwork.getListings

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 past
    • notAfter must be after notBefore
    • 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.

Additional Resources