Median

Contact form

Props and behavior for MedianContactForm, plus useMedianContact and sendMedianContact for a form of your own.

Updated Oct 1, 20267 minute read
import { MedianContactForm } from "@mediansh/widget";

<MedianContactForm apiKey="median_pk_..." />;

The form sends the same message the support widget would. The agent answers first, and your team can take over in the inbox. Mounted alongside MedianSupport, the two share a visitor, so a person who writes here and later opens the widget finds the same thread.

Props

NameTypeDescription
apiKeystringThe browser-safe median_pk_ id, derived from MEDIAN_KEY with publicKeyFromMedianKey. Required unless identity is set.
identitystringPath to a route built with medianIdentity. The same prop MedianSupport takes. The form shows but stays switched off until the route answers. See Identity.
fieldsMedianContactField[]Fields to ask for besides the email and the message.
userMedianUserThe signed in user, if any. Their email and name fill empty boxes. What the visitor types wins.
telemetrybooleanSend browser context with the message: time zone, language, browser, OS, device, screen, and the page it was sent from. Default: true.
diagnosticsboolean | { errors?: boolean; network?: boolean; console?: boolean }Attach uncaught errors, and optionally failed requests and console.error calls, to the message. Default: false.
appStateRecord<string, string | number | boolean>Facts about your app to send with the message. Read when the form is sent. Sent only when diagnostics is on.
titlestringDefault: "Contact us".
descriptionstringDefault: "We'll reply by email.".
onSent() => voidCalled after the message is delivered, before the thank-you shows. If it throws, the error goes to the console and the thank-you still shows.

MedianUser, telemetry, diagnostics and appState work as they do on MedianSupport. See Context and diagnostics.

Fields

Email and message are always on the form. Add the rest with fields:

<MedianContactForm
  apiKey="median_pk_..."
  fields={[
    { name: "name", label: "Name", required: true },
    { name: "company", label: "Company" },
    {
      name: "topic",
      label: "Topic",
      type: "select",
      options: ["Billing", "A bug", "Something else"],
    },
  ]}
/>;
NameTypeDescription
namestringThe field's key. Three names are reserved: email, message and name.
labelstringShown above the box, and before the answer in the message. Made from name when left out. "order_number" and "orderNumber" both become "Order number".
type"text" | "textarea" | "select"One line, a paragraph, or one choice from a list. Ignored on email and message. Default: "text".
optionsstring[]The choices for a select. A select with no options is shown as a text box.
placeholderstringShown in an empty box. On a select it labels the empty choice, "Choose one" by default.
requiredbooleanMust be filled in. Fields that are not required show "Optional" beside the label. Email and message are always required. Default: false.
RuleWhat happens
OrderFields show in the order given. Email goes first, or right after a name field. Message goes last. Name either one in fields to place it yourself
name: "email" or name: "message"Changes that field's label and placeholder. Nothing else
name: "name"The answer goes on the customer record, not in the message
Any other nameThe answer goes above the message as its own line, such as Topic: Billing
An empty optional fieldLeft out of the message
Two fields with one nameOnly the first is shown
Built-in fieldLabelPlaceholder
EmailEmailyou@example.com
MessageMessageHow can we help?

Validation

The form checks its fields when Send message is pressed, not while the visitor types. Each problem shows under its field, and the first one takes focus. A message clears as soon as the visitor edits that field.

FieldMessage
Email, emptyEnter your email.
Email, not shaped like name@domain.tldEnter a valid email address.
Message, emptyWrite a message.
A required field, emptyRequired.

Enter in a one line field sends the form. Cmd+Enter or Ctrl+Enter sends from a paragraph field.

After sending

  • Send message shows a spinner while the message is on its way.
  • On success the card shows Message sent, a line such as "Replies go to ada@example.com." with the address given, and a Send another button. The card keeps the form's height.
  • Send another brings the form back with the email and name filled in and the other fields empty.
  • On failure the reason shows above the button and every answer stays.
  • The email is saved in the browser. MedianSupport on the same site will not ask for it again, and the form fills it in next time.

The form fills the width of its container and has no width of its own.

A "Powered by Median" line sits beside the button and under the thank-you. Hide it with Show Median branding in Settings → Billing, on a plan that allows it. See Plans.

What your team sees

The form's answers become one message:

Company: Acme
Topic: Billing

We were charged twice for May. Order 4821.
  • If this browser has an open widget conversation, the message joins it. Otherwise it starts a new Live chat conversation in the inbox. A resolved conversation is never reopened.
  • The email and the name go on the customer record. Typed values win over user.
  • With telemetry on, the page URL and browser context come with it. So do diagnostics and data queued with attach(), when you use them.
  • The page URL is kept only when the message starts a new conversation.
  • The agent and your team reply in the thread. The visitor sees the replies when they open MedianSupport in the same browser.

:::warning Replies are copied to the visitor's email only when email support is on for your workspace. See Email. :::

The copy goes out 2 minutes after a reply the visitor has not seen. If they later open the support panel on your site, the panel decides instead, the same as for any widget visitor.

Limits

WhatLimit
Whole message, field lines included4,000 characters. Longer fails with "Messages have to be 4000 characters or fewer."
One line field, including email and name200 characters
Paragraph field4,000 characters
Email on the customer recordTrimmed and cut at 320 characters
Name on the customer recordTrimmed and cut at 80 characters
New conversations and sendsRate limited. Over the limit fails with "Too many requests. Wait a moment and try again." See Rate limits

Your own form

useMedianContact is the same send without the markup:

import { useMedianContact } from "@mediansh/widget";
import { useState } from "react";

function ContactPage() {
  const contact = useMedianContact({ apiKey: "median_pk_..." });
  const [email, setEmail] = useState("");
  const [message, setMessage] = useState("");

  return (
    <form
      onSubmit={async (event) => {
        event.preventDefault();
        await contact.send({ email, message }).catch(() => {});
      }}
    >
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <textarea value={message} onChange={(e) => setMessage(e.target.value)} />
      {contact.error && <p>{contact.error}</p>}
      <button disabled={!contact.isReady || contact.isSending}>Send</button>
    </form>
  );
}

It takes apiKey or identity, plus user, telemetry, diagnostics and appState, as the component does. It needs no provider and no other component on the page.

NameTypeDescription
send(message: MedianContactMessage) => Promise<void>Resolves once the message is delivered. Rejects with an Error whose message is fit to show, and puts the same text in error.
isSendingbooleanA send is in flight.
errorstring | nullWhy the last send failed. Cleared when the next send starts.
isReadybooleanWhether a send can go. False on the server, and until an identity route has answered with the key.
userMedianUser | undefinedWhat the page knows about the visitor. That is the user you passed or the identity route returned, plus the email this browser already gave. Use it to fill your boxes.
showBrandingbooleanWhether to show a "Powered by Median" line. False only when your plan allows hiding it and Show Median branding is off.

send does not check the email or the message. Validate in your form. It fails before sending with these messages:

MessageCause
This form is not connected yet. Try again in a moment.No key yet. The identity route has not answered
Still connecting. Try again in a moment.No browser session yet

The message

send and sendMedianContact take a MedianContactMessage:

NameTypeDescription
emailstringWhere replies go. Saved on the customer record.
messagestringWhat the visitor wrote.
namestringSaved on the customer record beside the email.
fieldsRecord<string, string>Other answers, as label to answer. Each goes above the message as its own line. { Topic: "Billing" } becomes "Topic: Billing". Empty answers are left out.

Without React

sendMedianContact is the same send as a plain function:

import { sendMedianContact } from "@mediansh/widget";

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  try {
    await sendMedianContact(
      { apiKey: "median_pk_..." },
      {
        email: email.value,
        message: message.value,
        fields: { Topic: topic.value },
      },
    );
  } catch (error) {
    notice.textContent = error.message;
  }
});
NameTypeDescription
apiKeystringThe browser-safe median_pk_ id.
userMedianUserThe signed in user, if any. The email and name in the message win over it.
telemetrybooleanSend browser context with the message. Default: true.
  • It takes no identity, diagnostics or appState. Use the hook for those.
  • It does not check the email or the message.
  • Called on the server, it throws "sendMedianContact() was called where there is no browser, so nothing was sent. Call it from the page, after it has loaded."

Both the hook and the function save the email in the browser after a successful send.

To render the ready-made form on a page without React, use mountMedianContactForm with a target. See Without React.

Troubleshooting

You seeCause
The form stays switched off. Console: Median: <MedianContactForm> was given neither apiKey nor identity, so it cannot send. From the hook, it names useMedianContact()Pass apiKey, or identity with the path of your identity route
The form stays switched off. Console: Median: could not read the identity route.The identity route failed and no apiKey was passed. See Identity
Console: Median: the apiKey passed to <MedianContactForm> is "...", which does not look like the browser-safe id from publicKeyFromMedianKey(MEDIAN_KEY). From the hook, it names useMedianContact()Derive apiKey from MEDIAN_KEY with publicKeyFromMedianKey. It starts with median_pk_
"That API key does not match any organization." above the buttonThe key was revoked, or is not a Median key. Derive it from your current MEDIAN_KEY
Console: Median: in the fields passed to <MedianContactForm>, ...A field has no name, shares a name with another, or is a select with no options. The message says which
A reason above the buttonThe send failed. The answers stay, so the visitor can press Send message again

Still need help?

    Esc