DocsDocs
- Reference
- Live today
Invite request API
The same invite request as the request form, as two HTTPS calls, for software that asks for a Pigeon invite for a person. The invite code goes to the person's own email address, never back to the caller.
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 two calls
First get a proof-of-work challenge, then send the request with its solution.
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, the calling software'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.
A worked 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": "ExampleApp/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.
The address is kept for 15 days after the last request for it, then deleted automatically (usually within 2 days after that). An email alias works.
What we keep, and for how long, is in the privacy policy. Questions: support@pigeonmessage.io.
Related
- Request an invite: the same request, as a form for a person.
- Invite codes: what the code does once it arrives.
- Privacy policy, invite requests: what is kept, and for how long.