Hermes
A wrapper API for Hack Club's Theseus mail system. Create letters, track costs, and manage events with ease.
Authentication
All API requests require authentication using a Bearer token in the Authorization header.
Event API Keys
Each event has its own API key. Use this key to create letters for that event.
Authorization: Bearer your_event_api_key
Admin API Key
The admin API key is used for administrative operations like marking events as paid.
Authorization: Bearer your_admin_api_key
API Endpoints
POST/api/v1/letters
Create a new letter. Requires an event API key.
Request Body
| Field | Type | Description |
|---|---|---|
| first_name required | string | Recipient's first name |
| last_name required | string | Recipient's last name |
| address_line_1 required | string | Street address |
| address_line_2 optional | string | Apartment, suite, etc. |
| city required | string | City name |
| state required | string | State/Province |
| postal_code required | string | ZIP/Postal code |
| country required | string | Country name (e.g., "Canada", "United States") |
| recipient_email optional | string | Email for tracking notifications |
| mail_type required | string | "lettermail", "bubble_packet", or "parcel" |
| weight_grams conditional | integer | Required for bubble_packet and parcel |
| rubber_stamps required | string | Items to pack (see Rubber Stamps) |
| notes optional | string | Additional metadata |
Response
{
"letter_id": "ltr!32jhyrnk",
"cost_usd": 1.19,
"formatted_rubber_stamps": "1x pack of\nstickers\n1x Postcard",
"status": "queued",
"theseus_url": "https://mail.hackclub.com/back_office/letters/ltr!32jhyrnk",
"email_sent": true
}
A confirmation email will be sent to the recipient if an email address was provided.
POST/admin/events/{event_id}/mark-paid
Mark an event as paid. Requires admin API key.
Response
{
"event_id": 1,
"event_name": "Haxmas 2024",
"previous_balance_cents": 15430,
"new_balance_cents": 0,
"is_paid": true
}
GET/admin/financial-summary
Get a summary of all unpaid events. Requires admin API key.
Response
{
"unpaid_events": [
{
"event_id": 1,
"event_name": "Haxmas 2024",
"balance_due_usd": 154.30,
"letter_count": 127,
"last_letter_at": "2025-01-09T14:32:00Z"
}
],
"total_due_usd": 154.30
}
POST/admin/check-letter-status
Manually trigger a status check for all pending letters. Requires admin API key.
Response
{
"checked": 47,
"updated": 12,
"mailed": 5
}
POST/api/v1/order
Create a new order request. Hermes will order from a local carrier and ship to the recipient. Charged to your event's HCB. $1 fee per item.
Request Body
| Field | Type | Description |
|---|---|---|
| order_text required | string | Description of what to order (max 5000 chars). Do NOT include names/addresses here - use the dedicated fields below. |
| first_name required | string | Recipient's first name (not stored) |
| last_name required | string | Recipient's last name (not stored) |
| email optional | string | Recipient's email (not stored) |
| address_line_1 required | string | Street address (not stored) |
| address_line_2 optional | string | Apartment, suite, etc. (not stored) |
| city required | string | City name (not stored) |
| state required | string | State/Province (not stored) |
| postal_code required | string | ZIP/Postal code (not stored) |
| country required | string | Country name (not stored) |
Response
{
"order_id": "aB3xK9m",
"status": "pending",
"status_url": "https://fulfillment.hackclub.com/odr!aB3xK9m",
"created_at": "2025-01-16T10:30:00Z",
"email_sent": true
}
A confirmation email will be sent to the recipient if an email address was provided.
Example: GitHub Notebook
curl -X POST https://fulfillment.hackclub.com/api/v1/order \
-H "Authorization: Bearer YOUR_EVENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"order_text": "GitHub Notebook - 1x black hardcover, 100 pages",
"first_name": "Orpheus",
"last_name": "Hacksworth",
"email": "orpheus@hackclub.com",
"address_line_1": "123 Hacker Way",
"city": "Shelburne",
"state": "VT",
"postal_code": "05482",
"country": "United States"
}'
GET/odr!{order_id}
Public order status page. Shows pending/fulfilled status with tracking code (no personal information displayed).
Status Display
- Pending: Shows "Your order is being processed"
- Fulfilled: Shows fulfillment note and tracking code if provided
GET/api/v1/order/{order_id}/status
Get order status via API (public endpoint, no auth required).
Response
{
"order_id": "aB3xK9m",
"status": "fulfilled",
"tracking_code": "1Z999AA10123456784",
"fulfillment_note": "Shipped via UPS Ground",
"created_at": "2025-01-16T10:30:00Z",
"fulfilled_at": "2025-01-17T14:00:00Z"
}
Orders
The Orders API allows you to request items that Hermes will order from a local carrier and ship to the recipient. Orders are charged to your event's HCB account.
Order Flow
- Send a POST request to
/api/v1/orderwith order details and shipping address - Receive an order ID and status URL (e.g.,
fulfillment.hackclub.com/odr!aB3xK9m) - Share the status URL with the recipient
- Hermes orders items from local carrier and ships to the provided address
- Costs are charged to your event's HCB account
- Once fulfilled, status page shows tracking code and notes
Example: Ordering a GitHub Notebook
curl -X POST https://fulfillment.hackclub.com/api/v1/order \
-H "Authorization: Bearer YOUR_EVENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"order_text": "GitHub Notebook - 1x black hardcover, 100 pages",
"first_name": "Orpheus",
"last_name": "Hacksworth",
"email": "orpheus@hackclub.com",
"address_line_1": "123 Hacker Way",
"city": "Shelburne",
"state": "VT",
"postal_code": "05482",
"country": "United States"
}'
import requests
response = requests.post(
"https://fulfillment.hackclub.com/api/v1/order",
headers={
"Authorization": "Bearer YOUR_EVENT_API_KEY",
"Content-Type": "application/json"
},
json={
"order_text": "GitHub Notebook - 1x black hardcover, 100 pages",
"first_name": "Orpheus",
"last_name": "Hacksworth",
"email": "orpheus@hackclub.com",
"address_line_1": "123 Hacker Way",
"city": "Shelburne",
"state": "VT",
"postal_code": "05482",
"country": "United States"
}
)
print(response.json())
Privacy & Security
Shipping addresses are NEVER stored in the database. PII is:
- Sent directly to the Slack #mail channel when the order is created
- Visible only in that one Slack message
- Never stored, cached, or logged anywhere
- Cannot be retrieved from the API or database
The public status page (/odr!{id}) only shows:
- Order ID
- Status (Pending or Fulfilled)
- Tracking code (if provided)
- Fulfillment note (if provided)
No personal information (names, addresses, etc.) is ever displayed on the public status page.
Cost Calculator
Use this calculator to estimate shipping costs before creating letters.
Estimate Shipping Cost
Mail Type Limits
| Type | Max Weight | Max Dimensions |
|---|---|---|
| Lettermail | 30g | 245mm × 156mm × 5mm (9.6" × 6.1" × 0.2") |
| Bubble Packet | 500g | 380mm × 270mm × 20mm (14.9" × 10.6" × 0.8") |
| Parcel | Custom quote required - contact @jenin | |
Rubber Stamps Field
The rubber_stamps field specifies what items should be packed in the envelope. This text gets printed on the fulfillment label.
Format Guidelines
- List each item on its own line
- Use clear, descriptive text
- Include quantities if applicable
- Use
\nfor line breaks in JSON
Good Examples
"rubber_stamps": "1x pack of stickers\n1x Postcard of Euan eating a Bread"
"rubber_stamps": "3x Hack Club stickers\n1x Thank you card\n1x Event badge"
"rubber_stamps": "Haxmas 2024 Winner Prize Package"
Bad Examples
"rubber_stamps": "stuff" // Too vague - what should be packed?
"rubber_stamps": "" // Empty - not allowed
Code Examples
curl -X POST https://your-api.com/api/v1/letters \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"first_name": "John",
"last_name": "Doe",
"address_line_1": "123 Main St",
"city": "Burlington",
"state": "VT",
"postal_code": "05401",
"country": "Canada",
"mail_type": "lettermail",
"rubber_stamps": "1x pack of stickers\n1x Postcard"
}'
import requests
response = requests.post(
"https://your-api.com/api/v1/letters",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json"
},
json={
"first_name": "John",
"last_name": "Doe",
"address_line_1": "123 Main St",
"city": "Burlington",
"state": "VT",
"postal_code": "05401",
"country": "Canada",
"mail_type": "lettermail",
"rubber_stamps": "1x pack of stickers\n1x Postcard"
}
)
print(response.json())
const response = await fetch('https://your-api.com/api/v1/letters', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
first_name: 'John',
last_name: 'Doe',
address_line_1: '123 Main St',
city: 'Burlington',
state: 'VT',
postal_code: '05401',
country: 'Canada',
mail_type: 'lettermail',
rubber_stamps: '1x pack of stickers\n1x Postcard'
})
});
const data = await response.json();
console.log(data);
API Playground
Test the API directly from your browser. Enter your API key and send requests — everything runs locally, nothing is stored.