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.
| Setting | Environment variable | Required | What it does |
|---|---|---|---|
NotifyEmailAnnounce 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. |
subjectsSubjects 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. |
thanksAfter sending | — | No | What the storefront says once the form is sent. By default “Thanks — we’ll reply by email.” Panel only. |
enquiriesTake 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
- Install the modulePass 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.
- Say where to announceSet “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.
- Post from the storefrontPOST /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.
- Answer from the inboxCustomers › 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 mailA 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 effortThe 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 captchaThe 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 checkedThe 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 demoA 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 threadDeleting a message deletes its replies with it. There is no undo.
- A link to the product needs PanelURLThe 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.
Source
Everything on this page is read from 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.
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.