# Contact form API in Go (GoCommerce module)

> A GoCommerce contact form and product enquiries in one inbox: announced by email, answered from the admin, rate-limited, with a honeypot against bots.

- Canonical: https://kitcommerce.store/integrations/contact/
- Last updated: 2026-10-30

---

Platform

## Contact form API in Go

A Go package that gives the storefront a contact form and an “ask about this product” form, and the admin one inbox behind both. Each new message is announced to the store’s own address. An operator answers from the message’s drawer, and the reply goes to the shopper by email through the store’s own provider.

- **Platform** module
- **5** settings
- **10** tests
- **7** API operations
- **ext/contact**

### What it does

In the package’s own words it is “the storefront’s contact form, its product enquiries, and the inbox behind both”. A shopper writes, the store keeps the message, and tells its own address by email when the Plugins screen says where.

Both kinds land in one inbox on Customers › Contact messages. An enquiry is a message marked as one, carrying the product, and the variant and quantity if the shopper named them, as they were called when the shopper asked. Only a product on sale can be asked about. The inbox has three states, new, replied and archived, and a place for notes.

An operator holding contact.reply answers in the message’s drawer. The reply goes to the shopper from the provider’s sender with its reply-to address, so the answer comes back to the shop. Once the provider has taken it, the reply joins the message’s thread with who sent it and when, and the message is marked replied.

### Configuration

Nothing is required. Set the announcement address in Config, which the reference binary reads from CONTACT_EMAIL, or on the Plugins screen, which wins. The form’s wording and the enquiry switch are on the Plugins screen only.

*Contact form module settings — 5 settings, 0 required*

| Setting | Environment variable | Required | What it does |
| --- | --- | --- | --- |
| `NotifyEmail` Announce new messages to | `CONTACT_EMAIL` | No | The store’s own address, told of each new message and enquiry. Without it messages are still kept, and nobody is told. The panel asks for it when Config leaves it empty. |
| `MessagesPerMinute` | — | No | How many messages and enquiries, counted together, one address may send in a minute. Ten when zero; a negative number turns the limit off. Go only; not in the panel. |
| `subjects` Subjects the form offers | — | No | One per line, shown by the storefront as a list. By default: An order, A product, Returns and exchanges, Something else. Panel only. |
| `thanks` After sending | — | No | What the storefront says once the form is sent. By default “Thanks — we’ll reply by email.” Panel only. |
| `enquiries` Take product enquiries | — | No | On by default. Off, the enquiry form answers 404 and the contact form stays open. Panel only. |

The module reads its Config struct, not the environment. The variable names are the ones the package’s own example or the reference binary uses; in your own `main()` you choose where each value comes from. Where a setting has a panel label, the admin’s settings drawer can hold it too, and a value typed there wins over Config.

### Setting it up

1. **Install the module** — Pass contact.New to gocommerce.New, as below, or start the reference binary with -contact and CONTACT_EMAIL set. The Contact form plugin starts switched on.
2. **Say where to announce** — Set “Announce new messages to” on the Plugins screen, or NotifyEmail in Config. Each announcement goes through the store’s email provider and is on the Notifications screen too.
3. **Post from the storefront** — POST /x/contact/messages with a name, an email and the message; POST /x/contact/enquiries with the same and a product_id or product_slug. Leave the website field empty: it is the honeypot.
4. **Answer from the inbox** — Customers › Contact messages lists both kinds, with filters for status and kind. Reply in the drawer, or with POST /api/admin/x/contact/messages/{id}/reply.

main.go

```
import (
	"os"

	"github.com/itswadesh/gocommerce/core"
	"github.com/itswadesh/gocommerce/ext/contact"
)

app, err := gocommerce.New(cfg,
	contact.New(contact.Config{
		NotifyEmail: os.Getenv("CONTACT_EMAIL"),
	}),
)
```

The package doc has no example of its own; this is how the reference binary’s -contact flag and examples/store/main.go install it. cfg is your gocommerce.Config. Import path `github.com/itswadesh/gocommerce/ext/contact`.

### How it works

- **One inbox, two kinds**

  The list filters by status and by kind, message or enquiry. Search matches a fragment of the name, email, subject or product asked about, and an order number exactly, so “42” does not sweep in every order with a 42 in it.

- **The product as it was**

  An enquiry keeps the product’s id, title and slug, and the variant’s name, as they were when the shopper asked, with no foreign key onto the catalogue. A product renamed or deleted since leaves the record as it was. A draft or archived product answers “that product is not on sale here”.

- **Sent first, recorded after**

  A reply goes to the provider before anything is written. Only once the provider takes it is the reply stored and the message marked replied, in one transaction. With no provider the answer is 409, with a refusal 422 and the provider’s words, and the drawer keeps the text.

- **A honeypot and a shared limit**

  A form with the hidden website field filled answers 201 and keeps nothing. Both forms share one limit per store and peer address, ten a minute by default, and every attempt counts, valid or not; past it the answer is 429 with Retry-After.

- **Three rights**

  Reading the inbox needs contact.read, held by managers and staff. Replying needs contact.reply, held by managers. Marking and deleting need contact.write, which no role holds by default, so only owners.

- **Masked where it should be**

  On a public demo store, and for an AI agent’s key, the sender’s email and phone and the replying operator’s address are masked, and search skips the email so it cannot be read back a character at a time.

### What it does not do

Read these before an order depends on it. No store is known to run GoCommerce in production yet, so these come from the code, not from anyone’s experience.

- **The shopper’s answer goes to your mail** — A reply carries the provider’s reply-to address. When the shopper writes back, it lands in that mailbox, not in the message’s thread.
- **Announcements are best effort** — The email to the store’s address does not hold up the form: if the mail backend is down, the message is kept and the shopper is told it was sent, but nobody may hear of it until they open the inbox.
- **A limit, not a captcha** — The limit is held in memory, so each replica counts on its own. It counts the connection’s peer address and never X-Forwarded-For, so behind a reverse proxy every shopper shares the proxy’s allowance. The package says it is no substitute for a captcha on a form that draws real abuse.
- **The order number is not checked** — The contact form’s order number is free text. It is kept and searchable, and not looked up or linked to an order.
- **No replies from a demo** — A store marked as a public demo refuses every reply with 403 demo_frozen, because anybody may sign in to one and anybody may type an address into its storefront.
- **Deleting takes the thread** — Deleting a message deletes its replies with it. There is no undo.
- **A link to the product needs PanelURL** — The enquiry email links to the product in the admin only when the engine’s PanelURL is set. Without it the email names the product and carries no link.

FAQ

### Questions about the Contact form module

**What does the storefront post?**

POST /x/contact/messages takes name, email and body, with an optional phone, subject and order_number. POST /x/contact/enquiries takes name, email and body with product_id or product_slug, and an optional phone, variant_id and quantity. Both answer 201 with the status new.

**What happens with no email provider set up?**

Messages and enquiries are still kept in the inbox. The announcement to the store’s address is best effort, so nobody is told by email. A reply is refused with 409, the drawer says why beside the button, and the text stays in the box.

**Can staff reply to shoppers?**

Not by default. Staff read the inbox with contact.read; replying needs contact.reply, which managers hold. A store that wants staff answering grants it on the Roles screen.

**How is spam kept out?**

A honeypot field no person sees, and one limit for both forms of ten messages a minute from one address. Every attempt counts, so alternating between the two forms does not double the allowance. There is no captcha.

**Can I turn off product enquiries and keep the contact form?**

Yes. Switch off “Take product enquiries” on the Plugins screen and POST /x/contact/enquiries answers 404, while the contact form stays open. Switching off the Contact form plugin closes both.

### Related modules

Each is a directory under `ext/`, imported the same way. Where this one fits in the store: [How modules extend GoCommerce](https://kitcommerce.store/features/development/).

- [Ecommerce FAQ API in Go Give a GoCommerce store a page of questions and answers: grouped into sections, ordered by hand, draft or published, served whole at /x/faq.](https://kitcommerce.store/integrations/faq/)
- [Customer accounts and login in Go Add shopper accounts to a GoCommerce store: sign-up, sign-in, saved addresses, password reset and order history by claim. Guest checkout keeps working.](https://kitcommerce.store/integrations/identity/)
- [Order and GST tax invoices in Go A numbered invoice for every paid GoCommerce order and, for Indian stores, a GST tax invoice with HSN codes and the tax split, exported to TallyPrime.](https://kitcommerce.store/integrations/invoices/)
- [Open-source ecommerce MCP server in Go Let an AI agent operate a GoCommerce store over the Model Context Protocol: 19 tools, per-tool rights, read-only agent keys, a read-only mode and an audit log.](https://kitcommerce.store/integrations/mcp/)

### Source

Everything on this page is read from [`ext/contact`](https://github.com/itswadesh/gocommerce/tree/main/ext/contact) in the GoCommerce repository, MIT licensed. When this page and the code disagree, the code is right and this page is out of date.

- [ext/contact on GitHub](https://github.com/itswadesh/gocommerce/tree/main/ext/contact)
- [All GoCommerce modules](https://kitcommerce.store/integrations/)

### Try it against a store of your own

The backend + admin quick start runs GoCommerce’s API and admin on your own machine, without a storefront. Add this module to it and try it on test orders before a real one depends on it.

[Backend + admin quick start](https://kitcommerce.store/#one-command) · [Read the module](https://github.com/itswadesh/gocommerce/tree/main/ext/contact) · [All integrations](https://kitcommerce.store/integrations/)
