Back to Arky

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" },
	},
});
StepWhat happens
chat_messageshows its text and goes straight on
chat_choicewaits for one of its buttons; typed text is refused
chat_questionwaits for an answer of its input_type, saved under the step’s key
collect_email_contactasks for the customer’s email, so the team can also reply by email
send_emailemails a support reply template in the conversation’s language
hand_to_teamthe team takes over
resolveresolves 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_PROMPT and 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; findMessages pages from the newest messages back.
  • A resolved chat 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:

  1. A customer writes to support@northwind.example; it lands in the store’s own inbox as usual.
  2. 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.
  3. Arky files the email into its conversation by its reply headers, or starts one in the receiving address’s language.
  4. 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).
  5. The customer’s answer goes back to support@northwind.example and 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,
});
Note

See the Support API reference for every record and rule.