DocsInvites for agents
Requesting an invite for someone
AI agents acting for a person are welcome. Two calls, no browser.
Before you ask
The person must have read and agreed to the Terms and the Privacy Policy, and want an invite sent to their email address. Sending accept_terms: true says they have. The code goes to their address, not to you.
The two calls
GET https://intake.pigeonmessage.io/v1/challenge | A proof-of-work challenge, valid for 5 minutes and for one request: challenge (send it back as it is), nonce, salt, cost, key_length, key_prefix, max_counter, expires_at |
|---|---|
POST https://intake.pigeonmessage.io/v1/invite-request | JSON: email, accept_terms (must be true), challenge, solution, and optionally on_behalf_of, your agent's name and version (up to 100 characters). Answers 200 {"status": "received", "request_id": "r-…"} |
The solution is the counter n, from 0 to max_counter, for which PBKDF2-HMAC-SHA256 of the nonce followed by n as 4 big-endian bytes, with the salt, cost iterations and key_length bytes, starts with key_prefix in hex. It takes a few seconds.
The POST also carries the SHA-256 of its body, in hex, in an x-amz-content-sha256 header: the edge checks it. Hash the exact bytes you send. Without it, or if it doesn't match, the answer is a 403 from the edge rather than one of the errors below.
Example
A solver with Python's standard library, solve.py. It writes the body to body.json and prints that file's SHA-256:
import hashlib, json, sys
c = json.load(sys.stdin)
nonce, salt = bytes.fromhex(c["nonce"]), bytes.fromhex(c["salt"])
n = next(n for n in range(c["max_counter"] + 1)
if hashlib.pbkdf2_hmac("sha256", nonce + n.to_bytes(4, "big"), salt, c["cost"],
c["key_length"]).hex().startswith(c["key_prefix"]))
body = json.dumps({"email": sys.argv[1], "accept_terms": True, "challenge": c["challenge"],
"solution": n, "on_behalf_of": "ExampleAgent/1.0"}).encode()
open("body.json", "wb").write(body)
print(hashlib.sha256(body).hexdigest())Then the two calls:
hash=$(curl -sf https://intake.pigeonmessage.io/v1/challenge | python3 solve.py person@example.org)
curl -s https://intake.pigeonmessage.io/v1/invite-request \
-H 'content-type: application/json' \
-H "x-amz-content-sha256: $hash" \
--data-binary @body.jsonErrors
Every error is JSON, {"error": "<code>", "reason": "<what to fix>"}. The codes are stable; the reasons may be reworded.
| 400 | bad_request, email_missing, email_invalid, agree_missing (accept_terms isn't true), on_behalf_of_invalid, pow_missing, pow_invalid, pow_expired |
|---|---|
| 404, 405 | not_found, method |
| 409 | pow_replayed: get a new challenge for each request |
| 413, 415 | too_large (over 8192 bytes), bad_type (not application/json) |
| 422 | email_disposable, email_no_mail (the domain can't receive mail) |
| 429 | ip_limited, with a retry-after header in seconds |
| 500, 503 | internal, unavailable, dns_unavailable: nothing was kept; retry later |
Limits
- 5 requests an hour from one network, counted together with the request form.
- One challenge per request, spent once, valid for 5 minutes.
- Asking again for the same address gets the same answer; we hear about it once a day at most.
- No throwaway email services, and the address's domain must receive mail.
What we keep, and for how long, is in the privacy policy. Questions: support@pigeonmessage.io.