diff --git a/src/ErrorBoundary.jsx b/src/ErrorBoundary.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..af330d4f3717ec40f0ac360d007365bbec15f18b
--- /dev/null
+++ b/src/ErrorBoundary.jsx
@@ -0,0 +1,27 @@
+// vendor
+import React from "react";
+
+class ErrorBoundary extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = { hasError: false };
+  }
+
+  static getDerivedStateFromError(error) {
+    return { hasError: true };
+  }
+
+  componentDidCatch(error, info) {
+    console.error(error, info.componentStack);
+  }
+
+  render() {
+    if (this.state.hasError) {
+      return this.props.fallback;
+    }
+
+    return this.props.children;
+  }
+}
+
+export default ErrorBoundary;
diff --git a/src/GeneralSettings/Backends/Backend.jsx b/src/GeneralSettings/Backends/Backend.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..1476c07cbe977727adbc304b69b405c80c4d8bef
--- /dev/null
+++ b/src/GeneralSettings/Backends/Backend.jsx
@@ -0,0 +1,149 @@
+// vendor
+import React from "react";
+import { __ } from "@wordpress/i18n";
+import {
+  TextControl,
+  Button,
+  __experimentalSpacer as Spacer,
+} from "@wordpress/components";
+import { useState, useRef, useEffect } from "@wordpress/element";
+
+// source
+import BackendHeaders from "./Headers";
+
+function NewBackend({ add }) {
+  const [name, setName] = useState("");
+  const [baseUrl, setBaseUrl] = useState("https://");
+
+  const onClick = () => add({ name, base_url: baseUrl, headers: [] });
+
+  const disabled = !(name && baseUrl);
+
+  return (
+    <div
+      style={{
+        padding: "calc(24px) calc(32px)",
+        width: "calc(100% - 64px)",
+        backgroundColor: "rgb(245, 245, 245)",
+      }}
+    >
+      <div
+        style={{
+          display: "flex",
+          gap: "1em",
+        }}
+      >
+        <TextControl
+          label={__("Backend name", "wpct-erp-forms")}
+          value={name}
+          onChange={setName}
+          __nextHasNoMarginBottom
+        />
+        <TextControl
+          style={{ minWidth: "250px" }}
+          label={__("Backend base URL", "wpct-erp-forms")}
+          value={baseUrl}
+          onChange={setBaseUrl}
+          __nextHasNoMarginBottom
+        />
+        <Button
+          variant="primary"
+          onClick={() => onClick()}
+          style={{ marginTop: "auto", height: "32px" }}
+          disabled={disabled}
+        >
+          {__("Add", "wpct-erp-forms")}
+        </Button>
+      </div>
+    </div>
+  );
+}
+
+let focus = false;
+export default function Backend({ update, remove, ...data }) {
+  if (data.name === "add") return <NewBackend add={update} />;
+
+  const [name, setName] = useState(data.name);
+  const nameInput = useRef();
+
+  const setHeaders = (headers) => update({ ...data, headers });
+
+  useEffect(() => {
+    if (focus) {
+      nameInput.current.focus();
+    }
+  }, []);
+
+  const timeout = useRef(false);
+  useEffect(() => {
+    if (timeout.current === false) {
+      timeout.current = 0;
+      return;
+    }
+
+    clearTimeout(timeout.current);
+    timeout.current = setTimeout(() => update({ ...data, name }), 500);
+  }, [name]);
+
+  useEffect(() => {
+    timeout.current = false;
+    setName(data.name);
+  }, [data.name]);
+
+  return (
+    <div
+      style={{
+        padding: "calc(24px) calc(32px)",
+        width: "calc(100% - 64px)",
+        backgroundColor: "rgb(245, 245, 245)",
+      }}
+    >
+      <div
+        style={{
+          display: "flex",
+          gap: "1em",
+        }}
+      >
+        <TextControl
+          ref={nameInput}
+          label={__("Backend name", "wpct-erp-forms")}
+          value={name}
+          onChange={setName}
+          onFocus={() => (focus = true)}
+          onBlur={() => (focus = false)}
+          __nextHasNoMarginBottom={true}
+        />
+        <TextControl
+          style={{ minWidth: "250px" }}
+          label={__("Backend base URL", "wpct-erp-forms")}
+          value={data.base_url}
+          onChange={(base_url) => update({ ...data, base_url })}
+          __nextHasNoMarginBottom={true}
+        />
+        <div>
+          <label
+            style={{
+              display: "block",
+              fontWeight: 500,
+              textTransform: "uppercase",
+              fontSize: "11px",
+              marginBottom: "calc(4px)",
+            }}
+          >
+            {__("Remove backend", "wpct-erp-forms")}
+          </label>
+          <Button
+            isDestructive
+            variant="primary"
+            onClick={() => remove(data)}
+            style={{ width: "130px", justifyContent: "center", height: "32px" }}
+          >
+            {__("Remove", "wpct-erp-forms")}
+          </Button>
+        </div>
+      </div>
+      <Spacer paddingY="calc(8px)" />
+      <BackendHeaders headers={data.headers} setHeaders={setHeaders} />
+    </div>
+  );
+}
diff --git a/src/GeneralSettings/Backends/Headers.jsx b/src/GeneralSettings/Backends/Headers.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..62d3161cadf5d204bcc135033b887ae73a529580
--- /dev/null
+++ b/src/GeneralSettings/Backends/Headers.jsx
@@ -0,0 +1,87 @@
+// vendor
+import React from "react";
+import { __ } from "@wordpress/i18n";
+import {
+  TextControl,
+  Button,
+  __experimentalSpacer as Spacer,
+} from "@wordpress/components";
+
+export default function BackendHeaders({ headers, setHeaders }) {
+  const setHeader = (attr, index, value) => {
+    const newHeaders = headers.map((header, i) => {
+      if (index === i) header[attr] = value;
+      return { ...header };
+    });
+
+    setHeaders(newHeaders);
+  };
+
+  const addHeader = () => {
+    const newHeaders = headers.concat([{ name: "", value: "" }]);
+    setHeaders(newHeaders);
+  };
+
+  const dropHeader = (index) => {
+    const newHeaders = headers.slice(0, index).concat(headers.slice(index + 2));
+    setHeaders(newHeaders);
+  };
+
+  return (
+    <div className="components-base-control__label">
+      <label
+        className="components-base-control__label"
+        style={{
+          fontSize: "11px",
+          textTransform: "uppercase",
+          fontWeight: 500,
+          marginBottom: "calc(8px)",
+        }}
+      >
+        {__("Backend HTTP Headers", "wpct-erp-forms")}
+      </label>
+      <table style={{ width: "100%" }}>
+        <tbody>
+          {headers.map(({ name, value }, i) => (
+            <tr key={i}>
+              <td>
+                <TextControl
+                  placeholder={__("Header-Name", "wpct-erp-forms")}
+                  value={name}
+                  onChange={(value) => setHeader("name", i, value)}
+                  __nextHasNoMarginBottom
+                />
+              </td>
+              <td>
+                <TextControl
+                  placeholder={__("Value")}
+                  value={value}
+                  onChange={(value) => setHeader("value", i, value)}
+                  __nextHasNoMarginBottom
+                />
+              </td>
+              <td style={{ borderLeft: "1rem solid transparent" }}>
+                <Button
+                  isDestructive
+                  variant="secondary"
+                  onClick={() => dropHeader(i)}
+                  style={{ height: "32px" }}
+                >
+                  {__("Drop", "wpct-erp-forms")}
+                </Button>
+              </td>
+            </tr>
+          ))}
+        </tbody>
+      </table>
+      <Spacer paddingY="calc(3px)" />
+      <Button
+        variant="secondary"
+        onClick={() => addHeader()}
+        style={{ height: "32px" }}
+      >
+        {__("Add", "wpct-erp-forms")}
+      </Button>
+    </div>
+  );
+}
diff --git a/src/GeneralSettings/Backends/index.jsx b/src/GeneralSettings/Backends/index.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..ad8c82d1136dafc6b662a1072527e96ec2ba9398
--- /dev/null
+++ b/src/GeneralSettings/Backends/index.jsx
@@ -0,0 +1,70 @@
+// vendor
+import React from "react";
+import { __ } from "@wordpress/i18n";
+import { TabPanel } from "@wordpress/components";
+
+// source
+import Backend from "./Backend";
+
+export default function Backends({ backends, setBackends }) {
+  const tabs = backends
+    .map(({ name, base_url, headers }) => ({
+      name,
+      title: name,
+      base_url,
+      headers,
+    }))
+    .concat([
+      {
+        title: __("Add Backend", "wpct-erp-forms"),
+        name: "add",
+      },
+    ]);
+
+  const updateBackend = (index, data) => {
+    if (index === -1) index = backends.length;
+    const newBackends = backends
+      .slice(0, index)
+      .concat([data])
+      .concat(backends.slice(index + 1, backends.length));
+    setBackends(newBackends);
+  };
+
+  const removeBackend = ({ name }) => {
+    const index = backends.findIndex((b) => b.name === name);
+    const newBackends = backends
+      .slice(0, index)
+      .concat(backends.slice(index + 2));
+    setBackends(newBackends);
+  };
+
+  return (
+    <div style={{ width: "100%" }}>
+      <label
+        className="components-base-control__label"
+        style={{
+          fontSize: "11px",
+          textTransform: "uppercase",
+          fontWeight: 500,
+          marginBottom: "calc(8px)",
+        }}
+      >
+        {__("Backends", "wpct-erp-forms")}
+      </label>
+      <TabPanel tabs={tabs}>
+        {(backend) => (
+          <Backend
+            {...backend}
+            remove={removeBackend}
+            update={(newBackend) =>
+              updateBackend(
+                backends.findIndex(({ name }) => name === backend.name),
+                newBackend
+              )
+            }
+          />
+        )}
+      </TabPanel>
+    </div>
+  );
+}
diff --git a/src/GeneralSettings/index.jsx b/src/GeneralSettings/index.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..6be00ca5fadf5e5302d8745da8579cf33c2e7690
--- /dev/null
+++ b/src/GeneralSettings/index.jsx
@@ -0,0 +1,47 @@
+// vendor
+import React from "react";
+import { __ } from "@wordpress/i18n";
+import {
+  Card,
+  CardHeader,
+  CardBody,
+  __experimentalHeading as Heading,
+  PanelRow,
+  TextControl,
+  __experimentalSpacer as Spacer,
+} from "@wordpress/components";
+
+// source
+import { useGeneral } from "../providers/Settings";
+import Backends from "./Backends";
+
+export default function GeneralSettings() {
+  const [{ receiver, backends }, save] = useGeneral();
+
+  const update = (field) => save({ receiver, backends, ...field });
+
+  return (
+    <Card size="large" style={{ height: "fit-content" }}>
+      <CardHeader>
+        <Heading level={3}>{__("General", "wpct-erp-forms")}</Heading>
+      </CardHeader>
+      <CardBody>
+        <PanelRow>
+          <TextControl
+            label={__("Notification receiver", "wpct-erp-forms")}
+            onChange={(receiver) => update({ receiver })}
+            value={receiver}
+            __nextHasNoMarginBottom
+          />
+        </PanelRow>
+        <Spacer paddingY="calc(8px)" />
+        <PanelRow>
+          <Backends
+            backends={backends}
+            setBackends={(backends) => update({ backends })}
+          />
+        </PanelRow>
+      </CardBody>
+    </Card>
+  );
+}
diff --git a/src/Loading.jsx b/src/Loading.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..83421aa74ef910c5204dc02a612f3317201a1c85
--- /dev/null
+++ b/src/Loading.jsx
@@ -0,0 +1,15 @@
+// vendor
+import React from "react";
+import { Animate, Notice } from "@wordpress/components";
+
+export default function Loading({ message }) {
+  return (
+    <Animate type="loading">
+      {(className) => (
+        <Notice className={className} status="success">
+          {message}
+        </Notice>
+      )}
+    </Animate>
+  );
+}
diff --git a/src/RestApiSettings/Forms/Form.jsx b/src/RestApiSettings/Forms/Form.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..cad6f665fc68a662655a7e0600e35276d3204ef0
--- /dev/null
+++ b/src/RestApiSettings/Forms/Form.jsx
@@ -0,0 +1,191 @@
+// vendor
+import React from "react";
+import { __ } from "@wordpress/i18n";
+import { TextControl, SelectControl, Button } from "@wordpress/components";
+import { useState, useRef, useEffect } from "@wordpress/element";
+
+// source
+import { useForms } from "../../providers/Forms";
+import { useGeneral } from "../../providers/Settings";
+
+function NewForm({ add }) {
+  const [{ backends }] = useGeneral();
+  const backendOptions = backends.map(({ name, base_url }) => ({
+    label: name,
+    value: base_url,
+  }));
+  const forms = useForms();
+  const formOptions = forms.map(({ id, title }) => ({
+    label: title,
+    value: id,
+  }));
+
+  const [name, setName] = useState("");
+  const [backend, setBackend] = useState("");
+  const [endpoint, setEndpoint] = useState("");
+  const [formId, setFormId] = useState("");
+
+  const onClick = () => add({ name, backend, endpoint, form_id: formId });
+
+  const disabled = !(name && backend && endpoint && formId);
+
+  return (
+    <div
+      style={{
+        padding: "calc(24px) calc(32px)",
+        width: "calc(100% - 64px)",
+        backgroundColor: "rgb(245, 245, 245)",
+      }}
+    >
+      <div
+        style={{
+          display: "flex",
+          gap: "1em",
+        }}
+      >
+        <TextControl
+          label={__("Bound ID", "wpct-erp-forms")}
+          value={name}
+          onChange={setName}
+          __nextHasNoMarginBottom
+        />
+        <SelectControl
+          label={__("Backend", "wpct-erp-forms")}
+          value={backend}
+          onChange={setBackend}
+          options={backendOptions}
+          __nextHasNoMarginBottom
+        />
+        <TextControl
+          label={__("Endpoint", "wpct-erp-forms")}
+          value={endpoint}
+          onChange={setEndpoint}
+          __nextHasNoMarginBottom
+        />
+        <SelectControl
+          label={__("Form", "wpct-erp-forms")}
+          value={formId}
+          onChange={setFormId}
+          options={formOptions}
+          __nextHasNoMarginBottom
+        />
+        <Button
+          variant="primary"
+          onClick={() => onClick()}
+          style={{ marginTop: "auto", height: "32px" }}
+          disabled={disabled}
+        >
+          {__("Add", "wpct-erp-forms")}
+        </Button>
+      </div>
+    </div>
+  );
+}
+let focus;
+export default function Form({ update, remove, ...data }) {
+  if (data.name === "add") return <NewForm add={update} />;
+
+  const [{ backends }] = useGeneral();
+  const backendOptions = backends.map(({ name, base_url }) => ({
+    label: name,
+    value: base_url,
+  }));
+  const forms = useForms();
+  const formOptions = forms.map(({ id, title }) => ({
+    label: title,
+    value: id,
+  }));
+
+  const [name, setName] = useState(data.name);
+  const nameInput = useRef();
+
+  useEffect(() => {
+    if (focus) {
+      nameInput.current.focus();
+    }
+  }, []);
+
+  const timeout = useRef(false);
+  useEffect(() => {
+    if (timeout.current === false) {
+      timeout.current = 0;
+      return;
+    }
+
+    clearTimeout(timeout.current);
+    timeout.current = setTimeout(() => update({ ...data, name }), 500);
+  }, [name]);
+
+  useEffect(() => {
+    timeout.current = false;
+    setName(data.name);
+  }, [data.name]);
+
+  return (
+    <div
+      style={{
+        padding: "calc(24px) calc(32px)",
+        width: "calc(100% - 64px)",
+        backgroundColor: "rgb(245, 245, 245)",
+      }}
+    >
+      <div
+        style={{
+          display: "flex",
+          gap: "1em",
+        }}
+      >
+        <TextControl
+          ref={nameInput}
+          label={__("Bound ID", "wpct-erp-forms")}
+          value={name}
+          onChange={setName}
+          onFocus={() => (focus = true)}
+          onBlur={() => (focus = false)}
+          __nextHasNoMarginBottom
+        />
+        <SelectControl
+          label={__("Backend", "wpct-erp-forms")}
+          value={data.backend}
+          onChange={(backend) => update({ ...data, backend })}
+          options={backendOptions}
+          __nextHasNoMarginBottom
+        />
+        <TextControl
+          label={__("Endpoint", "wpct-erp-forms")}
+          value={data.endpoint}
+          onChange={(endpoint) => update({ ...data, endpoint })}
+          __nextHasNoMarginBottom
+        />
+        <SelectControl
+          label={__("Form", "wpct-erp-forms")}
+          value={data.form_id}
+          onChange={(form_id) => update({ ...data, form_id })}
+          options={formOptions}
+          __nextHasNoMarginBottom
+        />
+        <div>
+          <label
+            style={{
+              display: "block",
+              fontWeight: 500,
+              textTransform: "uppercase",
+              fontSize: "11px",
+              marginBottom: "calc(4px)",
+            }}
+          >
+            {__("Remove form", "wpct-erp-forms")}
+          </label>
+          <Button
+            isDestructive
+            variant="primary"
+            onClick={() => remove(data)}
+            style={{ width: "130px", justifyContent: "center", height: "32px" }}
+          >
+            {__("Remove", "wpct-erp-forms")}
+          </Button>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/src/RestApiSettings/Forms/index.jsx b/src/RestApiSettings/Forms/index.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..93598ce453c7cb0b3ea66e2fc4efcf740cf6c713
--- /dev/null
+++ b/src/RestApiSettings/Forms/index.jsx
@@ -0,0 +1,72 @@
+// vendor
+import React from "react";
+import { __ } from "@wordpress/i18n";
+import { TabPanel } from "@wordpress/components";
+
+// source
+import Form from "./Form";
+
+export default function Forms({ forms, setForms }) {
+  const tabs = forms
+    .map(({ backend, endpoint, form_id, ref }) => ({
+      name: ref,
+      title: ref,
+      form_id,
+      endpoint,
+      backend,
+    }))
+    .concat([
+      {
+        title: __("Add Form", "wpct-erp-forms"),
+        name: "add",
+      },
+    ]);
+
+  const updateForm = (index, data) => {
+    data = { ...data, ref: data.name };
+    delete data.name;
+
+    if (index === -1) index = forms.length;
+    const newForms = forms
+      .slice(0, index)
+      .concat([data])
+      .concat(forms.slice(index + 1, forms.length));
+    setForms(newForms);
+  };
+
+  const removeForm = ({ name }) => {
+    const index = forms.findIndex((f) => f.ref === name);
+    const newForms = forms.slice(0, index).concat(forms.slice(index + 2));
+    setForms(newForms);
+  };
+
+  return (
+    <div style={{ width: "100%" }}>
+      <label
+        className="components-base-control__label"
+        style={{
+          fontSize: "11px",
+          textTransform: "uppercase",
+          fontWeight: 500,
+          marginBottom: "calc(8px)",
+        }}
+      >
+        {__("Forms", "wpct-erp-forms")}
+      </label>
+      <TabPanel tabs={tabs}>
+        {(form) => (
+          <Form
+            {...form}
+            remove={removeForm}
+            update={(newForm) =>
+              updateForm(
+                forms.findIndex(({ ref }) => ref === form.name),
+                newForm
+              )
+            }
+          />
+        )}
+      </TabPanel>
+    </div>
+  );
+}
diff --git a/src/RestApiSettings/index.jsx b/src/RestApiSettings/index.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..9a74ac85377da911dfa59b034bbccd5fd8c2a6d5
--- /dev/null
+++ b/src/RestApiSettings/index.jsx
@@ -0,0 +1,30 @@
+// vendor
+import React from "react";
+import { __ } from "@wordpress/i18n";
+import {
+  Card,
+  CardHeader,
+  CardBody,
+  __experimentalHeading as Heading,
+  PanelRow,
+} from "@wordpress/components";
+
+// source
+import { useRestApi } from "../providers/Settings";
+import Forms from "./Forms";
+
+export default function RestApiSettings() {
+  const [{ forms }, save] = useRestApi();
+  return (
+    <Card size="large" style={{ height: "fit-content" }}>
+      <CardHeader>
+        <Heading level={3}>{__("REST API", "wpct-erp-forms")}</Heading>
+      </CardHeader>
+      <CardBody>
+        <PanelRow>
+          <Forms forms={forms} setForms={(forms) => save({ forms })} />
+        </PanelRow>
+      </CardBody>
+    </Card>
+  );
+}
diff --git a/src/RpcApiSettings/Forms/Form.jsx b/src/RpcApiSettings/Forms/Form.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..5c55b40c9ee4e489ab05f4cb82f6af1d823b53bc
--- /dev/null
+++ b/src/RpcApiSettings/Forms/Form.jsx
@@ -0,0 +1,192 @@
+// vendor
+import React from "react";
+import { __ } from "@wordpress/i18n";
+import { TextControl, SelectControl, Button } from "@wordpress/components";
+import { useState, useRef, useEffect } from "@wordpress/element";
+
+// source
+import { useForms } from "../../providers/Forms";
+import { useGeneral } from "../../providers/Settings";
+
+function NewForm({ add }) {
+  const [{ backends }] = useGeneral();
+  const backendOptions = backends.map(({ name, base_url }) => ({
+    label: name,
+    value: base_url,
+  }));
+  const forms = useForms();
+  const formOptions = forms.map(({ id, title }) => ({
+    label: title,
+    value: id,
+  }));
+
+  const [name, setName] = useState("");
+  const [backend, setBackend] = useState("");
+  const [model, setModel] = useState("");
+  const [formId, setFormId] = useState("");
+
+  const onClick = () => add({ name, backend, model, form_id: formId });
+
+  const disabled = !(name, backend, model, formId);
+
+  return (
+    <div
+      style={{
+        padding: "calc(24px) calc(32px)",
+        width: "calc(100% - 64px)",
+        backgroundColor: "rgb(245, 245, 245)",
+      }}
+    >
+      <div
+        style={{
+          display: "flex",
+          gap: "1em",
+        }}
+      >
+        <TextControl
+          label={__("Bound ID", "wpct-erp-forms")}
+          value={name}
+          onChange={setName}
+          __nextHasNoMarginBottom
+        />
+        <SelectControl
+          label={__("Backend", "wpct-erp-forms")}
+          value={backend}
+          onChange={setBackend}
+          options={backendOptions}
+          __nextHasNoMarginBottom
+        />
+        <TextControl
+          label={__("Model", "wpct-erp-forms")}
+          value={model}
+          onChange={setModel}
+          __nextHasNoMarginBottom
+        />
+        <SelectControl
+          label={__("Form", "wpct-erp-forms")}
+          value={formId}
+          onChange={setFormId}
+          options={formOptions}
+          __nextHasNoMarginBottom
+        />
+        <Button
+          variant="primary"
+          onClick={() => onClick()}
+          style={{ marginTop: "auto", height: "32px" }}
+          disabled={disabled}
+        >
+          {__("Add", "wpct-erp-forms")}
+        </Button>
+      </div>
+    </div>
+  );
+}
+
+let focus;
+export default function Form({ update, remove, ...data }) {
+  if (data.name === "add") return <NewForm add={update} />;
+
+  const [{ backends }] = useGeneral();
+  const backendOptions = backends.map(({ name, base_url }) => ({
+    label: name,
+    value: base_url,
+  }));
+  const forms = useForms();
+  const formOptions = forms.map(({ id, title }) => ({
+    label: title,
+    value: id,
+  }));
+
+  const [name, setName] = useState(data.name);
+  const nameInput = useRef();
+
+  useEffect(() => {
+    if (focus) {
+      nameInput.current.focus();
+    }
+  }, []);
+
+  const timeout = useRef(false);
+  useEffect(() => {
+    if (timeout.current === false) {
+      timeout.current = 0;
+      return;
+    }
+
+    clearTimeout(timeout.current);
+    timeout.current = setTimeout(() => update({ ...data, name }), 500);
+  }, [name]);
+
+  useEffect(() => {
+    timeout.current = false;
+    setName(data.name);
+  }, [data.name]);
+
+  return (
+    <div
+      style={{
+        padding: "calc(24px) calc(32px)",
+        width: "calc(100% - 64px)",
+        backgroundColor: "rgb(245, 245, 245)",
+      }}
+    >
+      <div
+        style={{
+          display: "flex",
+          gap: "1em",
+        }}
+      >
+        <TextControl
+          ref={nameInput}
+          label={__("Bound ID", "wpct-erp-forms")}
+          value={name}
+          onChange={setName}
+          onFocus={() => (focus = true)}
+          onBlur={() => (focus = false)}
+          __nextHasNoMarginBottom={true}
+        />
+        <SelectControl
+          label={__("Backend", "wpct-erp-forms")}
+          value={data.backend}
+          onChange={(backend) => update({ ...data, backend })}
+          options={backendOptions}
+          __nextHasNoMarginBottom
+        />
+        <TextControl
+          label={__("Model", "wpct-erp-forms")}
+          value={data.model}
+          onChange={(model) => update({ ...data, model })}
+          __nextHasNoMarginBottom={true}
+        />
+        <SelectControl
+          label={__("Form", "wpct-erp-forms")}
+          value={data.form_id}
+          onChange={(form_id) => update({ ...data, form_id })}
+          options={formOptions}
+          __nextHasNoMarginBottom
+        />
+        <div>
+          <label
+            style={{
+              display: "block",
+              fontWeight: 500,
+              textTransform: "uppercase",
+              fontSize: "11px",
+              marginBottom: "calc(4px)",
+            }}
+          >
+            {__("Remove form", "wpct-erp-forms")}
+          </label>
+          <Button
+            isDestructive
+            variant="primary"
+            onClick={() => remove(data)}
+            style={{ width: "130px", justifyContent: "center", height: "32px" }}
+          >
+            {__("Remove", "wpct-erp-forms")}
+          </Button>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/src/RpcApiSettings/Forms/index.jsx b/src/RpcApiSettings/Forms/index.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..0f734c1680e04574d6acc211d2c829a2df6b5d8d
--- /dev/null
+++ b/src/RpcApiSettings/Forms/index.jsx
@@ -0,0 +1,72 @@
+// vendor
+import React from "react";
+import { __ } from "@wordpress/i18n";
+import { TabPanel } from "@wordpress/components";
+
+// source
+import Form from "./Form";
+
+export default function Forms({ forms, setForms }) {
+  const tabs = forms
+    .map(({ backend, model, form_id, ref }) => ({
+      name: ref,
+      title: ref,
+      form_id,
+      model,
+      backend,
+    }))
+    .concat([
+      {
+        title: __("Add Form", "wpct-erp-forms"),
+        name: "add",
+      },
+    ]);
+
+  const updateForm = (index, data) => {
+    data = { ...data, ref: data.name };
+    delete data.name;
+
+    if (index === -1) index = forms.length;
+    const newForms = forms
+      .slice(0, index)
+      .concat([data])
+      .concat(forms.slice(index + 1, forms.length));
+    setForms(newForms);
+  };
+
+  const removeForm = ({ name }) => {
+    const index = forms.findIndex((f) => f.ref === name);
+    const newForms = forms.slice(0, index).concat(forms.slice(index + 2));
+    setForms(newForms);
+  };
+
+  return (
+    <div style={{ width: "100%" }}>
+      <label
+        className="components-base-control__label"
+        style={{
+          fontSize: "11px",
+          textTransform: "uppercase",
+          fontWeight: 500,
+          marginBottom: "calc(8px)",
+        }}
+      >
+        {__("Forms", "wpct-erp-forms")}
+      </label>
+      <TabPanel tabs={tabs}>
+        {(form) => (
+          <Form
+            {...form}
+            remove={removeForm}
+            update={(newForm) =>
+              updateForm(
+                forms.findIndex(({ ref }) => ref === form.name),
+                newForm
+              )
+            }
+          />
+        )}
+      </TabPanel>
+    </div>
+  );
+}
diff --git a/src/RpcApiSettings/index.jsx b/src/RpcApiSettings/index.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..b59ff6749d3aacea1af49aa43e9af3d60268971a
--- /dev/null
+++ b/src/RpcApiSettings/index.jsx
@@ -0,0 +1,69 @@
+// vendor
+import React from "react";
+import { __ } from "@wordpress/i18n";
+import {
+  Card,
+  CardHeader,
+  CardBody,
+  __experimentalHeading as Heading,
+  PanelRow,
+  TextControl,
+  __experimentalSpacer as Spacer,
+} from "@wordpress/components";
+
+// source
+import { useRpcApi } from "../providers/Settings";
+import Forms from "./Forms";
+
+export default function RpcApiSettings() {
+  const [{ endpoint, user, password, database, forms }, save] = useRpcApi();
+
+  const update = (field) =>
+    save({ endpoint, user, password, database, forms, ...field });
+
+  return (
+    <Card size="large" style={{ height: "fit-content" }}>
+      <CardHeader>
+        <Heading level={3}>{__("RPC API", "wpct-erp-forms")}</Heading>
+      </CardHeader>
+      <CardBody>
+        <PanelRow>
+          <TextControl
+            label={__("Endpoint", "wpct-erp-forms")}
+            onChange={(endpoint) => update({ endpoint })}
+            value={endpoint}
+            __nextHasNoMarginBottom
+          />
+        </PanelRow>
+        <PanelRow>
+          <TextControl
+            label={__("Database", "wpct-erp-forms")}
+            onChange={(database) => update({ database })}
+            value={database}
+            __nextHasNoMarginBottom
+          />
+        </PanelRow>
+        <PanelRow>
+          <TextControl
+            label={__("User", "wpct-erp-forms")}
+            onChange={(user) => update({ user })}
+            value={user}
+            __nextHasNoMarginBottom
+          />
+        </PanelRow>
+        <PanelRow>
+          <TextControl
+            label={__("Password", "wpct-erp-forms")}
+            onChange={(password) => update({ password })}
+            value={password}
+            __nextHasNoMarginBottom
+          />
+        </PanelRow>
+        <Spacer paddingY="calc(8px)" />
+        <PanelRow>
+          <Forms forms={forms} setForms={(forms) => update({ forms })} />
+        </PanelRow>
+      </CardBody>
+    </Card>
+  );
+}
diff --git a/src/SettingsPage/index.jsx b/src/SettingsPage/index.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..c67c711e6f8edbea2ce828aa6dd389ccc6a09463
--- /dev/null
+++ b/src/SettingsPage/index.jsx
@@ -0,0 +1,87 @@
+// vendor
+import React from "react";
+import { __ } from "@wordpress/i18n";
+import {
+  TabPanel,
+  __experimentalHeading as Heading,
+  Button,
+  __experimentalSpacer as Spacer,
+} from "@wordpress/components";
+import { useState } from "@wordpress/element";
+
+// source
+import SettingsProvider, { useSubmitSettings } from "../providers/Settings";
+import FormsProvider from "../providers/Forms";
+import GeneralSettings from "../GeneralSettings";
+import RestApiSettings from "../RestApiSettings";
+import RpcApiSettings from "../RpcApiSettings";
+
+const tabs = [
+  {
+    name: "general",
+    title: "General",
+  },
+  {
+    name: "rest-api",
+    title: "REST API",
+  },
+  {
+    name: "rpc-api",
+    title: "RPC API",
+  },
+];
+
+function Content({ tab }) {
+  switch (tab.name) {
+    case "rest-api":
+      return <RestApiSettings />;
+    case "rpc-api":
+      return <RpcApiSettings />;
+    default:
+      return <GeneralSettings />;
+  }
+}
+
+function SaveButton() {
+  const submit = useSubmitSettings();
+
+  const [loading, setLoading] = useState(false);
+  const [error, setError] = useState(false);
+
+  const onClick = () => {
+    setLoading(true);
+    submit()
+      .then(() => setLoading(false))
+      .catch(() => setError(true));
+  };
+
+  return (
+    <Button
+      variant={error ? "secondary" : "primary"}
+      onClick={onClick}
+      style={{ minWidth: "130px", justifyContent: "center" }}
+      disabled={loading}
+      __next40pxDefaultSize
+    >
+      {(error && __("Error")) || __("Save")}
+    </Button>
+  );
+}
+
+export default function SettingsPage() {
+  return (
+    <SettingsProvider>
+      <Heading level={1}>Wpct ERP Forms</Heading>
+      <TabPanel initialTabName="general" tabs={tabs}>
+        {(tab) => (
+          <FormsProvider>
+            <Spacer />
+            <Content tab={tab} />
+          </FormsProvider>
+        )}
+      </TabPanel>
+      <Spacer />
+      <SaveButton />
+    </SettingsProvider>
+  );
+}
diff --git a/src/index.jsx b/src/index.jsx
index 02703e36f3814e51f09e9d591ce9a5907c3878f6..a8db10c8f87309485c847882358746aa560ae6ef 100644
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -1,14 +1,18 @@
+// vendor
+import React from "react";
 import domReady from "@wordpress/dom-ready";
 import { createRoot } from "@wordpress/element";
 
-const SettingsPage = () => {
-  return <div>Placeholder for settings page</div>;
-};
+// source
+import SettingsPage from "./SettingsPage/index.jsx";
+import ErrorBoundary from "./ErrorBoundary.jsx";
 
 domReady(() => {
-  const root = createRoot(
-    document.getElementById("unadorned-announcement-bar-settings")
-  );
+  const root = createRoot(document.getElementById("wpct-erp-forms"));
 
-  root.render(<SettingsPage />);
+  root.render(
+    <ErrorBoundary fallback={<h1>Error</h1>}>
+      <SettingsPage />
+    </ErrorBoundary>
+  );
 });
diff --git a/src/providers/Forms.jsx b/src/providers/Forms.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..4af303c166fc411dc7cd7adc37ee897cd3db44a3
--- /dev/null
+++ b/src/providers/Forms.jsx
@@ -0,0 +1,43 @@
+// vendor
+import React from "react";
+import { __ } from "@wordpress/i18n";
+import apiFetch from "@wordpress/api-fetch";
+import {
+  createContext,
+  useContext,
+  useState,
+  useEffect,
+} from "@wordpress/element";
+
+// source
+import Loading from "../Loading";
+
+const FormsContext = createContext([]);
+
+export default function FormsProvider({ children }) {
+  const [forms, setForms] = useState([]);
+
+  const [loading, setLoading] = useState(true);
+
+  useEffect(() => {
+    apiFetch({
+      path: `${window.wpApiSettings.root}wpct/v1/erp-forms/forms`,
+      headers: {
+        "X-WP-Nonce": wpApiSettings.nonce,
+      },
+    })
+      .then((forms) => setForms(forms))
+      .finally(() => setLoading(false));
+  }, []);
+
+  return (
+    <FormsContext.Provider value={forms}>
+      {(loading && <Loading message={__("Loading", "wpct-erp-forms")} />) ||
+        children}
+    </FormsContext.Provider>
+  );
+}
+
+export function useForms() {
+  return useContext(FormsContext);
+}
diff --git a/src/providers/Settings.jsx b/src/providers/Settings.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..c0864147ef317bcd85c7b472671d9e90952440c4
--- /dev/null
+++ b/src/providers/Settings.jsx
@@ -0,0 +1,120 @@
+// vendor
+import React from "react";
+import { __ } from "@wordpress/i18n";
+import apiFetch from "@wordpress/api-fetch";
+import {
+  createContext,
+  useContext,
+  useState,
+  useEffect,
+} from "@wordpress/element";
+
+// source
+import Loading from "../Loading";
+
+const noop = () => {};
+
+const defaultSettings = {
+  "general": {
+    notification_receiver: `admin@${window.location.hostname}`,
+    backends: [],
+  },
+  "rest-api": {
+    forms: [],
+  },
+  "rpc-api": {
+    endpoint: "/jsonrpc",
+    database: "crm.lead",
+    user: "admin",
+    password: "admin",
+    forms: [],
+  },
+};
+
+const SettingsContext = createContext([defaultSettings, noop]);
+
+export default function SettingsProvider({ children }) {
+  const [general, setGeneral] = useState({ ...defaultSettings.general });
+  const [restApi, setRestApi] = useState({ ...defaultSettings["rest-api"] });
+  const [rpcApi, setRpcApi] = useState({ ...defaultSettings["rpc-api"] });
+
+  const [loading, setLoading] = useState(true);
+
+  useEffect(() => {
+    apiFetch({
+      path: `${window.wpApiSettings.root}wpct/v1/erp-forms/settings`,
+      headers: {
+        "X-WP-Nonce": wpApiSettings.nonce,
+      },
+    })
+      .then((settings) => {
+        setGeneral(settings.general);
+        setRestApi(settings["rest-api"]);
+        setRpcApi(settings["rpc-api"]);
+      })
+      .finally(() => setLoading(false));
+  }, []);
+
+  const saveSettings = () => {
+    return apiFetch({
+      path: `${window.wpApiSettings.root}wpct/v1/erp-forms/settings`,
+      method: "POST",
+      headers: {
+        "X-WP-Nonce": wpApiSettings.nonce,
+      },
+      data: {
+        general,
+        "rest-api": restApi,
+        "rpc-api": rpcApi,
+      },
+    });
+  };
+
+  return (
+    <SettingsContext.Provider
+      value={[
+        {
+          general,
+          setGeneral,
+          restApi,
+          setRestApi,
+          rpcApi,
+          setRpcApi,
+        },
+        saveSettings,
+      ]}
+    >
+      {(loading && <Loading message={__("Loading", "wpct-erp-forms")} />) ||
+        children}
+    </SettingsContext.Provider>
+  );
+}
+
+export function useGeneral() {
+  const [{ general, setGeneral }] = useContext(SettingsContext);
+
+  const { notification_receiver: receiver, backends } = general;
+
+  const update = ({ receiver, backends }) =>
+    setGeneral({
+      notification_receiver: receiver,
+      backends,
+    });
+
+  return [{ receiver, backends }, update];
+}
+
+export function useRestApi() {
+  const [{ restApi, setRestApi }] = useContext(SettingsContext);
+  return [restApi, setRestApi];
+}
+
+export function useRpcApi() {
+  const [{ rpcApi, setRpcApi }] = useContext(SettingsContext);
+  return [rpcApi, setRpcApi];
+}
+
+export function useSubmitSettings() {
+  const [, submit] = useContext(SettingsContext);
+  return submit;
+}