Practical guides
From token to verified event
1. Authentication and scopes
Send Authorization: Bearer jm_pat_… over HTTPS. Tokens are shown once, stored as keyed digests, limited to ten active tokens, and suspended—not deleted—if Pro access ends.
read- List and fetch aliases/messages, export status, and mark mail read.
manage-aliases- Reserve candidates, claim, pause, resume, and burn aliases.
delete- Permanently delete retained messages.
webhooks- Manage endpoints, delivery logs, tests, redelivery, and secret rotation.
test- Inject deterministic test mail into an owned active alias.
curl --fail-with-body \
-H 'Authorization: Bearer YOUR_PRO_TOKEN' \
'https://junksink.com/api/v1/messages?limit=50'
2. Pagination, ETags, and errors
Follow the opaque nextCursor exactly and do not construct cursors. Message resources return weak ETags; send If-None-Match to receive 304 Not Modified. Errors use one stable object with code, message, and optional field details. Rate responses include X-RateLimit-* and may include Retry-After.
3. Webhooks end to end
- Expose an HTTPS endpoint on port 443 with a publicly routable address.
- Calculate the receiver certificate public-key SPKI SHA-256 pin and save it with the endpoint for the pinned acceptance journey below.
- Store the endpoint secret shown once. Also cache the current Ed25519 public key from
https://junksink.com/.well-known/junkmail-webhooks.json. - Use Send test event, verify
webhook-id,webhook-timestamp, and both values inwebhook-signature, then return any 2xx. - Reject timestamps more than five minutes old and deduplicate on the stable event id.
Calculate the pin independently from the receiver you control. Review the hostname and resulting certificate before saving it:
openssl s_client -connect receiver.example:443 -servername receiver.example </dev/null 2>/dev/null \
| openssl x509 -pubkey -noout \
| openssl pkey -pubin -outform DER \
| openssl dgst -sha256 -binary \
| openssl base64 -A
# Save the result as: sha256/BASE64_OUTPUT
Create the endpoint with that exact pin, save the one-time secret outside source control, then queue a signed test event:
curl --fail-with-body \
-H 'Authorization: Bearer YOUR_PRO_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"name":"CI receiver","url":"https://receiver.example/webhooks/junkmail","subscription":{"mode":"all"},"spki_pin":"sha256/BASE64_OUTPUT","include_text_body":false}' \
https://junksink.com/api/v1/webhooks
curl --fail-with-body -X POST \
-H 'Authorization: Bearer YOUR_PRO_TOKEN' \
https://junksink.com/api/v1/webhooks/WEBHOOK_ID/test
Junkmail retries connection errors, timeouts, 429, and 5xx at roughly 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours, and 24 hours. Delivery stops after eight attempts; endpoints with three continuous days of total failure are disabled and their owner is notified. Redirects and SPKI mismatches fail closed.
4. Verify both signatures
The exact signed bytes are {webhook-id}.{webhook-timestamp}.{raw request body}. Split the signature header on spaces: v1,BASE64_HMAC uses the endpoint secret; v1a,BASE64_ED25519 uses the service key whose kid is included in the published key set. Always compare decoded signatures in constant time.
JavaScript (Node.js)
import { createHmac, createPublicKey, timingSafeEqual, verify } from 'node:crypto'
const signed = Buffer.concat([Buffer.from(`${id}.${timestamp}.`, 'utf8'), rawBody])
if (Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > 300) throw new Error('stale webhook')
const expected = createHmac('sha256', endpointSecret).update(signed).digest()
const receivedHmac = Buffer.from(v1, 'base64')
const hmacOk = receivedHmac.length === expected.length && timingSafeEqual(expected, receivedHmac)
const rawPublicKey = Buffer.from(publicKeyBase64, 'base64')
if (rawPublicKey.length !== 32) throw new Error('invalid Ed25519 public key')
// RFC 8410 SubjectPublicKeyInfo prefix for a raw 32-byte Ed25519 key.
const spki = Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'), rawPublicKey])
const serviceKey = createPublicKey({ key: spki, format: 'der', type: 'spki' })
const ed25519Ok = verify(null, signed, serviceKey, Buffer.from(v1a, 'base64'))Python
import base64, hashlib, hmac
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
signed = f"{event_id}.{timestamp}.".encode() + raw_body
hmac_ok = hmac.compare_digest(
hmac.new(endpoint_secret.encode(), signed, hashlib.sha256).digest(),
base64.b64decode(v1),
)
Ed25519PublicKey.from_public_bytes(public_key).verify(base64.b64decode(v1a), signed)PHP
<?php
$signed = $id.'.'.$timestamp.'.'.$rawBody;
$hmacOk = hash_equals(hash_hmac('sha256', $signed, $endpointSecret, true), base64_decode($v1, true));
$ed25519Ok = sodium_crypto_sign_verify_detached(base64_decode($v1a, true), $signed, base64_decode($publicKey, true));curl capture
curl --fail-with-body --raw \
-D webhook-headers.txt -o webhook-body.json \
-H 'Content-Type: application/json' \
--data-binary @event.json https://receiver.example/webhooks/junkmail
# Verify the untouched bytes in webhook-body.json with one of the snippets above.5. Deterministic test mail
POST /test/messages accepts JSON or raw message/rfc822, never touches SMTP, and follows the normal parse → rules → index → realtime → webhook pipeline. Test messages count toward storage, carry source: test, and expire after seven days.
curl --fail-with-body \
-H 'Authorization: Bearer YOUR_PRO_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"aliasId":42,"from":"ci@example.test","subject":"Login code","text":"Code: 482913","otp":"482913"}' \
https://junksink.com/api/v1/test/messages
In Playwright, Cypress, or pytest: inject; poll the returned message URL until processingStatus is processed; assert the extracted code; then delete the fixture. Use a unique subject per test and respect the 500/day plan limit.
6. Export retained mail
Queue a whole-mailbox export or pass an aliasId for one alias. Poll the returned Location until the status is ready, then download the ZIP within 24 hours. It contains each retained raw .eml plus manifest.json.
curl --fail-with-body -i -X POST \
-H 'Authorization: Bearer YOUR_PRO_TOKEN' \
-H 'Content-Type: application/json' \
-d '{}' https://junksink.com/api/v1/exports
curl --fail-with-body \
-H 'Authorization: Bearer YOUR_PRO_TOKEN' \
https://junksink.com/api/v1/exports/EXPORT_ID
curl --fail-with-body \
-H 'Authorization: Bearer YOUR_PRO_TOKEN' \
-o junkmail-mailbox.zip \
https://junksink.com/api/v1/exports/EXPORT_ID/download
7. API changelog
2026-08-31 · api_version: 2026-08-31. Initial Pro API: scoped tokens, cursor pagination and ETags, Standard Webhooks dual signatures and SPKI pins, test-mail injection, and mailbox export. Additive fields may appear within this version; incompatible changes require a new dated version and overlap notice.