{"openapi":"3.0.3","info":{"title":"Postar API","version":"1.0.0","description":"Transactional email API (login links, receipts, notifications). Queue one email per call; it is delivered asynchronously, DKIM-signed, from a domain with SPF and DMARC. Not for bulk marketing.\n\nA step-by-step integration guide for developers and AI agents is at [https://api.postar.vip/llm.txt](https://api.postar.vip/llm.txt).","contact":{"name":"Postar support","email":"office@rapttor.com"}},"servers":[{"url":"https://api.postar.vip"}],"tags":[{"name":"Messages","description":"Send email."},{"name":"Unsubscribes","description":"The per-project list of recipients who opted out."},{"name":"Guides","description":"Machine-readable integration guide."}],"paths":{"/v1/send":{"post":{"tags":["Messages"],"operationId":"sendMessage","summary":"Queue an email","description":"Validates the request and queues one email. `202` means *accepted*, not yet delivered; follow the message in the dashboard.\n\nEvery message gets an unsubscribe link at the bottom and `List-Unsubscribe` headers. This cannot be turned off: a recipient's right to opt out belongs to them. A recipient who unsubscribed from the project is rejected with `422` and `code: recipient_unsubscribed`, for every kind of mail (login links included). The response carries a `resubscribe_url`: show it to the user, who can ask there to receive your email again (confirmed by a link mailed to their address).\n\nMail to `localhost`, `*.local` and `*.test` addresses is never sent. When the project has *Allow local testing* enabled it is captured in the sandbox inbox (status `captured`, no credit used).","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendRequest"},"examples":{"loginLink":{"summary":"Login link","value":{"from":"login@postar.vip","name":"My App","reply_to":"office@myapp.example","to":"user@example.com","subject":"Your login link","html":"<p><a href=\"https://myapp.example/login?token=abc\">Log in</a></p>","text":"Log in: https://myapp.example/login?token=abc"}},"receipt":{"summary":"Plain-text receipt","value":{"from":"billing@postar.vip","to":"user@example.com","subject":"Your receipt","text":"Thanks for your order. Total: 19.00 EUR."}},"sandbox":{"summary":"Local test (captured, never sent)","value":{"from":"test@postar.vip","to":"dev@localhost","subject":"Template check","text":"Hello"}}}}}},"responses":{"202":{"description":"Accepted and queued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendAccepted"},"example":{"id":"aff09b6152d1d12bc00a5c3efb","status":"queued"}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Invalid or missing API key"}}}},"402":{"description":"The account has no credits left.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"No credits left for this account; top up or subscribe under Billing"}}}},"403":{"description":"The `from` domain is not allowed for this key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Domain 'example.org' is not allowed for this API key"}}}},"422":{"description":"Invalid field or missing body, or the recipient cannot be mailed: `code` is `recipient_unsubscribed` (with `resubscribe_url`) or `recipient_suppressed`. Do not retry unchanged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"invalid":{"summary":"Invalid request","value":{"error":"html or text is required"}},"unsubscribed":{"summary":"Recipient unsubscribed","value":{"error":"Recipient has unsubscribed from this project","code":"recipient_unsubscribed","resubscribe_url":"https://api.postar.vip/resubscribe/eyJ"}}}}}},"429":{"description":"Hourly rate limit of the key reached. Retry later.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Hourly rate limit reached for this API key"}}}},"500":{"description":"Temporary server error. Retry with backoff."}}}},"/v1/unsubscribes":{"post":{"tags":["Unsubscribes"],"operationId":"addUnsubscribe","summary":"Block an address for this project","description":"Use this when a user opts out inside your own app. From then on `/v1/send` rejects that address for this project with `422`. Idempotent: `201` when created, `200` when it was already blocked.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnsubscribeRequest"},"example":{"email":"user@example.com"}}}},"responses":{"201":{"description":"The address is now blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnsubscribeState"}}}},"200":{"description":"It was already blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnsubscribeState"}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Invalid or missing API key"}}}},"422":{"description":"Invalid email address.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"email must be a valid email address"}}}}}},"get":{"tags":["Unsubscribes"],"operationId":"getUnsubscribe","summary":"Check whether an address is blocked","security":[{"bearerAuth":[]}],"parameters":[{"name":"email","in":"query","required":true,"schema":{"type":"string","format":"email"}}],"responses":{"200":{"description":"Current state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnsubscribeState"}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Invalid or missing API key"}}}},"422":{"description":"Invalid email address.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"email must be a valid email address"}}}}}},"delete":{"tags":["Unsubscribes"],"operationId":"removeUnsubscribe","summary":"Lift a block your app placed","description":"Only for blocks added with `POST /v1/unsubscribes` (source `api`). When the recipient unsubscribed through the link in an email (source `recipient`) only they can resubscribe, from the confirmation page, so this returns `409`.","security":[{"bearerAuth":[]}],"parameters":[{"name":"email","in":"query","required":true,"schema":{"type":"string","format":"email"}}],"responses":{"200":{"description":"The address is not blocked (any more).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnsubscribeState"}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Invalid or missing API key"}}}},"409":{"description":"The recipient unsubscribed themselves.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"The recipient unsubscribed themselves; only they can resubscribe"}}}},"422":{"description":"Invalid email address.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"email must be a valid email address"}}}}}}},"/llm.txt":{"get":{"tags":["Guides"],"operationId":"getIntegrationGuide","summary":"Integration guide for developers and AI agents","description":"Plain-text Markdown guide with rules, error handling and ready-to-use code. Give this URL to a coding agent.","responses":{"200":{"description":"The guide.","content":{"text/plain":{"schema":{"type":"string"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Project API key, e.g. `pk_` followed by 48 hex characters. Create it in the dashboard; it is shown once. Keep it in an environment variable, never in git or client-side code."}},"schemas":{"SendRequest":{"type":"object","required":["from","to","subject"],"anyOf":[{"required":["html"]},{"required":["text"]}],"properties":{"from":{"type":"string","format":"email","maxLength":254,"example":"login@postar.vip","description":"Sender address. Any local part, but the domain must be allowed for the API key."},"name":{"type":"string","maxLength":120,"example":"My App","description":"Sender display name. No line breaks. Defaults to the project sender name."},"to":{"type":"string","format":"email","maxLength":254,"example":"user@example.com","description":"One recipient address."},"subject":{"type":"string","maxLength":998,"example":"Your login link","description":"No line breaks."},"html":{"type":"string","maxLength":1000000,"description":"HTML body. Send `html` and/or `text`; both is best."},"text":{"type":"string","maxLength":1000000,"description":"Plain-text body."},"reply_to":{"type":"string","format":"email","maxLength":254,"example":"office@myapp.example","description":"Where replies go. Addresses at postar.vip cannot receive mail, so set this to a mailbox you read. Defaults to the project Reply-To."}}},"SendAccepted":{"type":"object","required":["id","status"],"properties":{"id":{"type":"string","description":"Message id.","example":"aff09b6152d1d12bc00a5c3efb"},"status":{"type":"string","enum":["queued","captured"],"description":"`queued`: accepted for delivery. `captured`: stored in the sandbox inbox, not sent."}}},"UnsubscribeRequest":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","maxLength":254,"example":"user@example.com"}}},"UnsubscribeState":{"type":"object","required":["email","unsubscribed"],"properties":{"email":{"type":"string","format":"email"},"unsubscribed":{"type":"boolean"},"source":{"type":"string","nullable":true,"enum":["recipient","api","admin"],"description":"`recipient`: they used the link in an email. `api`: your app added it. `admin`: added in the dashboard."},"since":{"type":"string","format":"date-time","nullable":true}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What went wrong."},"code":{"type":"string","enum":["recipient_unsubscribed","recipient_suppressed"],"description":"Stable code for recipients that cannot be mailed (HTTP 422). Switch on this, not on the message."},"resubscribe_url":{"type":"string","format":"uri","description":"With `recipient_unsubscribed`: a page where the user can ask to receive your email again. Show or link it to them."}}}}}}