Skip to content

Contact us enquiries

POST /admin/v1/contact sends a "Contact us" enquiry (upgrade / enterprise / general contact) from the AI Gateway app from the server, by email, to a Myra contact mailbox. It is the server-send backing the Billing & Payments Contact us tab and the Discuss requirements dialog, replacing the old mailto: links that did nothing where the browser had no mail handler configured.

It is a sibling of the Contact Support feedback type and reuses the same mail machinery (message composition, header/line sanitisation, the shared per-user + per-tenant rate window). It differs in one way: the send is synchronous and its outcome is returned, so the form can show the user success or a retryable error.

Authorization

The endpoint is gated on the SUPPORT_CONTACT permission — the same gate as the feedback "Support" option. It is a tenant_admin gate-class permission (held by admin and tenant_admin by default, and delegable by a tenant admin to a custom role). Everyone else is rejected 403. The client hides the form from non-holders (they see "Please talk to your organisation administrator." instead), but that is a UX affordance only — the server is the boundary.

  • No session → 401.
  • Authenticated without SUPPORT_CONTACT → 403 (no mail is sent).

Recipient (server config only)

The recipient is the contact_email setting, falling back to the built-in default mail@myrasecurity.com when no row is set. It is resolved on the server and re-validated; it is never taken from the request and never serialised into any response. A malformed configured value fails closed — the send is refused with 502 contact_unavailable rather than mailing an unknown destination. (The contact mailbox must be provisioned by IT/Ops before go-live; until it exists the relay rejects and each send fails with a surfaced, retryable error.)

Accepted body

Every field is untrusted and validated fail-closed. message is required and must be a substantive enquiry (at least 15 characters after trimming); name / email / url are optional (name and email fall back to the signed-in account). A JSON null or a whitespace-only value counts as absent (not malformed) — and for message, an absent value is rejected exactly like a too-short one (AGF-3115: the form previously accepted an empty enquiry, which bot signups abused to spam the contact mailbox).

Field Accepted Notes
name A string (≤ 200 bytes after scrubbing). Absent → the signed-in account name is used. Display only — see Identity below. A non-string (number, boolean, object) is rejected 400 invalid_contact_field.
email A single valid address local@domain.tld (≤ 254 bytes). Absent → the signed-in account email is used. Display / listed contact detail only. A present but malformed value (bad shape, a control byte, or a non-string) is rejected 400 invalid_contact_email — it never degrades to the absent-case fallback.
message A string, required, at least 15 characters (codepoints) after trimming and ≤ 8000 bytes after scrubbing. The enquiry body. Absent / whitespace-only / shorter than the minimum is rejected 400 contact_message_too_short (AGF-3115). The minimum is measured in codepoints, so a multibyte-language enquiry is not penalised.
url A string (≤ 1024 bytes). The page the enquiry was sent from.

ABSENT and MALFORMED are different answers: an absent email falls back to the account email (a legitimate, defaulted shape), while a present-but-invalid email is rejected — sending garbage can never make the request proceed with a substituted value.

Identity (not client-forgeable)

The authoritative identity in the email — the From: line, the Reply-To, and the audit row — is the signed-in account (or, under impersonation, the acting admin), never client-supplied text. Any name / email the user typed into the form is carried as a separate, clearly-labelled "Contact details the user entered" line in the body. A holder therefore cannot forge the reply target or the apparent sender by editing those fields.

Rate limit

Charged against the shared support-egress budget (the same window as the feedback "Support" type): 3 per user per hour and 10 per tenant per hour. Over the cap → 429 support_rate_limited. The window is consulted after authorization, so an unauthorized caller cannot burn a tenant's budget from behind a 403. A failed send (contact_send_failed / contact_unavailable) still spends a slot — a failed attempt opens a relay session, the exact abuse the window bounds — so "retryable" is bounded by the hourly cap, not unlimited.

Responses

Status Body When
200 { "ok": true, "id": "<contact_id>" } The email was handed to the relay.
400 { "error": …, "code": "invalid_contact_email" } email is present but not a valid address.
400 { "error": …, "code": "invalid_contact_field" } name / message / url is a non-string.
400 { "error": …, "code": "contact_message_too_short" } message is absent, blank, or shorter than 15 characters.
403 { "error": "forbidden" } Caller lacks SUPPORT_CONTACT.
401 — No session.
429 { "error": …, "code": "support_rate_limited" } The support-egress window is spent.
502 { "error": …, "code": "contact_send_failed" } The relay rejected the message or the send threw. The failure is logged and audited (delivered=false); the user can retry.
502 { "error": …, "code": "contact_unavailable" } The configured recipient is invalid, so nothing was sent.

Branch on the top-level code, never on the English error prose (see Error codes).

Egress audit and retention

Every attempt writes an audit_log row (entity_type = "contact_request", action = "contact_request.emailed") recording the recipient, the delivered flag, the acting user (and, under impersonation, whom it was on behalf of) and the tenant — metadata only, never the message content. The enquiry content is not persisted (this endpoint keeps no durable row): a transient contact enquiry that fails to send is surfaced to the user to retry, and the attempt is logged and audited. (A data-subject request keys on audit_log.actor_id / tenant_id.)