Support
Run a fixed-step chat flow on the website, take support email by forwarding, and answer both from one inbox.
Customers reach a store two ways, and Admin shows both in one inbox:
- the chat on the store’s website or app, which can start with a support flow: a fixed set of messages, buttons, questions and steps that hand the conversation to the team;
- email: the store forwards its support address to an Arky receiving address, and the team’s replies go out from the store’s own address.
Every reply comes from a person or from a flow’s fixed steps. Each conversation is a chat, an email thread or both, and runs in one language: the one a chat starts in, the receiving address’s language for an email, or the one staff choose for a thread they start.
1. Write A Flow
A flow is a set of steps. Each step names the step that comes next, and every chat text has one entry per language the chat runs in.
const flow = await sdk.support.flow.create({
store_id: storeId,
id: crypto.randomUUID(),
key: "default",
start_step_key: "greeting",
steps: {
greeting: {
type: "chat_choice",
text: { en: "Hi. Ask us anything, or leave your email." },
buttons: [
{ label: { en: "Ask a question" }, next_step_key: "question" },
{ label: { en: "Talk to us" }, next_step_key: "email" },
],
},
question: {
type: "chat_message",
text: { en: "Type your question below. The team answers here." },
next_step_key: "team",
},
email: {
type: "collect_email_contact",
text: { en: "What's your email?" },
subject: { en: "Your question to Northwind" },
initial_sending_address_id: null,
next_step_key: "thanks",
},
thanks: {
type: "chat_message",
text: { en: "Thanks, {{email}}. A team member will reply here or by email." },
next_step_key: "team",
},
team: { type: "hand_to_team" },
},
});
| Step | What happens |
|---|---|
chat_message | shows its text and goes straight on |
chat_choice | waits for one of its buttons; typed text is refused |
chat_question | waits for an answer of its input_type, saved under the step’s key |
collect_email_contact | asks for the customer’s email, so the team can also reply by email |
send_email | emails a support reply template in the conversation’s language |
hand_to_team | the team takes over |
resolve | resolves the conversation |
collect_email_contact can name the sending address email replies come from; with null, the team
picks one before replying by email. A step has no language of its own: send_email sends its
template in the conversation’s language. A conversation runs the flow it started with: editing the
flow changes only conversations that start afterwards. When a step can’t run, such as a template
without text in the conversation’s language, the team gets the conversation and sees why.
2. Put The Chat On The Site
The storefront starts a chat in one of the store’s languages and names the flow to start with.
Without flow_key the chat goes straight to the team.
import { initialize } from "arky-sdk/storefront";
import { createOrderToken } from "arky-sdk/utils";
const arky = initialize("arky_pk_...", {
locale: "en",
createVisitorIdentity: () => ({
customer_id: crypto.randomUUID(),
session_id: crypto.randomUUID(),
}),
createSessionId: () => crypto.randomUUID(),
createId: () => crypto.randomUUID(),
createOrderToken,
});
const started = await arky.support.conversation.start({
language: "en",
flow_key: "default",
});
localStorage.setItem("support-chat", started.conversation.id);
The conversation id comes from your createId. When the answer is lost, starting the same chat
again (the same language and flow) reuses that id, so the customer gets one conversation. Pass an
id yourself when you want to pick it.
There is no chat token. A chat belongs to the customer of the browser’s session: the SDK starts or resumes that session before the first call, and every later read or message needs only the conversation id. Another browser, or a customer who is signed out of that session, can’t read it.
Show what the conversation waits for:
import type { ConversationStatus, StorefrontConversationMessage } from "arky-sdk";
function prompt(status: ConversationStatus, messages: StorefrontConversationMessage[]) {
if (status.type === "resolved") return null;
if (status.type === "team") return { input: "text", prompt_message_id: null };
const latest = messages
.filter((message) => message.type.type === "flow_choice" || message.type.type === "flow_question")
.at(-1);
if (latest?.type.type === "flow_choice") return { buttons: latest.type.buttons, prompt_message_id: latest.id };
if (latest?.type.type === "flow_question") return { input: latest.type.input_type, prompt_message_id: latest.id };
return null;
}
Send a button press or text and name the prompt it answers:
const shown = prompt(started.conversation.status, started.messages);
const response = await arky.support.conversation.sendMessage({
conversation_id: started.conversation.id,
input: { type: "button", label: "Talk to us" },
prompt_message_id: shown?.prompt_message_id ?? undefined,
});
The message id comes from createId too. When the answer is lost, keep the message on screen and
send the same input for the same prompt again: the SDK reuses the kept id, so the message is added
once. A different message is a new message with a new id. The SDK keeps only the id and a
fingerprint of the message, never its text.
- If the chat moved on since the prompt was shown, the answer is refused with
409 CONVERSATION.STALE_PROMPTand nothing is added: read the conversation again and show the current prompt. - Poll the conversation every few seconds to pick up the team’s replies;
findMessagespages from the newest messages back. - A
resolvedchat takes no more messages (409 CONVERSATION.ENDED): offer a button that starts a new one.
The arky.io website’s own chat widget follows this pattern. Its source is the
ChatWidget.svelte component of the arky.io repository.
3. Take Support Email By Forwarding
Arky doesn’t log into the store’s mailbox. The store forwards its support address to Arky instead, the way Zendesk and Help Scout receive email. Give the address a receiving side, with the language its conversations run in:
const supportAddressId = crypto.randomUUID();
const support = await sdk.notification.emailAddress.create({
store_id: storeId,
id: supportAddressId,
email: "support@northwind.example",
type: {
type: "sending_and_receiving",
sending: { from_name: "Northwind support", reply_to: "support@northwind.example" },
receiving: { language: "en", initial_sending_address_id: supportAddressId, initial_flow_id: null },
},
});
if (support.type.type !== "sending") console.log(support.type.receiving.forwarding_email);
Arky gives the address its own forwarding address on its receiving domain, such as
northwind-7f3a9c01@in.arky.io, which never changes. Then:
- A customer writes to
support@northwind.example; it lands in the store’s own inbox as usual. - The store’s email host forwards a copy to the forwarding address. Set this up once; Gmail first sends a confirmation code to the forwarding address, which arrives as a conversation in Admin.
- Arky files the email into its conversation by its reply headers, or starts one in the receiving address’s language.
- The team replies in Admin; Arky sends the reply as
Northwind support <support@northwind.example>through the conversation’s sending address and the store’s own Resend account, so the store’s domain must be verified there (see Store Email). - The customer’s answer goes back to
support@northwind.exampleand around again, into the same conversation.
A reply typed in the store’s own mailbox goes straight to the customer and isn’t recorded, so the team answers in Admin.
To answer every new email at once, give the address a flow made only of email steps, such as an acknowledgment from a support reply template:
const acknowledgment = await sdk.notification.template.create({
store_id: storeId,
id: crypto.randomUUID(),
key: "support-acknowledgment",
type: { type: "support_reply" },
content: {
en: {
subject: "Re: {{conversation.subject}}",
preheader: null,
body: "<p>Thanks for writing to {{store.name}}. We reply within one working day.</p>",
},
},
});
const emailFlow = await sdk.support.flow.create({
store_id: storeId,
id: crypto.randomUUID(),
key: "email-acknowledgment",
start_step_key: "acknowledge",
steps: {
acknowledge: { type: "send_email", email_template_id: acknowledgment.id, next_step_key: "team" },
team: { type: "hand_to_team" },
},
});
await sdk.notification.emailAddress.update({
store_id: storeId,
id: support.id,
expected_updated_at: support.updated_at,
type: {
type: "sending_and_receiving",
sending: { from_name: "Northwind support", reply_to: "support@northwind.example" },
receiving: { language: "en", initial_sending_address_id: support.id, initial_flow_id: emailFlow.id },
},
});
The acknowledgment answers a new conversation’s first email at most once a day per address, and never answers automatic email, such as out-of-office replies or mailing lists.
4. Answer From The Inbox
const inbox = await sdk.support.conversation.find({
store_id: storeId,
statuses: ["team"],
limit: 25,
});
const conversation = inbox.items[0];
await sdk.support.conversation.reply({
store_id: storeId,
conversation_id: conversation.id,
id: crypto.randomUUID(),
expected_updated_at: conversation.updated_at,
text: "Your order leaves tomorrow morning.",
delivery: conversation.type.type === "chat" ? "chat" : "email",
resolve: true,
});
A reply is delivered by chat, email or chat_and_email; resolve: true sends and resolves in
one step. A customer’s new email on a resolved conversation reopens it.
Instead of typing, staff can answer with a saved support reply template: leave text out, name the
template, and deliver by email. Arky renders the template in the conversation’s language, and both
the email and the message in the conversation come from it:
await sdk.support.conversation.reply({
store_id: storeId,
conversation_id: conversation.id,
id: crypto.randomUUID(),
expected_updated_at: conversation.updated_at,
email_template_id: acknowledgment.id,
delivery: "email",
});
A template without text in the conversation’s language is refused with
409 EMAIL_TEMPLATE.LANGUAGE_MISSING.
5. Start An Email Thread
Staff can write to a customer first, for example about an order. The thread starts with the team and its first message is the staff reply; the customer’s answer joins it like any reply:
await sdk.support.conversation.startEmail({
store_id: storeId,
id: crypto.randomUUID(),
message_id: crypto.randomUUID(),
customer_id: customerId,
email: "ana@example.com",
subject: "About your order 1042",
language: "en",
sending_address_id: support.id,
text: "Your parcel is delayed by a day. Sorry about that.",
});
startEmail also takes email_template_id instead of text, like a reply.
6. Show A Received Email In The Chat
A received email stays out of a customer’s chat. When a conversation has both a chat and an email thread, staff can show one received email in the chat, or hide it again, with the message version they read:
await sdk.support.conversation.changeChatVisibility({
store_id: storeId,
conversation_id: conversation.id,
message_id: receivedMessage.id,
expected_updated_at: receivedMessage.updated_at,
visible_in_chat: true,
});See the Support API reference for every record and rule.