{"openapi":"3.1.0","info":{"title":"Barua API","version":"1","contact":{"email":"hello@barua.tz","url":"https://barua.tz/developers"},"description":"Send email from your own domain, read what became of it, and manage domains, sub-accounts, keys, webhooks and suppressions.\n\n**Authentication.** Every request carries `Authorization: Bearer barua_...`. Make a key under Settings → API in Barua; only its hash is stored, so copy it when it is shown. Each endpoint needs one scope (`x-scope` on the operation). A key can mint keys through `POST /keys`, holding at most the scopes it holds itself.\n\n**Sub-accounts.** Add `X-Barua-Account: <id>` to act for a sub-account of the key's account. The key's own scopes apply. An id that is not one of yours answers `404 account_not_found`, the same as one that does not exist.\n\n**Idempotency.** `POST /emails` accepts `Idempotency-Key` (1 to 255 characters). The same key with the same body within 24 hours returns the first answer with `Idempotent-Replayed: true`; with a different body it is `422 idempotency_mismatch`; while the first request is still running it is `409 idempotency_in_progress`. Only answers that settled the send are filed: a validation error or a `502 send_failed` is not, so a retry with the same key after one is a fresh attempt.\n\n**Pagination.** Lists take `limit` (1 to 100, default 25) and `cursor`, and answer `{ data, nextCursor }`. Pass `nextCursor` back as `cursor`; it is null on the last page.\n\n**Rate limit.** 60 requests a minute per key, and 600 a minute per account across all its keys, on every endpoint together. Over either: `429 rate_limited`.\n\n**Errors.** Every error is `{ \"error\": { \"code\", \"message\" } }`. Branch on `code`; `message` is written for a person and may change."},"servers":[{"url":"https://barua.tz/api/v1"}],"security":[{"bearer":[]}],"x-rate-limit":{"perKey":60,"perAccount":600,"window":"minute"},"tags":[{"name":"Emails","description":"Sending, and the log of what was sent."},{"name":"Domains","description":"Connect a domain, publish its records, verify it."},{"name":"Accounts","description":"The account a key acts for, and its sub-accounts."},{"name":"Keys","description":"API keys, minted and revoked by API key."},{"name":"Webhooks","description":"URLs delivery outcomes are posted to."},{"name":"Suppressions","description":"Addresses this account will not send to."},{"name":"Database","description":"Your own Postgres, where your mail is written."}],"paths":{"/emails":{"post":{"operationId":"sendEmail","tags":["Emails"],"summary":"Send an email","x-scope":"emails:send","description":"Checks run in a fixed order, so the refusal says which layer said no: the key, its scope and X-Barua-Account; the Idempotency-Key; whether the account may send at all (suspended, or over its daily quota), and for a sub-account whether its parent may; whether the paying account has credit; the body; the from-domain; the suppression list, and the parent's too for a sub-account. Only then does anything leave. With sandbox true every check still runs and the send is logged, but nothing is sent, counted or charged.","parameters":[{"$ref":"#/components/parameters/Account"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendRequest"},"example":{"from":"Duka Langu <receipts@dukalangu.co.tz>","to":"neema@gmail.com","template":{"name":"receipt","receiptNumber":"R-1042","total":"25,000","paidOn":"24 September 2026","paymentMethod":"M-Pesa"}}}}},"responses":{"200":{"description":"Sent, or sandboxed. A warning is present while the paying account is on its free trial or in the grace window after its credits ran out.","headers":{"Idempotent-Replayed":{"description":"Present, with the value true, when this response is the stored answer to an earlier request that carried the same Idempotency-Key and the same body.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendResult"},"example":{"id":"9b2e6d7a-3c4f-4e5a-9b1c-2d3e4f5a6b7c","sandbox":true}}}},"400":{"description":"invalid_json: The request body could not be parsed as JSON. invalid_idempotency_key: Idempotency-Key was sent but is empty or longer than 255 characters.","x-codes":["invalid_json","invalid_idempotency_key"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_json","message":"The request body is not valid JSON."}}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"402":{"description":"no_credits: The paying account has no credits and its grace window has closed.","x-codes":["no_credits"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"no_credits","message":"You have run out of email credits and the grace period has ended. Top up under Settings → API in Barua and sending resumes immediately."}}}}},"403":{"description":"insufficient_scope: The key lacks the scope the endpoint needs, or tried to mint a key with a scope it does not hold itself. suspended: Sending is paused on the account this request acts for, or on its parent. The message is the reason, written for the sender; a parent's pause is prefixed with: The parent account cannot send. domain_not_yours: The from-address is on a domain this account has not connected. domain_not_ready: The domain is connected but its ownership record is not verified or sending is not yet enabled for it.","x-codes":["insufficient_scope","suspended","domain_not_yours","domain_not_ready"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"insufficient_scope","message":"This key cannot do that. It needs the emails:send scope."}}}}},"404":{"$ref":"#/components/responses/AccountNotFound"},"409":{"description":"idempotency_in_progress: The first request with this Idempotency-Key is still running. Retry in a few seconds to get its answer.","x-codes":["idempotency_in_progress"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"idempotency_in_progress","message":"A request with this Idempotency-Key is still being processed. Retry in a few seconds to get its result."}}}}},"413":{"description":"body_too_large: The request body is 1 MB or more.","x-codes":["body_too_large"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"body_too_large","message":"The request body must be under 1 MB."}}}}},"422":{"description":"invalid_request: The body or query failed validation. The message names the first field that failed. recipient_suppressed: One or more recipients are on the suppression list. Nothing was sent; the message names them. idempotency_mismatch: This Idempotency-Key was already used with a different body. Use a new key for each new email.","x-codes":["invalid_request","recipient_suppressed","idempotency_mismatch"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"template.total: Invalid input: expected string, received undefined"}}}}},"429":{"description":"rate_limited: More than 60 requests in a minute from this key, or more than 600 in a minute from all of the account's keys together, on any endpoints. Wait for the window to pass. quota_exceeded: The daily quota of the account, or of its parent, is used up. It resets at midnight UTC. sandbox_limit: The account has made 1000 sandbox sends in the last 24 hours.","x-codes":["rate_limited","quota_exceeded","sandbox_limit"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Too many requests. The limit is 60 a minute per key."}}}}},"502":{"description":"send_failed: Barua's mail server did not accept the message. The send is logged with status failed. Not filed under the Idempotency-Key, so a retry with the same key is a fresh attempt.","x-codes":["send_failed"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"send_failed","message":"The mail could not be handed to the mail system. Try again; if it keeps failing, the failure is on our side."}}}}}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl https://barua.tz/api/v1/emails \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Idempotency-Key: receipt-R-1042\" \\\n  -d '{\n    \"from\": \"Duka Langu <receipts@dukalangu.co.tz>\",\n    \"to\": \"neema@gmail.com\",\n    \"template\": {\n      \"name\": \"receipt\",\n      \"receiptNumber\": \"R-1042\",\n      \"total\": \"25,000\",\n      \"paidOn\": \"24 September 2026\",\n      \"paymentMethod\": \"M-Pesa\"\n    },\n    \"sandbox\": true\n  }'"}]},"get":{"operationId":"listEmails","tags":["Emails"],"summary":"List sent emails","x-scope":"emails:read","description":"The send log, newest first. Pages are keyset cursors rather than offsets, so a log that gains rows while you walk it does not shift under you.","parameters":[{"$ref":"#/components/parameters/Account"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"},{"name":"status","in":"query","schema":{"type":"string","enum":["sent","failed","sandboxed"]}},{"name":"since","in":"query","description":"Only sends created at or after this time. ISO 8601 with a zone offset or Z.","schema":{"type":"string","format":"date-time"},"example":"2026-09-01T00:00:00Z"}],"responses":{"200":{"description":"A page of the log.","content":{"application/json":{"schema":{"type":"object","required":["data","nextCursor"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Email"}},"nextCursor":{"type":["string","null"],"description":"Pass this as cursor to get the next page. Null on the last page."}},"example":{"data":[{"id":"9b2e6d7a-3c4f-4e5a-9b1c-2d3e4f5a6b7c","from":"receipts@dukalangu.co.tz","to":["neema@gmail.com"],"subject":"Receipt R-1042: TZS 25,000","template":"receipt","status":"sent","error":null,"createdAt":"2026-09-24T08:15:30.412Z","deliveredAt":"2026-09-24T08:15:33.901Z","bouncedAt":null,"complainedAt":null,"sandbox":false}],"nextCursor":null}}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/AccountNotFound"},"422":{"description":"invalid_request: The body or query failed validation. The message names the first field that failed. invalid_cursor: cursor is not one the emails or suppressions list issued.","x-codes":["invalid_request","invalid_cursor"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"template.total: Invalid input: expected string, received undefined"}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl \"https://barua.tz/api/v1/emails?status=sent&since=2026-09-01T00:00:00Z&limit=25\" \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\""}]}},"/emails/{id}":{"get":{"operationId":"getEmail","tags":["Emails"],"summary":"Get one email with its delivery events","x-scope":"emails:read","description":"The send as logged, plus events: what each receiving server did, per recipient, in the order it happened, from Barua's own mail server log. A sandboxed or failed send has no events.","parameters":[{"$ref":"#/components/parameters/Account"},{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The email and its events.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailWithEvents"},"example":{"id":"9b2e6d7a-3c4f-4e5a-9b1c-2d3e4f5a6b7c","from":"receipts@dukalangu.co.tz","to":["neema@gmail.com"],"subject":"Receipt R-1042: TZS 25,000","template":"receipt","status":"sent","error":null,"createdAt":"2026-09-24T08:15:30.412Z","deliveredAt":"2026-09-24T08:15:33.901Z","bouncedAt":null,"complainedAt":null,"sandbox":false,"events":[{"recipient":"neema@gmail.com","status":"delivered","dsn":"2.0.0","detail":"250 2.0.0 OK 1758701733 a1b2c3d4e5 - gsmtp","occurredAt":"2026-09-24T08:15:33.901Z"}]}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"description":"email_not_found: No email with that id on this account. account_not_found: X-Barua-Account names something that is not a sub-account of this key's account. The same answer as for an id that does not exist.","x-codes":["email_not_found","account_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"email_not_found","message":"No email with that id on this account."}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl https://barua.tz/api/v1/emails/9b2e6d7a-3c4f-4e5a-9b1c-2d3e4f5a6b7c \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\""}]}},"/domains":{"get":{"operationId":"listDomains","tags":["Domains"],"summary":"List domains","x-scope":"domains:read","parameters":[{"$ref":"#/components/parameters/Account"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"A page of domains, newest first.","content":{"application/json":{"schema":{"type":"object","required":["data","nextCursor"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Domain"}},"nextCursor":{"type":["string","null"],"description":"Pass this as cursor to get the next page. Null on the last page."}},"example":{"data":[{"id":"5f0c1a2b-6d7e-4f80-91a2-b3c4d5e6f708","name":"dukalangu.co.tz","status":"verified","ownershipVerified":true,"sending":true,"receiving":true,"direct":true,"records":[{"record":"Ownership","type":"TXT","name":"_barua-verify.dukalangu.co.tz","value":"barua-verify=k3Jx9vQ2mN8pL5wR7tY1uZ4aB6cD0eFg","status":"published"},{"record":"SPF","type":"TXT","name":"dukalangu.co.tz","value":"v=spf1 include:_spf.barua.tz ~all","status":"published"},{"record":"Receiving","type":"MX","name":"dukalangu.co.tz","value":"10 mx.tznova.com","status":"published"},{"record":"Signing","type":"TXT","name":"barua._domainkey.dukalangu.co.tz","value":"v=DKIM1; h=sha256; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...","status":"published"}],"createdAt":"2026-09-24T07:58:12.000Z","lastCheckedAt":"2026-09-24T09:31:40.000Z"}],"nextCursor":null}}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/AccountNotFound"},"422":{"description":"invalid_request: a bad limit, or a cursor this list did not issue.","x-codes":["invalid_request"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"template.total: Invalid input: expected string, received undefined"}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl https://barua.tz/api/v1/domains \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\""}]},"post":{"operationId":"createDomain","tags":["Domains"],"summary":"Connect a domain","x-scope":"domains:write","description":"Registers the name and returns the DNS records to publish. Nothing is checked yet, and the domain cannot send until POST /domains/{id}/verify finds the ownership record published. A name belongs to one account at a time, with one exception: a claim that was never verified and is more than 7 days old is cleared when someone else claims the name, since only whoever controls the DNS can then verify it. Up to 20 domains per account.","parameters":[{"$ref":"#/components/parameters/Account"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"The bare domain: no scheme, no path, no trailing dot. Lower-cased.","example":"dukalangu.co.tz"}}},"example":{"name":"dukalangu.co.tz"}}}},"responses":{"201":{"description":"Created, with every record to publish. All of them are pending.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Domain"},"example":{"id":"5f0c1a2b-6d7e-4f80-91a2-b3c4d5e6f708","name":"dukalangu.co.tz","status":"pending","ownershipVerified":false,"sending":false,"receiving":false,"direct":false,"records":[{"record":"Ownership","type":"TXT","name":"_barua-verify.dukalangu.co.tz","value":"barua-verify=k3Jx9vQ2mN8pL5wR7tY1uZ4aB6cD0eFg","status":"pending"},{"record":"SPF","type":"TXT","name":"dukalangu.co.tz","value":"v=spf1 include:_spf.barua.tz ~all","status":"pending"},{"record":"Receiving","type":"MX","name":"dukalangu.co.tz","value":"10 mx.tznova.com","status":"pending"},{"record":"Signing","type":"TXT","name":"barua._domainkey.dukalangu.co.tz","value":"v=DKIM1; h=sha256; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...","status":"pending"}],"createdAt":"2026-09-24T07:58:12.000Z","lastCheckedAt":"2026-09-24T07:58:12.000Z"}}}},"400":{"description":"invalid_json: The request body could not be parsed as JSON.","x-codes":["invalid_json"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_json","message":"The request body is not valid JSON."}}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/AccountNotFound"},"409":{"description":"domain_exists: The domain is already on this account. domain_unavailable: The name is claimed elsewhere and cannot be added here. Whether it belongs to another Barua account is deliberately not confirmed. A claim that was never verified and is more than a week old is cleared instead, and the name can be claimed. domain_limit: The account already has 20 domains.","x-codes":["domain_exists","domain_unavailable","domain_limit"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"domain_exists","message":"dukalangu.co.tz is already on this account."}}}}},"422":{"description":"invalid_request: The body or query failed validation. The message names the first field that failed.","x-codes":["invalid_request"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"template.total: Invalid input: expected string, received undefined"}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl https://barua.tz/api/v1/domains \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"dukalangu.co.tz\"\n  }'"}]}},"/domains/{id}":{"get":{"operationId":"getDomain","tags":["Domains"],"summary":"Get a domain","x-scope":"domains:read","description":"The domain as stored, with each record's status from the last verification.","parameters":[{"$ref":"#/components/parameters/Account"},{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The domain.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Domain"},"example":{"id":"5f0c1a2b-6d7e-4f80-91a2-b3c4d5e6f708","name":"dukalangu.co.tz","status":"verified","ownershipVerified":true,"sending":true,"receiving":true,"direct":true,"records":[{"record":"Ownership","type":"TXT","name":"_barua-verify.dukalangu.co.tz","value":"barua-verify=k3Jx9vQ2mN8pL5wR7tY1uZ4aB6cD0eFg","status":"published"},{"record":"SPF","type":"TXT","name":"dukalangu.co.tz","value":"v=spf1 include:_spf.barua.tz ~all","status":"published"},{"record":"Receiving","type":"MX","name":"dukalangu.co.tz","value":"10 mx.tznova.com","status":"published"},{"record":"Signing","type":"TXT","name":"barua._domainkey.dukalangu.co.tz","value":"v=DKIM1; h=sha256; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...","status":"published"}],"createdAt":"2026-09-24T07:58:12.000Z","lastCheckedAt":"2026-09-24T09:31:40.000Z"}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"description":"not_found: No domain, sub-account or key with that id on this account. account_not_found: X-Barua-Account names something that is not a sub-account of this key's account. The same answer as for an id that does not exist.","x-codes":["not_found","account_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"No domain with that id on this account."}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl https://barua.tz/api/v1/domains/5f0c1a2b-6d7e-4f80-91a2-b3c4d5e6f708 \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\""}]},"delete":{"operationId":"deleteDomain","tags":["Domains"],"summary":"Remove a domain","x-scope":"domains:write","description":"Refused while the domain still has active mailboxes. Its relay registration is kept, so the name can be connected again later without new DNS records.","parameters":[{"$ref":"#/components/parameters/Account"},{"$ref":"#/components/parameters/Id"}],"responses":{"204":{"description":"Removed."},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"description":"not_found: No domain, sub-account or key with that id on this account. account_not_found: X-Barua-Account names something that is not a sub-account of this key's account. The same answer as for an id that does not exist.","x-codes":["not_found","account_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"No domain with that id on this account."}}}}},"409":{"description":"domain_in_use: The domain still has active mailboxes. Remove them first.","x-codes":["domain_in_use"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"domain_in_use","message":"dukalangu.co.tz still has 2 mailboxes. Remove them first."}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -X DELETE https://barua.tz/api/v1/domains/5f0c1a2b-6d7e-4f80-91a2-b3c4d5e6f708 \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\""}]}},"/domains/{id}/verify":{"post":{"operationId":"verifyDomain","tags":["Domains"],"summary":"Check the domain's DNS now","x-scope":"domains:write","description":"Reads DNS now, stores what it found, and answers synchronously. Run it after publishing records and again whenever you like: a record found gone withdraws what it granted. Ownership is proved by the _barua-verify TXT record; status becomes verified only when it is. sending turns on when ownership is proven and either both the signing and SPF records are published (direct) or the relay still holds the domain. receiving turns on when the MX record is published. unknown in checks means a resolver did not answer, not that anything is wrong.","parameters":[{"$ref":"#/components/parameters/Account"},{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The domain as now stored, with what each check found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainWithChecks"},"example":{"id":"5f0c1a2b-6d7e-4f80-91a2-b3c4d5e6f708","name":"dukalangu.co.tz","status":"verified","ownershipVerified":true,"sending":true,"receiving":true,"direct":true,"records":[{"record":"Ownership","type":"TXT","name":"_barua-verify.dukalangu.co.tz","value":"barua-verify=k3Jx9vQ2mN8pL5wR7tY1uZ4aB6cD0eFg","status":"published"},{"record":"SPF","type":"TXT","name":"dukalangu.co.tz","value":"v=spf1 include:_spf.barua.tz ~all","status":"published"},{"record":"Receiving","type":"MX","name":"dukalangu.co.tz","value":"10 mx.tznova.com","status":"published"},{"record":"Signing","type":"TXT","name":"barua._domainkey.dukalangu.co.tz","value":"v=DKIM1; h=sha256; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...","status":"published"}],"createdAt":"2026-09-24T07:58:12.000Z","lastCheckedAt":"2026-09-24T09:31:40.000Z","checks":{"ownership":"proven","receiving":"published","signing":"published","spf":"published"}}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"description":"not_found: No domain, sub-account or key with that id on this account. account_not_found: X-Barua-Account names something that is not a sub-account of this key's account. The same answer as for an id that does not exist.","x-codes":["not_found","account_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"No domain with that id on this account."}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -X POST https://barua.tz/api/v1/domains/5f0c1a2b-6d7e-4f80-91a2-b3c4d5e6f708/verify \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\""}]}},"/account":{"get":{"operationId":"getAccount","tags":["Accounts"],"summary":"Get the account this request acts for","x-scope":"accounts:read","description":"The key's own account, or the sub-account named in X-Barua-Account: one call to read a customer's standing the same way you read your own.","parameters":[{"$ref":"#/components/parameters/Account"}],"responses":{"200":{"description":"The account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Account"},"example":{"id":"1d2c3b4a-5f6e-4d7c-8b9a-0f1e2d3c4b5a","name":"Duka Langu","parentId":null,"createdAt":"2026-08-01T09:12:00.000Z","sending":{"tier":"healthy","totalSent":1284,"totalBounced":6,"totalComplained":0,"sentToday":37,"dailyQuota":500,"effectiveQuota":500,"suspended":false},"credits":{"balance":3716,"state":"ok","freeUntil":null,"graceDaysLeft":0,"graceEmailsLeft":0,"totalPurchased":5000,"totalUsed":1284}}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"description":"not_found: No domain, sub-account or key with that id on this account. account_not_found: X-Barua-Account names something that is not a sub-account of this key's account. The same answer as for an id that does not exist.","x-codes":["not_found","account_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"No domain with that id on this account."}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl https://barua.tz/api/v1/account \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\" \\\n  -H \"X-Barua-Account: 7c9e6679-7425-40de-944b-e07fc1f90ae7\""}]}},"/accounts":{"get":{"operationId":"listAccounts","tags":["Accounts"],"summary":"List sub-accounts","x-scope":"accounts:read","parameters":[{"$ref":"#/components/parameters/Account"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/CursorId"}],"responses":{"200":{"description":"A page of sub-accounts, newest first.","content":{"application/json":{"schema":{"type":"object","required":["data","nextCursor"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Account"}},"nextCursor":{"type":["string","null"],"description":"Pass this as cursor to get the next page. Null on the last page."}},"example":{"data":[{"id":"7c9e6679-7425-40de-944b-e07fc1f90ae7","name":"Mama Ntilie Catering","parentId":"1d2c3b4a-5f6e-4d7c-8b9a-0f1e2d3c4b5a","createdAt":"2026-09-24T10:02:11.000Z","sending":{"tier":"healthy","totalSent":0,"totalBounced":0,"totalComplained":0,"sentToday":0,"dailyQuota":500,"effectiveQuota":500,"suspended":false},"credits":{"balance":0,"state":"trial","freeUntil":null,"graceDaysLeft":3,"graceEmailsLeft":300,"totalPurchased":0,"totalUsed":0}}],"nextCursor":null}}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/AccountNotFound"},"422":{"description":"invalid_request: a bad limit, or a cursor that is not one of this account's sub-accounts. Start again without one.","x-codes":["invalid_request"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"template.total: Invalid input: expected string, received undefined"}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl https://barua.tz/api/v1/accounts \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\""}]},"post":{"operationId":"createAccount","tags":["Accounts"],"summary":"Open a sub-account","x-scope":"accounts:write","description":"One per customer. A sub-account has its own domains, keys, suppression list, quota, reputation and suspension; its sends are paid for with the parent's credits. One level only: a sub-account cannot open sub-accounts, so do not send X-Barua-Account with this call. Up to 200 sub-accounts per account.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":2,"maxLength":80}}},"example":{"name":"Mama Ntilie Catering"}}}},"responses":{"201":{"description":"Opened. It has sent nothing yet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Account"},"example":{"id":"7c9e6679-7425-40de-944b-e07fc1f90ae7","name":"Mama Ntilie Catering","parentId":"1d2c3b4a-5f6e-4d7c-8b9a-0f1e2d3c4b5a","createdAt":"2026-09-24T10:02:11.000Z","sending":{"tier":"healthy","totalSent":0,"totalBounced":0,"totalComplained":0,"sentToday":0,"dailyQuota":500,"effectiveQuota":500,"suspended":false},"credits":{"balance":0,"state":"trial","freeUntil":null,"graceDaysLeft":3,"graceEmailsLeft":300,"totalPurchased":0,"totalUsed":0}}}}},"400":{"description":"invalid_json: The request body could not be parsed as JSON.","x-codes":["invalid_json"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_json","message":"The request body is not valid JSON."}}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"description":"not_found: No domain, sub-account or key with that id on this account. account_not_found: X-Barua-Account names something that is not a sub-account of this key's account. The same answer as for an id that does not exist.","x-codes":["not_found","account_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"No domain with that id on this account."}}}}},"409":{"description":"conflict: A sub-account tried to open a sub-account, or a suspension cannot be lifted from the API: it was placed by Barua rather than through the API, or the sub-account's own bounce and complaint rates still grade as paused. limit_reached: The account already has 200 sub-accounts, or 50 live keys.","x-codes":["conflict","limit_reached"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"conflict","message":"A sub-account cannot have sub-accounts of its own. Create it from the parent account, without X-Barua-Account."}}}}},"422":{"description":"invalid_request: The body or query failed validation. The message names the first field that failed.","x-codes":["invalid_request"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"template.total: Invalid input: expected string, received undefined"}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl https://barua.tz/api/v1/accounts \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"Mama Ntilie Catering\"\n  }'"}]}},"/accounts/{id}":{"get":{"operationId":"getSubAccount","tags":["Accounts"],"summary":"Get a sub-account","x-scope":"accounts:read","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The sub-account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Account"},"example":{"id":"7c9e6679-7425-40de-944b-e07fc1f90ae7","name":"Mama Ntilie Catering","parentId":"1d2c3b4a-5f6e-4d7c-8b9a-0f1e2d3c4b5a","createdAt":"2026-09-24T10:02:11.000Z","sending":{"tier":"healthy","totalSent":0,"totalBounced":0,"totalComplained":0,"sentToday":0,"dailyQuota":500,"effectiveQuota":500,"suspended":false},"credits":{"balance":0,"state":"trial","freeUntil":null,"graceDaysLeft":3,"graceEmailsLeft":300,"totalPurchased":0,"totalUsed":0}}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"description":"not_found: No domain, sub-account or key with that id on this account. account_not_found: X-Barua-Account names something that is not a sub-account of this key's account. The same answer as for an id that does not exist.","x-codes":["not_found","account_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"No domain with that id on this account."}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl https://barua.tz/api/v1/accounts/7c9e6679-7425-40de-944b-e07fc1f90ae7 \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\""}]},"delete":{"operationId":"deleteAccount","tags":["Accounts"],"summary":"Delete a sub-account","x-scope":"accounts:write","description":"Deletes the sub-account and everything under it: keys, domains, send log, counters, credit, webhooks. There is no soft delete and no undo.","parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"204":{"description":"Deleted."},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"description":"not_found: No domain, sub-account or key with that id on this account. account_not_found: X-Barua-Account names something that is not a sub-account of this key's account. The same answer as for an id that does not exist.","x-codes":["not_found","account_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"No domain with that id on this account."}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -X DELETE https://barua.tz/api/v1/accounts/7c9e6679-7425-40de-944b-e07fc1f90ae7 \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\""}]}},"/accounts/{id}/suspend":{"post":{"operationId":"suspendAccount","tags":["Accounts"],"summary":"Stop or restart a sub-account's sending","x-scope":"accounts:write","description":"The reason is not a note to you: it is the exact message the sub-account gets on every refused send, so say what happened and what would lift it. Only a suspension placed through the API can be lifted through the API. One placed by Barua, whether by an operator or by the reputation breaker, and one whose bounce or complaint rates still grade as paused, is refused and pointed at support.","parameters":[{"$ref":"#/components/parameters/Id"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["suspended"],"properties":{"suspended":{"type":"boolean"},"reason":{"type":"string","minLength":15,"maxLength":500,"description":"Required when suspended is true. Shown verbatim to the sub-account."}}},"example":{"suspended":true,"reason":"Invoices bounced at three customers this morning. Lifted once the address list has been checked."}}}},"responses":{"200":{"description":"The sub-account as it now stands.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Account"},"example":{"id":"7c9e6679-7425-40de-944b-e07fc1f90ae7","name":"Mama Ntilie Catering","parentId":"1d2c3b4a-5f6e-4d7c-8b9a-0f1e2d3c4b5a","createdAt":"2026-09-24T10:02:11.000Z","sending":{"tier":"healthy","totalSent":0,"totalBounced":0,"totalComplained":0,"sentToday":0,"dailyQuota":500,"effectiveQuota":500,"suspended":true,"suspendedReason":"Invoices bounced at three customers this morning. Lifted once the address list has been checked."},"credits":{"balance":0,"state":"trial","freeUntil":null,"graceDaysLeft":3,"graceEmailsLeft":300,"totalPurchased":0,"totalUsed":0}}}}},"400":{"description":"invalid_json: The request body could not be parsed as JSON.","x-codes":["invalid_json"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_json","message":"The request body is not valid JSON."}}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"description":"not_found: No domain, sub-account or key with that id on this account. account_not_found: X-Barua-Account names something that is not a sub-account of this key's account. The same answer as for an id that does not exist.","x-codes":["not_found","account_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"No domain with that id on this account."}}}}},"409":{"description":"conflict: the suspension was placed by Barua rather than through the API, or the sub-account's own bounce or complaint rates still grade as paused. Either way it cannot be lifted from the API; write to support@barua.tz.","x-codes":["conflict"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"conflict","message":"A sub-account cannot have sub-accounts of its own. Create it from the parent account, without X-Barua-Account."}}}}},"422":{"description":"invalid_request: The body or query failed validation. The message names the first field that failed.","x-codes":["invalid_request"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"template.total: Invalid input: expected string, received undefined"}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl https://barua.tz/api/v1/accounts/7c9e6679-7425-40de-944b-e07fc1f90ae7/suspend \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"suspended\": true,\n    \"reason\": \"Invoices bounced at three customers this morning. Lifted once the address list has been checked.\"\n  }'"}]}},"/keys":{"get":{"operationId":"listKeys","tags":["Keys"],"summary":"List API keys","x-scope":"keys:read","description":"Every key of the account this request acts for, newest first, revoked ones included.","parameters":[{"$ref":"#/components/parameters/Account"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/CursorId"}],"responses":{"200":{"description":"A page of keys.","content":{"application/json":{"schema":{"type":"object","required":["data","nextCursor"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Key"}},"nextCursor":{"type":["string","null"],"description":"Pass this as cursor to get the next page. Null on the last page."}},"example":{"data":[{"id":"c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f","name":"receipts service","prefix":"barua_3f9a1c","scopes":["emails:send","emails:read"],"createdAt":"2026-09-24T10:05:00.000Z","lastUsedAt":"2026-09-24T11:40:19.000Z","revokedAt":null}],"nextCursor":null}}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/AccountNotFound"},"422":{"description":"invalid_request: a bad limit, or a cursor that is not one of this account's keys.","x-codes":["invalid_request"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"template.total: Invalid input: expected string, received undefined"}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl https://barua.tz/api/v1/keys \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\""}]},"post":{"operationId":"createKey","tags":["Keys"],"summary":"Mint an API key","x-scope":"keys:write","description":"The new key holds the scopes you list, or all of the minting key's scopes when you list none, and can never hold one the minting key lacks. With X-Barua-Account the key belongs to that sub-account, which is how you give a customer a key of their own. The response carries the full key and this is the only time anyone will see it.","parameters":[{"$ref":"#/components/parameters/Account"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":60,"description":"What you will call it in a list."},"scopes":{"type":"array","items":{"type":"string","enum":["emails:send","emails:read","domains:read","domains:write","keys:read","keys:write","accounts:read","accounts:write","webhooks:read","webhooks:write","suppressions:read","suppressions:write","database:read","database:write"]},"minItems":1,"description":"A subset of the minting key's scopes. Defaults to all of them."}}},"example":{"name":"receipts service","scopes":["emails:send","emails:read"]}}}},"responses":{"201":{"description":"Minted. Store key now; it is not shown again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewKey"},"example":{"id":"c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f","name":"receipts service","prefix":"barua_3f9a1c","scopes":["emails:send","emails:read"],"createdAt":"2026-09-24T10:05:00.000Z","key":"barua_3f9a1c7e2b4d6f8a0c1e3b5d7f9a2c4e6b8d0f1a3c5e7b9d"}}}},"400":{"description":"invalid_json: The request body could not be parsed as JSON.","x-codes":["invalid_json"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_json","message":"The request body is not valid JSON."}}}}},"401":{"description":"invalid_key: the minting key was revoked while this request was in flight.","x-codes":["invalid_key"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_key","message":"Missing or unknown API key. Send it as: Authorization: Bearer barua_..."}}}}},"403":{"description":"insufficient_scope: the key lacks keys:write, or asked for a scope it does not hold itself. A key can only hand out scopes it holds.","x-codes":["insufficient_scope"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"insufficient_scope","message":"This key cannot do that. It needs the emails:send scope."}}}}},"404":{"$ref":"#/components/responses/AccountNotFound"},"409":{"description":"limit_reached: The account already has 200 sub-accounts, or 50 live keys.","x-codes":["limit_reached"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"limit_reached","message":"This account already has 50 live keys. Revoke one first."}}}}},"422":{"description":"invalid_request: The body or query failed validation. The message names the first field that failed.","x-codes":["invalid_request"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"template.total: Invalid input: expected string, received undefined"}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl https://barua.tz/api/v1/keys \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"receipts service\",\n    \"scopes\": [\n      \"emails:send\",\n      \"emails:read\"\n    ]\n  }'"}]}},"/keys/{id}":{"delete":{"operationId":"revokeKey","tags":["Keys"],"summary":"Revoke an API key","x-scope":"keys:write","description":"Requests with the key stop working at once. It stays in the list with revokedAt set, so the send log keeps its author. A key may revoke itself, which matters when the leaked key is the only one you have to hand.","parameters":[{"$ref":"#/components/parameters/Account"},{"$ref":"#/components/parameters/Id"}],"responses":{"204":{"description":"Revoked, or already was."},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"description":"not_found: No domain, sub-account or key with that id on this account. account_not_found: X-Barua-Account names something that is not a sub-account of this key's account. The same answer as for an id that does not exist.","x-codes":["not_found","account_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"No domain with that id on this account."}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -X DELETE https://barua.tz/api/v1/keys/c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\""}]}},"/webhooks":{"get":{"operationId":"listWebhooks","tags":["Webhooks"],"summary":"List webhook endpoints","x-scope":"webhooks:read","parameters":[{"$ref":"#/components/parameters/Account"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/CursorId"}],"responses":{"200":{"description":"A page of endpoints, newest first.","content":{"application/json":{"schema":{"type":"object","required":["data","nextCursor"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEndpoint"}},"nextCursor":{"type":["string","null"],"description":"Pass this as cursor to get the next page. Null on the last page."}},"example":{"data":[{"id":"e4d3c2b1-a098-4765-b432-10fedcba9876","url":"https://shop.dukalangu.co.tz/hooks/barua","events":["email.delivered","email.bounced","email.complained"],"active":true,"failureCount":0,"disabledAt":null,"createdAt":"2026-09-24T10:20:00.000Z"}],"nextCursor":null}}}}},"400":{"description":"invalid_request: a bad limit or cursor. This list answers 400 rather than 422 for it.","x-codes":["invalid_request"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"template.total: Invalid input: expected string, received undefined"}}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/AccountNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl https://barua.tz/api/v1/webhooks \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\""}]},"post":{"operationId":"createWebhook","tags":["Webhooks"],"summary":"Create a webhook endpoint","x-scope":"webhooks:write","description":"The URL must be https and point at a public host; it is resolved at creation and refused if any address is private. Up to 20 endpoints per account. The response carries the signing secret, and this is the only time it is shown: only ciphertext is stored and no endpoint reads it back. A lost secret means a new endpoint.","parameters":[{"$ref":"#/components/parameters/Account"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url","events"],"properties":{"url":{"type":"string","format":"uri","minLength":1,"maxLength":2000,"description":"https only."},"events":{"type":"array","items":{"type":"string","enum":["email.delivered","email.bounced","email.deferred","email.complained","ping"]},"minItems":1,"description":"Which events to post. Repeats are dropped."}}},"example":{"url":"https://shop.dukalangu.co.tz/hooks/barua","events":["email.delivered","email.bounced","email.complained"]}}}},"responses":{"201":{"description":"Created. Store secret now; it is not shown again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewWebhookEndpoint"},"example":{"id":"e4d3c2b1-a098-4765-b432-10fedcba9876","url":"https://shop.dukalangu.co.tz/hooks/barua","events":["email.delivered","email.bounced","email.complained"],"active":true,"failureCount":0,"disabledAt":null,"createdAt":"2026-09-24T10:20:00.000Z","secret":"whsec_5Yt2Kq8ZbP0mV7xL3nJ9cW1dR4hF6gT8uA2sE5yB7iO"}}}},"400":{"description":"invalid_json: The request body could not be parsed as JSON.","x-codes":["invalid_json"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_json","message":"The request body is not valid JSON."}}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/AccountNotFound"},"422":{"description":"invalid_request: The body or query failed validation. The message names the first field that failed. invalid_url: The URL cannot be used. A webhook URL must be https, carry no credentials and point at a public host that resolves. A database URL must be postgresql:// with a host and a user, and may not turn TLS off with sslmode=disable. too_many_endpoints: The account already has 20 webhook endpoints.","x-codes":["invalid_request","invalid_url","too_many_endpoints"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"template.total: Invalid input: expected string, received undefined"}}}}},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"description":"not_configured: The server cannot store secrets yet, so neither a webhook secret nor a database URL can be kept.","x-codes":["not_configured"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_configured","message":"Webhook secrets cannot be stored on this server yet."}}}}}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl https://barua.tz/api/v1/webhooks \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"url\": \"https://shop.dukalangu.co.tz/hooks/barua\",\n    \"events\": [\n      \"email.delivered\",\n      \"email.bounced\",\n      \"email.complained\"\n    ]\n  }'"}]}},"/webhooks/{id}":{"get":{"operationId":"getWebhook","tags":["Webhooks"],"summary":"Get a webhook endpoint","x-scope":"webhooks:read","parameters":[{"$ref":"#/components/parameters/Account"},{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The endpoint. Never its secret.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"},"example":{"id":"e4d3c2b1-a098-4765-b432-10fedcba9876","url":"https://shop.dukalangu.co.tz/hooks/barua","events":["email.delivered","email.bounced","email.complained"],"active":true,"failureCount":0,"disabledAt":null,"createdAt":"2026-09-24T10:20:00.000Z"}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"description":"webhook_not_found: No webhook endpoint with that id on this account. account_not_found: X-Barua-Account names something that is not a sub-account of this key's account. The same answer as for an id that does not exist.","x-codes":["webhook_not_found","account_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"webhook_not_found","message":"No webhook endpoint with that id on this account."}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl https://barua.tz/api/v1/webhooks/e4d3c2b1-a098-4765-b432-10fedcba9876 \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\""}]},"delete":{"operationId":"deleteWebhook","tags":["Webhooks"],"summary":"Delete a webhook endpoint","x-scope":"webhooks:write","description":"Stops posting to the URL now and deletes every delivery queued or kept for it.","parameters":[{"$ref":"#/components/parameters/Account"},{"$ref":"#/components/parameters/Id"}],"responses":{"204":{"description":"Deleted."},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"description":"webhook_not_found: No webhook endpoint with that id on this account. account_not_found: X-Barua-Account names something that is not a sub-account of this key's account. The same answer as for an id that does not exist.","x-codes":["webhook_not_found","account_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"webhook_not_found","message":"No webhook endpoint with that id on this account."}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -X DELETE https://barua.tz/api/v1/webhooks/e4d3c2b1-a098-4765-b432-10fedcba9876 \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\""}]}},"/webhooks/{id}/test":{"post":{"operationId":"testWebhook","tags":["Webhooks"],"summary":"Post a ping and wait for the answer","x-scope":"webhooks:write","description":"Posts a ping event now and returns what your server answered, so a signature check can be seen passing or failing here rather than on the first real bounce. A ping that succeeds also switches back on an endpoint that was disabled after 10 consecutive failures. Pings are never retried, and one endpoint accepts one test every 10 seconds.","parameters":[{"$ref":"#/components/parameters/Account"},{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"The ping delivery as it stands after the one attempt.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookTestResult"},"example":{"id":"0f1e2d3c-4b5a-4968-8776-655443322110","status":200,"delivered":true,"error":null}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"description":"webhook_not_found: No webhook endpoint with that id on this account. account_not_found: X-Barua-Account names something that is not a sub-account of this key's account. The same answer as for an id that does not exist.","x-codes":["webhook_not_found","account_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"webhook_not_found","message":"No webhook endpoint with that id on this account."}}}}},"429":{"description":"rate_limited: More than 60 requests in a minute from this key, or more than 600 in a minute from all of the account's keys together, on any endpoints. Wait for the window to pass. ping_cooldown: A test ping was sent to this endpoint in the last ten seconds.","x-codes":["rate_limited","ping_cooldown"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Too many requests. The limit is 60 a minute per key."}}}}}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -X POST https://barua.tz/api/v1/webhooks/e4d3c2b1-a098-4765-b432-10fedcba9876/test \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\""}]}},"/webhooks/{id}/deliveries":{"get":{"operationId":"listWebhookDeliveries","tags":["Webhooks"],"summary":"List deliveries to an endpoint","x-scope":"webhooks:read","description":"What was posted to this endpoint and how it went, newest first: the status your server returned, or the reason it never answered, and when the next try is.","parameters":[{"$ref":"#/components/parameters/Account"},{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/CursorId"}],"responses":{"200":{"description":"A page of deliveries.","content":{"application/json":{"schema":{"type":"object","required":["data","nextCursor"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}},"nextCursor":{"type":["string","null"],"description":"Pass this as cursor to get the next page. Null on the last page."}},"example":{"data":[{"id":"0f1e2d3c-4b5a-4968-8776-655443322110","eventType":"email.bounced","attempts":2,"lastStatus":503,"lastError":"HTTP 503","deliveredAt":null,"nextAttemptAt":"2026-09-24T08:22:05.000Z","createdAt":"2026-09-24T08:16:02.000Z"}],"nextCursor":null}}}}},"400":{"description":"invalid_request: a bad limit or cursor. This list answers 400 rather than 422 for it.","x-codes":["invalid_request"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"template.total: Invalid input: expected string, received undefined"}}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"description":"webhook_not_found: No webhook endpoint with that id on this account. account_not_found: X-Barua-Account names something that is not a sub-account of this key's account. The same answer as for an id that does not exist.","x-codes":["webhook_not_found","account_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"webhook_not_found","message":"No webhook endpoint with that id on this account."}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl \"https://barua.tz/api/v1/webhooks/e4d3c2b1-a098-4765-b432-10fedcba9876/deliveries?limit=10\" \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\""}]}},"/suppressions":{"get":{"operationId":"listSuppressions","tags":["Suppressions"],"summary":"List suppressed addresses","x-scope":"suppressions:read","description":"Newest first. Hard bounces and complaints arrive here on their own; a manual entry is one you added. An outcome recorded under a sub-account lists the address on the sub-account and on its parent.","parameters":[{"$ref":"#/components/parameters/Account"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"},{"name":"address","in":"query","description":"Only this address, lower-cased before comparison. One way to ask whether an address is listed.","schema":{"type":"string","minLength":1,"maxLength":320}}],"responses":{"200":{"description":"A page of the list, newest first.","content":{"application/json":{"schema":{"type":"object","required":["data","nextCursor"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Suppression"}},"nextCursor":{"type":["string","null"],"description":"Pass this as cursor to get the next page. Null on the last page."}},"example":{"data":[{"id":"3a4b5c6d-7e8f-4901-a2b3-c4d5e6f70819","address":"juma@example.co.tz","reason":"bounce","detail":"550 5.1.1 The email account that you tried to reach does not exist","createdAt":"2026-09-24T08:15:41.000Z"}],"nextCursor":null}}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/AccountNotFound"},"422":{"description":"invalid_request: The body or query failed validation. The message names the first field that failed. invalid_cursor: cursor is not one the emails or suppressions list issued.","x-codes":["invalid_request","invalid_cursor"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"template.total: Invalid input: expected string, received undefined"}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl \"https://barua.tz/api/v1/suppressions?address=juma%40example.co.tz\" \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\""}]},"post":{"operationId":"addSuppression","tags":["Suppressions"],"summary":"Add an address by hand","x-scope":"suppressions:write","description":"An upsert: asking twice is not an error, and an address the system already caught becomes a manual entry carrying your note.","parameters":[{"$ref":"#/components/parameters/Account"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["address"],"properties":{"address":{"type":"string","format":"email","description":"Lower-cased before it is stored."},"detail":{"type":"string","maxLength":500,"description":"Why, in your words, for whoever reads the list later."}}},"example":{"address":"juma@example.co.tz","detail":"Asked on WhatsApp on 12 September to stop receiving statements."}}}},"responses":{"201":{"description":"On the list, as a manual entry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Suppression"},"example":{"id":"3a4b5c6d-7e8f-4901-a2b3-c4d5e6f70819","address":"juma@example.co.tz","reason":"manual","detail":"Asked on WhatsApp on 12 September to stop receiving statements.","createdAt":"2026-09-24T08:15:41.000Z"}}}},"400":{"description":"invalid_json: The request body could not be parsed as JSON.","x-codes":["invalid_json"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_json","message":"The request body is not valid JSON."}}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/AccountNotFound"},"422":{"description":"invalid_request: The body or query failed validation. The message names the first field that failed.","x-codes":["invalid_request"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"template.total: Invalid input: expected string, received undefined"}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl https://barua.tz/api/v1/suppressions \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"address\": \"juma@example.co.tz\",\n    \"detail\": \"Asked on WhatsApp on 12 September to stop receiving statements.\"\n  }'"}]},"delete":{"operationId":"removeSuppression","tags":["Suppressions"],"summary":"Remove an address from the list","x-scope":"suppressions:write","description":"A deliberate call rather than a flag on the send: the list protects the sending reputation every customer shares, so sending anyway is a decision someone makes.","parameters":[{"$ref":"#/components/parameters/Account"},{"name":"address","in":"query","required":true,"description":"The address to remove, URL-encoded. Lower-cased before comparison.","schema":{"type":"string","minLength":1,"maxLength":320},"example":"juma@example.co.tz"}],"responses":{"204":{"description":"Removed. Sends to the address are accepted again."},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"description":"suppression_not_found: The address is not on this account's suppression list. account_not_found: X-Barua-Account names something that is not a sub-account of this key's account. The same answer as for an id that does not exist.","x-codes":["suppression_not_found","account_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"suppression_not_found","message":"That address is not on this account's suppression list."}}}}},"422":{"description":"invalid_request: no address was given.","x-codes":["invalid_request"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"template.total: Invalid input: expected string, received undefined"}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -X DELETE \"https://barua.tz/api/v1/suppressions?address=juma%40example.co.tz\" \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\""}]}},"/database":{"get":{"operationId":"getDatabase","tags":["Database"],"summary":"Read the connected database","x-scope":"database:read","description":"Where it is, whether Barua last reached it, and how many writes are waiting on Barua because it could not. Always 200: with no database set, connected is false and the rest is empty.","parameters":[{"$ref":"#/components/parameters/Account"}],"responses":{"200":{"description":"The database as Barua knows it. Never the URL or the password.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DatabaseStatus"},"example":{"connected":true,"host":"ep-quiet-lake-123.eu-central-1.aws.neon.tech","port":5432,"database":"neondb","user":"app","sslMode":"verify","state":"connected","schemaVersion":1,"lastOkAt":"2026-09-24T10:21:00.000Z","lastError":null,"updatedAt":"2026-09-24T09:00:00.000Z","pending":0}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/AccountNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl https://barua.tz/api/v1/database \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\""}]},"post":{"operationId":"connectDatabase","tags":["Database"],"summary":"Connect your own Postgres","x-scope":"database:write","description":"Barua connects to the URL, creates a schema called barua there with its tables, and stores the URL encrypted. From then on every email sent, each delivery outcome, every message received on your domains and its attachments are written to it. Connecting again replaces the stored URL; the old database is left as it is. Private, loopback and reserved hosts are refused, and so is sslmode=disable; use sslmode=no-verify for a server with a self-signed certificate. The URL is never returned.","parameters":[{"$ref":"#/components/parameters/Account"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","minLength":1,"maxLength":2000,"description":"A postgresql:// URL with the host, user, password and database name, as your provider shows it."}}},"example":{"url":"postgresql://app:secret@ep-quiet-lake-123.eu-central-1.aws.neon.tech/neondb?sslmode=require"}}}},"responses":{"200":{"description":"Connected, with the schema installed. state is connected and lastOkAt is now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DatabaseStatus"},"example":{"connected":true,"host":"ep-quiet-lake-123.eu-central-1.aws.neon.tech","port":5432,"database":"neondb","user":"app","sslMode":"verify","state":"connected","schemaVersion":1,"lastOkAt":"2026-09-24T10:21:00.000Z","lastError":null,"updatedAt":"2026-09-24T09:00:00.000Z","pending":0}}}},"400":{"description":"invalid_json: The request body could not be parsed as JSON.","x-codes":["invalid_json"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_json","message":"The request body is not valid JSON."}}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/AccountNotFound"},"422":{"description":"invalid_request: The body or query failed validation. The message names the first field that failed. invalid_url: The URL cannot be used. A webhook URL must be https, carry no credentials and point at a public host that resolves. A database URL must be postgresql:// with a host and a user, and may not turn TLS off with sslmode=disable. blocked_host: The host resolves to a private, loopback or reserved address. Barua connects only to public hosts. database_unreachable: Barua could not connect, log in, or create its schema there. The message says which.","x-codes":["invalid_request","invalid_url","blocked_host","database_unreachable"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"template.total: Invalid input: expected string, received undefined"}}}}},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"description":"not_configured: The server cannot store secrets yet, so neither a webhook secret nor a database URL can be kept.","x-codes":["not_configured"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_configured","message":"Webhook secrets cannot be stored on this server yet."}}}}}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl https://barua.tz/api/v1/database \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"url\": \"postgresql://app:secret@ep-quiet-lake-123.eu-central-1.aws.neon.tech/neondb?sslmode=require\"\n  }'"}]},"delete":{"operationId":"disconnectDatabase","tags":["Database"],"summary":"Disconnect it; nothing in your database is touched","x-scope":"database:write","description":"Barua forgets the URL and stops writing. The barua schema and every row in it stay where they are, because they are yours.","parameters":[{"$ref":"#/components/parameters/Account"}],"responses":{"204":{"description":"Disconnected. Mail is no longer written anywhere but Barua's own metadata."},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"description":"database_not_connected: No database is set on this account. account_not_found: X-Barua-Account names something that is not a sub-account of this key's account. The same answer as for an id that does not exist.","x-codes":["database_not_connected","account_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"database_not_connected","message":"No database is set on this account."}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -X DELETE https://barua.tz/api/v1/database \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\""}]}},"/database/test":{"post":{"operationId":"testDatabase","tags":["Database"],"summary":"Check the connection now","x-scope":"database:write","description":"Connects to the stored URL and reports what happened, so a rotated password or a moved server shows up here rather than on the next write. The status returned is fresh: state, lastOkAt and lastError all reflect this attempt.","parameters":[{"$ref":"#/components/parameters/Account"}],"responses":{"200":{"description":"Reachable, logged in, schema in place.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DatabaseStatus"},"example":{"connected":true,"host":"ep-quiet-lake-123.eu-central-1.aws.neon.tech","port":5432,"database":"neondb","user":"app","sslMode":"verify","state":"connected","schemaVersion":1,"lastOkAt":"2026-09-24T10:21:00.000Z","lastError":null,"updatedAt":"2026-09-24T09:00:00.000Z","pending":0}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"description":"database_not_connected: No database is set on this account. account_not_found: X-Barua-Account names something that is not a sub-account of this key's account. The same answer as for an id that does not exist.","x-codes":["database_not_connected","account_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"database_not_connected","message":"No database is set on this account."}}}}},"422":{"description":"database_unreachable: Barua could not connect, log in, or create its schema there. The message says which. blocked_host: The host resolves to a private, loopback or reserved address. Barua connects only to public hosts.","x-codes":["database_unreachable","blocked_host"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"database_unreachable","message":"Barua could not set up your database: password authentication failed for user \"app\"."}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -X POST https://barua.tz/api/v1/database/test \\\n  -H \"Authorization: Bearer barua_YOUR_KEY\""}]}}},"webhooks":{"email.delivered":{"post":{"summary":"email.delivered: The receiving server accepted the message for this recipient.","description":"Posted once the event is recorded; the dispatcher runs once a minute. Answer any 2xx within 10 seconds. Anything else, or no answer, is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours (6 attempts in all), and 10 consecutive failures at one URL switch the endpoint off until a successful test ping. The response body is never read, redirects are not followed, and the URL is checked again before every attempt: one that has come to resolve to a private address is not posted to.","parameters":[{"name":"X-Barua-Event","in":"header","required":true,"description":"The event type, the same as type in the body.","schema":{"type":"string","enum":["email.delivered","email.bounced","email.deferred","email.complained","ping"]}},{"name":"X-Barua-Delivery","in":"header","required":true,"description":"The delivery id, the same as id in the body. A retry repeats it, so a delivery already handled can be dropped.","schema":{"type":"string","format":"uuid"}},{"name":"X-Barua-Signature","in":"header","required":true,"description":"t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 over `${t}.${rawBody}` with the endpoint's secret. Compare with a constant-time function.","schema":{"type":"string","pattern":"^t=\\d+,v1=[0-9a-f]{64}$"}},{"name":"User-Agent","in":"header","required":true,"schema":{"type":"string","const":"Barua-Webhooks/1"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"},"example":{"id":"0f1e2d3c-4b5a-4968-8776-655443322110","type":"email.delivered","createdAt":"2026-09-24T08:16:02.000Z","data":{"messageId":"1f2e3d4c-5b6a-4798-8a9b-0c1d2e3f4a5b@dukalangu.co.tz","recipient":"juma@example.co.tz","status":"delivered","dsn":"5.1.1","detail":"550 5.1.1 The email account that you tried to reach does not exist","occurredAt":"2026-09-24T08:15:41.000Z","from":"receipts@dukalangu.co.tz"}}}}},"responses":{"2XX":{"description":"Delivered. The endpoint's failure count resets to zero."},"default":{"description":"A failure. Retried on the schedule; a ping is not."}}}},"email.bounced":{"post":{"summary":"email.bounced: The receiving server refused it for good. A 5.x.x code also puts the address on the suppression list.","description":"Posted once the event is recorded; the dispatcher runs once a minute. Answer any 2xx within 10 seconds. Anything else, or no answer, is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours (6 attempts in all), and 10 consecutive failures at one URL switch the endpoint off until a successful test ping. The response body is never read, redirects are not followed, and the URL is checked again before every attempt: one that has come to resolve to a private address is not posted to.","parameters":[{"name":"X-Barua-Event","in":"header","required":true,"description":"The event type, the same as type in the body.","schema":{"type":"string","enum":["email.delivered","email.bounced","email.deferred","email.complained","ping"]}},{"name":"X-Barua-Delivery","in":"header","required":true,"description":"The delivery id, the same as id in the body. A retry repeats it, so a delivery already handled can be dropped.","schema":{"type":"string","format":"uuid"}},{"name":"X-Barua-Signature","in":"header","required":true,"description":"t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 over `${t}.${rawBody}` with the endpoint's secret. Compare with a constant-time function.","schema":{"type":"string","pattern":"^t=\\d+,v1=[0-9a-f]{64}$"}},{"name":"User-Agent","in":"header","required":true,"schema":{"type":"string","const":"Barua-Webhooks/1"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"},"example":{"id":"0f1e2d3c-4b5a-4968-8776-655443322110","type":"email.bounced","createdAt":"2026-09-24T08:16:02.000Z","data":{"messageId":"1f2e3d4c-5b6a-4798-8a9b-0c1d2e3f4a5b@dukalangu.co.tz","recipient":"juma@example.co.tz","status":"bounced","dsn":"5.1.1","detail":"550 5.1.1 The email account that you tried to reach does not exist","occurredAt":"2026-09-24T08:15:41.000Z","from":"receipts@dukalangu.co.tz"}}}}},"responses":{"2XX":{"description":"Delivered. The endpoint's failure count resets to zero."},"default":{"description":"A failure. Retried on the schedule; a ping is not."}}}},"email.deferred":{"post":{"summary":"email.deferred: The receiving server refused it for now and Barua's mail server keeps trying. Normal on its own; only a run of them matters.","description":"Posted once the event is recorded; the dispatcher runs once a minute. Answer any 2xx within 10 seconds. Anything else, or no answer, is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours (6 attempts in all), and 10 consecutive failures at one URL switch the endpoint off until a successful test ping. The response body is never read, redirects are not followed, and the URL is checked again before every attempt: one that has come to resolve to a private address is not posted to.","parameters":[{"name":"X-Barua-Event","in":"header","required":true,"description":"The event type, the same as type in the body.","schema":{"type":"string","enum":["email.delivered","email.bounced","email.deferred","email.complained","ping"]}},{"name":"X-Barua-Delivery","in":"header","required":true,"description":"The delivery id, the same as id in the body. A retry repeats it, so a delivery already handled can be dropped.","schema":{"type":"string","format":"uuid"}},{"name":"X-Barua-Signature","in":"header","required":true,"description":"t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 over `${t}.${rawBody}` with the endpoint's secret. Compare with a constant-time function.","schema":{"type":"string","pattern":"^t=\\d+,v1=[0-9a-f]{64}$"}},{"name":"User-Agent","in":"header","required":true,"schema":{"type":"string","const":"Barua-Webhooks/1"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"},"example":{"id":"0f1e2d3c-4b5a-4968-8776-655443322110","type":"email.deferred","createdAt":"2026-09-24T08:16:02.000Z","data":{"messageId":"1f2e3d4c-5b6a-4798-8a9b-0c1d2e3f4a5b@dukalangu.co.tz","recipient":"juma@example.co.tz","status":"deferred","dsn":"5.1.1","detail":"550 5.1.1 The email account that you tried to reach does not exist","occurredAt":"2026-09-24T08:15:41.000Z","from":"receipts@dukalangu.co.tz"}}}}},"responses":{"2XX":{"description":"Delivered. The endpoint's failure count resets to zero."},"default":{"description":"A failure. Retried on the schedule; a ping is not."}}}},"email.complained":{"post":{"summary":"email.complained: The recipient reported the message as spam. The address goes on the suppression list.","description":"Posted once the event is recorded; the dispatcher runs once a minute. Answer any 2xx within 10 seconds. Anything else, or no answer, is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours (6 attempts in all), and 10 consecutive failures at one URL switch the endpoint off until a successful test ping. The response body is never read, redirects are not followed, and the URL is checked again before every attempt: one that has come to resolve to a private address is not posted to.","parameters":[{"name":"X-Barua-Event","in":"header","required":true,"description":"The event type, the same as type in the body.","schema":{"type":"string","enum":["email.delivered","email.bounced","email.deferred","email.complained","ping"]}},{"name":"X-Barua-Delivery","in":"header","required":true,"description":"The delivery id, the same as id in the body. A retry repeats it, so a delivery already handled can be dropped.","schema":{"type":"string","format":"uuid"}},{"name":"X-Barua-Signature","in":"header","required":true,"description":"t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 over `${t}.${rawBody}` with the endpoint's secret. Compare with a constant-time function.","schema":{"type":"string","pattern":"^t=\\d+,v1=[0-9a-f]{64}$"}},{"name":"User-Agent","in":"header","required":true,"schema":{"type":"string","const":"Barua-Webhooks/1"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"},"example":{"id":"0f1e2d3c-4b5a-4968-8776-655443322110","type":"email.complained","createdAt":"2026-09-24T08:16:02.000Z","data":{"messageId":"1f2e3d4c-5b6a-4798-8a9b-0c1d2e3f4a5b@dukalangu.co.tz","recipient":"juma@example.co.tz","status":"complained","dsn":"5.1.1","detail":"550 5.1.1 The email account that you tried to reach does not exist","occurredAt":"2026-09-24T08:15:41.000Z","from":"receipts@dukalangu.co.tz"}}}}},"responses":{"2XX":{"description":"Delivered. The endpoint's failure count resets to zero."},"default":{"description":"A failure. Retried on the schedule; a ping is not."}}}},"ping":{"post":{"summary":"ping: Sent by POST /webhooks/{id}/test and by nothing else. Never retried.","description":"Posted once the event is recorded; the dispatcher runs once a minute. Answer any 2xx within 10 seconds. Anything else, or no answer, is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours (6 attempts in all), and 10 consecutive failures at one URL switch the endpoint off until a successful test ping. The response body is never read, redirects are not followed, and the URL is checked again before every attempt: one that has come to resolve to a private address is not posted to.","parameters":[{"name":"X-Barua-Event","in":"header","required":true,"description":"The event type, the same as type in the body.","schema":{"type":"string","enum":["email.delivered","email.bounced","email.deferred","email.complained","ping"]}},{"name":"X-Barua-Delivery","in":"header","required":true,"description":"The delivery id, the same as id in the body. A retry repeats it, so a delivery already handled can be dropped.","schema":{"type":"string","format":"uuid"}},{"name":"X-Barua-Signature","in":"header","required":true,"description":"t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 over `${t}.${rawBody}` with the endpoint's secret. Compare with a constant-time function.","schema":{"type":"string","pattern":"^t=\\d+,v1=[0-9a-f]{64}$"}},{"name":"User-Agent","in":"header","required":true,"schema":{"type":"string","const":"Barua-Webhooks/1"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"},"example":{"id":"0f1e2d3c-4b5a-4968-8776-655443322110","type":"ping","createdAt":"2026-09-24T10:21:00.000Z","data":{"endpointId":"e4d3c2b1-a098-4765-b432-10fedcba9876"}}}}},"responses":{"2XX":{"description":"Delivered. The endpoint's failure count resets to zero."},"default":{"description":"A failure. Retried on the schedule; a ping is not."}}}}},"components":{"securitySchemes":{"bearer":{"type":"http","scheme":"bearer","bearerFormat":"barua_ followed by 48 hex characters","description":"An API key from Settings → API, or one minted with POST /keys. Only its SHA-256 is stored; a lost key is revoked and replaced, never recovered."}},"parameters":{"Id":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},"Account":{"name":"X-Barua-Account","in":"header","required":false,"description":"Act for this sub-account instead of the key's own account. It must be a sub-account of the key's account; anything else answers 404 account_not_found. The key's own scopes apply.","schema":{"type":"string","format":"uuid"}},"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"description":"A name for this request, unique per email; an order or receipt number works. A retry with the same name and body within 24 hours gets the first answer back instead of sending twice.","schema":{"type":"string","minLength":1,"maxLength":255},"example":"receipt-R-1042"},"Limit":{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},"Cursor":{"name":"cursor","in":"query","required":false,"description":"The nextCursor of the previous page. Opaque: do not build one.","schema":{"type":"string"}},"CursorId":{"name":"cursor","in":"query","required":false,"description":"The nextCursor of the previous page, which for this list is the id of its last row.","schema":{"type":"string","format":"uuid"}}},"responses":{"InvalidKey":{"description":"invalid_key: No Authorization header, a token that does not start with barua_, or a key that was revoked.","x-codes":["invalid_key"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_key","message":"Missing or unknown API key. Send it as: Authorization: Bearer barua_..."}}}}},"InsufficientScope":{"description":"insufficient_scope: The key lacks the scope the endpoint needs, or tried to mint a key with a scope it does not hold itself.","x-codes":["insufficient_scope"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"insufficient_scope","message":"This key cannot do that. It needs the emails:send scope."}}}}},"AccountNotFound":{"description":"account_not_found: X-Barua-Account names something that is not a sub-account of this key's account. The same answer as for an id that does not exist.","x-codes":["account_not_found"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"account_not_found","message":"No sub-account with that id belongs to this key's account."}}}}},"RateLimited":{"description":"rate_limited: More than 60 requests in a minute from this key, or more than 600 in a minute from all of the account's keys together, on any endpoints. Wait for the window to pass.","x-codes":["rate_limited"],"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Too many requests. The limit is 60 a minute per key."}}}}}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["invalid_key","rate_limited","insufficient_scope","account_not_found","invalid_json","invalid_request","invalid_cursor","invalid_idempotency_key","body_too_large","idempotency_in_progress","idempotency_mismatch","suspended","quota_exceeded","sandbox_limit","no_credits","domain_not_yours","domain_not_ready","recipient_suppressed","send_failed","email_not_found","not_found","domain_exists","domain_unavailable","domain_limit","limit_reached","domain_in_use","conflict","suppression_not_found","not_configured","invalid_url","too_many_endpoints","webhook_not_found","ping_cooldown","blocked_host","database_unreachable","database_not_connected"]},"message":{"type":"string","description":"What happened and what would change it, written for the developer reading the log. May change; branch on code."}}}},"example":{"error":{"code":"domain_not_ready","message":"dukalangu.co.tz cannot send yet. Finish its DNS records under Settings → API → Sending domains."}}},"Warning":{"type":"object","required":["code","message"],"description":"Present on a successful send while the paying account is on its free trial (trial) or in the grace window after its credits ran out (credits_exhausted).","properties":{"code":{"type":"string","enum":["trial","credits_exhausted"]},"message":{"type":"string"}}},"LineItem":{"type":"object","required":["description","amount"],"properties":{"description":{"type":"string","minLength":1,"maxLength":200},"quantity":{"type":"integer","minimum":1,"maximum":100000,"description":"Leave out for one-off charges."},"amount":{"type":"string","minLength":1,"maxLength":24,"description":"Already formatted, as you want it to read: \"25,000\", not 25000. The currency goes in the currency field."}}},"OtpTemplate":{"type":"object","required":["name","code"],"properties":{"name":{"type":"string","const":"otp"},"code":{"type":"string","minLength":3,"maxLength":12,"description":"Leads the subject line, so it can be read from a notification banner."},"app":{"type":"string","maxLength":120,"description":"What the code is for. Defaults to your brand name."},"expiresMinutes":{"type":"integer","minimum":1,"maximum":1440,"description":"Mentioned in the email when given."}}},"WelcomeTemplate":{"type":"object","required":["name"],"properties":{"name":{"type":"string","const":"welcome"},"app":{"type":"string","maxLength":120,"description":"Defaults to your brand name."},"userName":{"type":"string","maxLength":120},"message":{"type":"string","maxLength":2000,"description":"Replaces the default welcome line."},"actionUrl":{"type":"string","format":"uri","maxLength":1000,"description":"http or https. Any other scheme is left out of the rendered email rather than linked."},"actionLabel":{"type":"string","maxLength":60,"default":"Get started"}}},"InvoiceTemplate":{"type":"object","required":["name","invoiceNumber","total"],"properties":{"name":{"type":"string","const":"invoice"},"invoiceNumber":{"type":"string","maxLength":120,"minLength":1},"currency":{"type":"string","maxLength":8,"default":"TZS"},"total":{"type":"string","minLength":1,"maxLength":24,"description":"Already formatted, as you want it to read: \"25,000\", not 25000. The currency goes in the currency field."},"dueDate":{"type":"string","maxLength":120,"description":"A string, so it reads the way your customers expect."},"customerName":{"type":"string","maxLength":120},"items":{"type":"array","maxItems":50,"items":{"$ref":"#/components/schemas/LineItem"}},"payUrl":{"type":"string","format":"uri","maxLength":1000,"description":"Adds a Pay now button."},"note":{"type":"string","maxLength":1000}}},"ReceiptTemplate":{"type":"object","required":["name","total"],"properties":{"name":{"type":"string","const":"receipt"},"receiptNumber":{"type":"string","maxLength":120},"currency":{"type":"string","maxLength":8,"default":"TZS"},"total":{"type":"string","minLength":1,"maxLength":24,"description":"Already formatted, as you want it to read: \"25,000\", not 25000. The currency goes in the currency field."},"paidOn":{"type":"string","maxLength":120},"customerName":{"type":"string","maxLength":120},"items":{"type":"array","maxItems":50,"items":{"$ref":"#/components/schemas/LineItem"}},"paymentMethod":{"type":"string","maxLength":120,"description":"For example \"M-Pesa\"."},"note":{"type":"string","maxLength":1000}}},"OrderConfirmationTemplate":{"type":"object","required":["name","orderNumber"],"properties":{"name":{"type":"string","const":"order_confirmation"},"orderNumber":{"type":"string","maxLength":120,"minLength":1},"currency":{"type":"string","maxLength":8,"default":"TZS"},"total":{"type":"string","minLength":1,"maxLength":24,"description":"Already formatted, as you want it to read: \"25,000\", not 25000. The currency goes in the currency field."},"customerName":{"type":"string","maxLength":120},"items":{"type":"array","maxItems":50,"items":{"$ref":"#/components/schemas/LineItem"}},"deliveryEstimate":{"type":"string","maxLength":120},"deliveryAddress":{"type":"string","maxLength":300},"trackUrl":{"type":"string","format":"uri","maxLength":1000,"description":"Adds a Track order button."}}},"PasswordResetTemplate":{"type":"object","required":["name","resetUrl"],"properties":{"name":{"type":"string","const":"password_reset"},"resetUrl":{"type":"string","format":"uri","maxLength":1000,"description":"http or https. Any other scheme is left out of the rendered email rather than linked."},"app":{"type":"string","maxLength":120,"description":"Defaults to your brand name."},"userName":{"type":"string","maxLength":120},"expiresMinutes":{"type":"integer","minimum":1,"maximum":1440,"description":"Mentioned in the email when given."}}},"NotificationTemplate":{"type":"object","required":["name","title","message"],"properties":{"name":{"type":"string","const":"notification"},"title":{"type":"string","minLength":1,"maxLength":200,"description":"Also the subject line."},"message":{"type":"string","minLength":1,"maxLength":2000},"userName":{"type":"string","maxLength":120},"actionUrl":{"type":"string","format":"uri","maxLength":1000,"description":"http or https. Any other scheme is left out of the rendered email rather than linked."},"actionLabel":{"type":"string","maxLength":60,"default":"Open"}}},"Template":{"description":"One of the seven templates, chosen by name. Rendered with your brand from Settings → API.","oneOf":[{"$ref":"#/components/schemas/OtpTemplate"},{"$ref":"#/components/schemas/WelcomeTemplate"},{"$ref":"#/components/schemas/InvoiceTemplate"},{"$ref":"#/components/schemas/ReceiptTemplate"},{"$ref":"#/components/schemas/OrderConfirmationTemplate"},{"$ref":"#/components/schemas/PasswordResetTemplate"},{"$ref":"#/components/schemas/NotificationTemplate"}],"discriminator":{"propertyName":"name","mapping":{"otp":"#/components/schemas/OtpTemplate","welcome":"#/components/schemas/WelcomeTemplate","invoice":"#/components/schemas/InvoiceTemplate","receipt":"#/components/schemas/ReceiptTemplate","order_confirmation":"#/components/schemas/OrderConfirmationTemplate","password_reset":"#/components/schemas/PasswordResetTemplate","notification":"#/components/schemas/NotificationTemplate"}}},"SendRequest":{"type":"object","required":["from","to"],"description":"Send a template, or a subject with html or text.","properties":{"from":{"type":"string","maxLength":320,"description":"Exactly one mailbox: a bare address, or \"Name <address>\" with a name of up to 80 characters. A comma, a second address or a line break is refused, and the header that goes out is rebuilt from the parsed parts. The domain must be connected to this account, ownership-verified and able to send."},"to":{"description":"One address, or an array of up to 10. Lower-cased before use.","oneOf":[{"type":"string","format":"email"},{"type":"array","items":{"type":"string","format":"email"},"minItems":1,"maxItems":10}]},"subject":{"type":"string","minLength":1,"maxLength":300,"description":"Required without a template. With one, it replaces the template's subject."},"html":{"type":"string","maxLength":500000,"description":"Ignored when a template is given."},"text":{"type":"string","maxLength":100000,"description":"Ignored when a template is given. Whichever of html and text you leave out is derived from the other."},"template":{"$ref":"#/components/schemas/Template"},"sandbox":{"type":"boolean","default":false,"description":"Run every check a real send would, log the send with status sandboxed, and send nothing. Nothing is counted against the quota or charged."}}},"SendResult":{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"uuid","description":"The send's id in the log. Fetch it with GET /emails/{id}."},"sandbox":{"type":"boolean","const":true,"description":"Present, and true, only for a sandbox send."},"warning":{"$ref":"#/components/schemas/Warning"}}},"Email":{"type":"object","required":["id","from","to","subject","template","status","error","createdAt","deliveredAt","bouncedAt","complainedAt","sandbox"],"properties":{"id":{"type":"string","format":"uuid"},"from":{"type":"string","description":"The bare address, lower-cased."},"to":{"type":"array","items":{"type":"string"}},"subject":{"type":"string"},"template":{"type":["string","null"],"enum":["otp","welcome","invoice","receipt","order_confirmation","password_reset","notification",null],"description":"Null for a raw send."},"status":{"type":"string","enum":["sent","failed","sandboxed"],"description":"sent means Barua's mail server accepted the message. What receivers did arrives later on deliveredAt, bouncedAt and complainedAt, and on events."},"error":{"type":["string","null"],"description":"The mail system's own words, for a failed send."},"createdAt":{"type":"string","format":"date-time","description":"ISO 8601, in UTC."},"deliveredAt":{"type":["string","null"],"format":"date-time"},"bouncedAt":{"type":["string","null"],"format":"date-time"},"complainedAt":{"type":["string","null"],"format":"date-time"},"sandbox":{"type":"boolean"}}},"DeliveryEvent":{"type":"object","required":["recipient","status","dsn","detail","occurredAt"],"description":"What one receiving server did with the message for one recipient.","properties":{"recipient":{"type":"string"},"status":{"type":"string","enum":["delivered","deferred","bounced","complained"]},"dsn":{"type":["string","null"],"description":"The receiving server's enhanced status code, such as 5.1.1."},"detail":{"type":["string","null"],"description":"What the receiving server said, trimmed."},"occurredAt":{"type":"string","format":"date-time","description":"ISO 8601, in UTC."}}},"EmailWithEvents":{"allOf":[{"$ref":"#/components/schemas/Email"},{"type":"object","required":["events"],"properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/DeliveryEvent"},"description":"In the order they happened. Empty for a sandboxed or failed send."}}}]},"DnsRecord":{"type":"object","required":["record","type","name","value","status"],"description":"One row to publish at your registrar. Barua issues four: Ownership (TXT at _barua-verify.<domain>), SPF (TXT at the domain), Receiving (MX at the domain) and Signing (TXT at barua._domainkey.<domain>). If the relay accepted the domain, its own record appears too.","properties":{"record":{"type":"string","description":"What the row is for: Ownership, SPF, Receiving, Signing, or the relay's own label."},"type":{"type":"string","description":"TXT or MX."},"name":{"type":"string","description":"The host to publish it at."},"value":{"type":"string","description":"Exactly what to publish."},"status":{"type":"string","enum":["published","pending"],"description":"Whether the last verification found it. pending until then."}}},"Domain":{"type":"object","required":["id","name","status","ownershipVerified","sending","receiving","direct","records","createdAt","lastCheckedAt"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"status":{"type":"string","enum":["pending","verified"],"description":"verified once ownership is proven."},"ownershipVerified":{"type":"boolean","description":"The _barua-verify TXT record was found published."},"sending":{"type":"boolean","description":"Whether from-addresses on this domain are accepted by POST /emails."},"receiving":{"type":"boolean","description":"Whether the MX record points at Barua."},"direct":{"type":"boolean","description":"Both the signing and SPF records are published, so mail leaves Barua's own server without the relay."},"records":{"type":"array","items":{"$ref":"#/components/schemas/DnsRecord"}},"createdAt":{"type":"string","format":"date-time","description":"ISO 8601, in UTC."},"lastCheckedAt":{"type":["string","null"],"format":"date-time","description":"When DNS was last read."}}},"DomainChecks":{"type":"object","required":["ownership","receiving","signing","spf"],"description":"What each lookup found, one per question. unknown means a resolver did not answer; check again.","properties":{"ownership":{"type":"string","enum":["proven","absent","unknown"]},"receiving":{"type":"string","enum":["published","missing","unknown"],"description":"The MX record."},"signing":{"type":"string","enum":["published","missing","unknown"],"description":"The DKIM record."},"spf":{"type":"string","enum":["published","missing","unknown"]}}},"DomainWithChecks":{"allOf":[{"$ref":"#/components/schemas/Domain"},{"type":"object","required":["checks"],"properties":{"checks":{"$ref":"#/components/schemas/DomainChecks"}}}]},"Sending":{"type":"object","required":["tier","totalSent","totalBounced","totalComplained","sentToday","dailyQuota","effectiveQuota","suspended"],"properties":{"tier":{"type":"string","enum":["healthy","warning","throttled","paused"],"description":"Graded from lifetime bounce and complaint rates once 50 emails have been sent. throttled cuts the quota to a tenth; paused stops sending."},"totalSent":{"type":"integer","description":"Recipients, not messages."},"totalBounced":{"type":"integer"},"totalComplained":{"type":"integer"},"sentToday":{"type":"integer","description":"Recipients so far today, UTC."},"dailyQuota":{"type":"integer","description":"Recipients a day. 500 to begin with."},"effectiveQuota":{"type":"integer","description":"What is actually allowed today: dailyQuota, or a tenth of it (never below 20) while throttled."},"suspended":{"type":"boolean"},"suspendedReason":{"type":"string","description":"Only present while suspended: the sentence every refused send is told."}}},"Credits":{"type":"object","required":["balance","state","freeUntil","graceDaysLeft","graceEmailsLeft","totalPurchased","totalUsed"],"description":"Counted in emails. On a sub-account this is its own row, which stays at zero because the parent is charged; read the parent's GET /account for the balance that matters.","properties":{"balance":{"type":"integer"},"state":{"type":"string","enum":["free","ok","trial","grace","empty"],"description":"free: an operator's grant, nothing is charged. ok: credit remains. trial: never bought, sending on the 3-day, 300-email window. grace: ran out, same window. empty: sending refused with 402 no_credits."},"freeUntil":{"type":["string","null"],"format":"date-time","description":"When a granted free period ends."},"graceDaysLeft":{"type":"integer"},"graceEmailsLeft":{"type":"integer"},"totalPurchased":{"type":"integer"},"totalUsed":{"type":"integer"}}},"Account":{"type":"object","required":["id","name","parentId","createdAt","sending","credits"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"parentId":{"type":["string","null"],"format":"uuid","description":"The account this one belongs to. Null for a top-level account."},"createdAt":{"type":"string","format":"date-time","description":"ISO 8601, in UTC."},"sending":{"$ref":"#/components/schemas/Sending"},"credits":{"$ref":"#/components/schemas/Credits"}}},"Key":{"type":"object","required":["id","name","prefix","scopes","createdAt","lastUsedAt","revokedAt"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"prefix":{"type":"string","description":"The first 12 characters of the key, for telling keys apart. Never the key."},"scopes":{"type":"array","items":{"type":"string","enum":["emails:send","emails:read","domains:read","domains:write","keys:read","keys:write","accounts:read","accounts:write","webhooks:read","webhooks:write","suppressions:read","suppressions:write","database:read","database:write"]}},"createdAt":{"type":"string","format":"date-time","description":"ISO 8601, in UTC."},"lastUsedAt":{"type":["string","null"],"format":"date-time","description":"Updated at most once an hour."},"revokedAt":{"type":["string","null"],"format":"date-time"}}},"NewKey":{"type":"object","required":["id","name","prefix","scopes","createdAt","key"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"prefix":{"type":"string"},"scopes":{"type":"array","items":{"type":"string","enum":["emails:send","emails:read","domains:read","domains:write","keys:read","keys:write","accounts:read","accounts:write","webhooks:read","webhooks:write","suppressions:read","suppressions:write","database:read","database:write"]}},"createdAt":{"type":"string","format":"date-time","description":"ISO 8601, in UTC."},"key":{"type":"string","pattern":"^barua_[0-9a-f]{48}$","description":"The full key. Shown once."}}},"Suppression":{"type":"object","required":["id","address","reason","detail","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"address":{"type":"string","description":"Lower-cased."},"reason":{"type":"string","enum":["bounce","complaint","manual"],"description":"bounce: a 5.x.x refusal. complaint: a spam report. manual: added through this API or the dashboard."},"detail":{"type":["string","null"],"description":"The receiving server's words, or your own."},"createdAt":{"type":"string","format":"date-time","description":"ISO 8601, in UTC."}}},"DatabaseStatus":{"type":"object","required":["connected","host","port","database","user","sslMode","state","schemaVersion","lastOkAt","lastError","updatedAt","pending"],"properties":{"connected":{"type":"boolean","description":"True when a database is set on this account."},"host":{"type":["string","null"],"description":"The host from the URL, so you can tell which database is connected."},"port":{"type":["integer","null"],"description":"The port from the URL."},"database":{"type":["string","null"],"description":"The database name from the URL."},"user":{"type":["string","null"],"description":"The user Barua connects as. Never the password."},"sslMode":{"type":["string","null"],"enum":["verify","no-verify",null],"description":"verify: TLS with the certificate checked. no-verify: TLS with a self-signed certificate accepted."},"state":{"type":["string","null"],"enum":["connected","failed",null],"description":"connected: the last write or test reached it. failed: it did not, and lastError says why."},"schemaVersion":{"type":["integer","null"],"description":"The version of the barua schema installed there."},"lastOkAt":{"type":["string","null"],"format":"date-time","description":"When Barua last reached it."},"lastError":{"type":["string","null"],"description":"Why the last attempt failed. Null while state is connected."},"updatedAt":{"type":["string","null"],"format":"date-time","description":"When the connection was set or last changed."},"pending":{"type":"integer","description":"Writes waiting on Barua because the database could not be reached. Retried for up to seven days."}}},"WebhookEndpoint":{"type":"object","required":["id","url","events","active","failureCount","disabledAt","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["email.delivered","email.bounced","email.deferred","email.complained","ping"]}},"active":{"type":"boolean","description":"False after 10 consecutive failures. Nothing is posted to an inactive endpoint until a successful test ping."},"failureCount":{"type":"integer","description":"Consecutive failures across every delivery to this URL. Any success resets it."},"disabledAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":"string","format":"date-time","description":"ISO 8601, in UTC."}}},"NewWebhookEndpoint":{"allOf":[{"$ref":"#/components/schemas/WebhookEndpoint"},{"type":"object","required":["secret"],"properties":{"secret":{"type":"string","pattern":"^whsec_","description":"The signing secret. Shown once; a lost secret means a new endpoint."}}}]},"WebhookDelivery":{"type":"object","required":["id","eventType","attempts","lastStatus","lastError","deliveredAt","nextAttemptAt","createdAt"],"properties":{"id":{"type":"string","format":"uuid","description":"Also the id inside the posted body and in X-Barua-Delivery."},"eventType":{"type":"string","enum":["email.delivered","email.bounced","email.deferred","email.complained","ping"]},"attempts":{"type":"integer","description":"Attempts so far, out of 6."},"lastStatus":{"type":["integer","null"],"description":"The HTTP status the last attempt got, if it got one."},"lastError":{"type":["string","null"],"description":"Why the last attempt did not count: HTTP <status> for an answer outside 2xx, or one of timeout, connection_refused, connection_reset, dns_error, tls_error, network_error, blocked_host (the URL now resolves to a private address). Null once delivered."},"deliveredAt":{"type":["string","null"],"format":"date-time"},"nextAttemptAt":{"type":"string","format":"date-time","description":"When the next try is due. Meaningless once delivered or out of attempts."},"createdAt":{"type":"string","format":"date-time","description":"ISO 8601, in UTC."}}},"WebhookTestResult":{"type":"object","required":["id","status","delivered","error"],"properties":{"id":{"type":"string","format":"uuid","description":"The ping's delivery id."},"status":{"type":["integer","null"],"description":"The HTTP status your server answered, or null if it never did."},"delivered":{"type":"boolean"},"error":{"type":["string","null"],"description":"Null when delivered; otherwise the same vocabulary as lastError on a delivery."}}},"EmailEventData":{"type":"object","required":["messageId","recipient","status","dsn","detail","occurredAt","from"],"properties":{"messageId":{"type":"string","description":"The Message-ID of the email, without angle brackets."},"recipient":{"type":"string"},"status":{"type":"string","enum":["delivered","deferred","bounced","complained"]},"dsn":{"type":["string","null"]},"detail":{"type":["string","null"]},"occurredAt":{"type":"string","format":"date-time","description":"ISO 8601, in UTC."},"from":{"type":["string","null"],"description":"The sending address, when known."}}},"PingEventData":{"type":"object","required":["endpointId"],"properties":{"endpointId":{"type":"string","format":"uuid"}}},"WebhookEvent":{"type":"object","required":["id","type","createdAt","data"],"description":"The body of every POST Barua makes to a webhook endpoint.","properties":{"id":{"type":"string","format":"uuid","description":"The delivery id. A retry repeats it."},"type":{"type":"string","enum":["email.delivered","email.bounced","email.deferred","email.complained","ping"]},"createdAt":{"type":"string","format":"date-time","description":"ISO 8601, in UTC."},"data":{"oneOf":[{"$ref":"#/components/schemas/EmailEventData"},{"$ref":"#/components/schemas/PingEventData"}]}},"example":{"id":"0f1e2d3c-4b5a-4968-8776-655443322110","type":"email.bounced","createdAt":"2026-09-24T08:16:02.000Z","data":{"messageId":"1f2e3d4c-5b6a-4798-8a9b-0c1d2e3f4a5b@dukalangu.co.tz","recipient":"juma@example.co.tz","status":"bounced","dsn":"5.1.1","detail":"550 5.1.1 The email account that you tried to reach does not exist","occurredAt":"2026-09-24T08:15:41.000Z","from":"receipts@dukalangu.co.tz"}}}}}}