Appearance
Custom Authentication Form
Garnet has 3 way to invite a vendor: either you invite the vendor manually, or via an application form, or via a custom form (you are on the right page).
Learn how to invite vendors
If you want to build your own login UI instead of using the Garnet login iframe, you can use simple HTML forms to authenticate users.
TIP
The code examples below are minimal working examples. You will need to add your own CSS styling to make them production-ready.
To adapt the form to your marketplace:
- Replace
your-storeby your Shopify store id. - If you are using a custom domain, replace
your-store.garnet.centerwith your custom domain in all forms below. - Edit the email notifications to point to your custom pages.
Example of a custom designed onboarding
Example of a custom login form using HTML and CSS:

Login form
html
<form method="POST" action="https://your-store.garnet.center/auth/public/login" target="_blank">
<label for="email">Email:</label>
<input type="email" name="email" required />
<label for="password">Password:</label>
<input type="password" name="password" required />
<input type="hidden" name="redirect" value="/" />
<button>Submit</button>
</form>Password recovery
Password recovery is a two-step process:
Step 1: Request password reset code
This form sends a reset code to the user's email address.
html
<form
method="POST"
action="https://your-store.garnet.center/auth/public/change-password/request"
target="_blank"
>
<label for="email">Email:</label>
<input type="email" name="email" required />
<button>Send reset code</button>
</form>Step 2: Set new password
After receiving the code via email, the user can set a new password.
html
<form
method="POST"
action="https://your-store.garnet.center/auth/public/change-password/update"
target="_blank"
>
<label for="email">Email:</label>
<input type="email" name="email" required />
<label for="code">Code (from email):</label>
<input type="text" name="code" required />
<label for="password">New password:</label>
<input type="password" name="password" minlength="8" required />
<input type="hidden" name="redirect" value="/" />
<button>Update password</button>
</form>Vendor registration
Allow new vendors to register directly on your marketplace. This requires:
- Enable
Custom onboarding forminAdmin Panel > Applications
The email and vendor name must not already exist on the marketplace.
html
<form method="POST" action="https://your-store.garnet.center/api3/public/vendor" id="registrationForm">
<fieldset>
<legend>Account</legend>
<label for="email">Email:</label>
<input type="email" id="email" name="email" required />
<label for="password">Password (min 10 chars):</label>
<input type="password" id="password" name="password" minlength="10" required />
<label for="vendor">Vendor Name:</label>
<input type="text" id="vendor" name="vendor" required />
</fieldset>
<!-- Add profile fields using bracket notation -->
<fieldset>
<legend>Vendor Profile</legend>
<label for="company-name">Company Name:</label>
<input type="text" id="company-name" name="profile[company-name]" />
<label for="phone">Phone:</label>
<input type="tel" id="phone" name="profile[phone]" />
<label for="description">Description:</label>
<textarea id="description" name="profile[description]" rows="3"></textarea>
<label for="instagram">Instagram:</label>
<input type="text" id="instagram" name="profile[instagram]" placeholder="@username" />
</fieldset>
<!-- Bot protection: honeypot field (must stay empty) -->
<input type="text" name="website" style="display:none" tabindex="-1" autocomplete="off" />
<!-- Bot protection: CSRF token -->
<input type="hidden" name="csrfToken" id="csrfToken" />
<button type="submit">Register</button>
</form>
<script>
// Fetch a CSRF token when the page loads
fetch('https://your-store.garnet.center/api3/public/csrf-token', {
headers: { 'x-store': 'your-store' },
})
.then((res) => res.json())
.then((data) => {
document.getElementById('csrfToken').value = data.token;
});
</script>The Vendor Profile fieldset is optional. Profile fields are stored in the vendor's metaobject. The field names must match your vendor profile metafield keys using the profile[] bracket notation (e.g., profile[facebook], profile[company-name]).
If you are using advanced metafields (like image upload), you will need to write additional logic to convert the user image into a valid value for Shopify metafields.
Bot protection
The registration form includes built-in bot protection: a CSRF token with a time check ensures the form was loaded before submission, and a hidden honeypot field catches automated bots. No external service or configuration is needed.
API reference
If you want to handle form submissions with JavaScript instead of using target="_blank", you can submit requests via fetch and display success or error messages inline. All endpoints below accept application/json and return JSON error responses.
POST /auth/public/login
Authenticates a vendor and sets session cookies.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
email | string | yes | Vendor email address |
password | string | yes | Password (minimum 8 characters) |
redirect | string | no | Path to redirect to after login (e.g. /) |
Success (201): Empty body. A session cookie (garnet-session-token) is set automatically. Redirect the user to their dashboard:
js
const res = await fetch('https://your-store.garnet.center/auth/public/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email, password }),
});
if (res.ok) {
window.location.href = '/'; // redirect to dashboard
} else {
const { message } = await res.json();
showError(message);
}Error responses:
| Status | Message | When |
|---|---|---|
| 400 | email is a required field | Missing or invalid email |
| 400 | password must be at least 8 characters | Password too short |
| 401 | Invalid email or password | Wrong credentials |
| 401 | User is not verified | Account not yet verified |
| 401 | You don't have access to this marketplace | User exists but not on this store |
| 429 | Rate limit exceeded | More than 60 requests per minute |
POST /auth/public/change-password/request
Sends a 6-character reset code to the user's email. The code is valid for 15 minutes.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
email | string | yes | Vendor email address |
Success (201): Empty body. For security, the endpoint returns 201 even if the email does not exist. Always show a generic message like "If an account exists with this email, a reset code has been sent."
Error responses:
| Status | Message | When |
|---|---|---|
| 400 | email must be a valid email | Invalid email format |
| 429 | Code already sent, please wait before sending another code. | Less than 1 minute since last code |
POST /auth/public/change-password/update
Sets a new password using the reset code. On success, the user is automatically logged in (session cookie is set).
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
email | string | yes | Vendor email address |
code | string | yes | 6-character reset code from email |
password | string | yes | New password (minimum 8 characters) |
redirect | string | no | Path to redirect to after login (e.g. /) |
Success (201): Empty body. Session cookie is set — redirect the user to their dashboard.
Error responses:
| Status | Message | When |
|---|---|---|
| 400 | password must be at least 8 characters | Password too short |
| 400 | Incorrect code or link has expired | Wrong code or user not found |
| 400 | Expired code | Code is older than 15 minutes |
| 401 | You don't have access to this marketplace | User exists but not on this store |
POST /api3/public/vendor
Registers a new vendor on the marketplace. Requires vendor registration to be enabled.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
email | string | yes | Vendor email address |
password | string | yes | Password (minimum 10 characters) |
vendor | string | yes | Vendor name |
csrfToken | string | yes | CSRF token from /api3/public/csrf-token |
profile | object | no | Custom profile fields (key-value pairs) |
Success (201): Empty body. The vendor is either approved immediately or placed in a pending approval queue, depending on your marketplace settings.
Error responses:
| Status | Message | When |
|---|---|---|
| 400 | Invalid submission | Bot protection triggered |
| 400 | Invalid or expired token | CSRF token is invalid or expired |
| 400 | Please wait before submitting | Form submitted too quickly after loading |
| 400 | Public registration is not enabled | Vendor registration is disabled |
| 400 | Vendor already exists | Vendor name is already taken |
| 400 | Email already exists | Email is already registered |
General error format
All error responses return JSON with a message field:
json
{ "message": "Error message describing what went wrong" }You can use this to display inline error messages:
js
const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body });
if (!res.ok) {
const { message } = await res.json();
document.getElementById('error').textContent = message;
}