How to Automate Vendor Registration on Etimad (2025)
Meta description: Step-by-step guide to reliably automate Etimad vendor registration using the Anchor Browser SDK—handles OTP, pop-ups, timeouts, and file uploads with audit logs.
TL;DR
Use Anchor Browser’s verified Chromium sessions + Playwright helpers to script Etimad’s vendor-registration flow with retries, OTP handling, and audit trails. Copy the code, swap selectors, and run.
Prerequisites
- Anchor Browser account & API key (project-level).
- Node 18+.
- Dedicated proxy for your region (recommended).
- Etimad account with the right role; comply with portal terms and all procurement rules.
npm i @anchorbrowser/sdk playwright
Create .env:
ANCHOR_API_KEY=... # from console
ANCHOR_REGION=me-central-1 # example; pick closest
PROXY_URL=http://user:pass@host:port # optional but recommendedThe approach (reliable pattern)
- Start a verified browser session (humanized™ fingerprint, bot-check safe).
- Navigate & log in (SSO/OTP with human-in-the-loop or SMS inbox integration).
- Fill the registration steps via resilient selectors (labels/roles, not brittle CSS).
- Upload documents with chunked upload + retry on 413/timeout.
- Submit & capture evidence (screenshots, HTML snapshot, network log).
- Emit audit: who, when, which fields were touched (no secrets).
Code: Etimad vendor registration with Anchor SDK
Notes
- Replace placeholder selectors with the current labels in Etimad.
- The OTP handler below is an example—swap in your SMS/Email source as needed.
- Uses Playwright routed through Anchor’s verified session.
// etimad-register.ts
import 'dotenv/config';
import { Anchor } from '@anchorbrowser/sdk';
import { chromium, Page } from 'playwright';
type VendorProfile = {
companyName: string;
crNumber: string; // Commercial Registration
taxNumber: string;
iban: string;
contactName: string;
contactEmail: string;
contactPhone: string;
docs: {
crPdf: string; // ./docs/cr.pdf
taxCertPdf: string; // ./docs/tax.pdf
ibanLetterPdf: string; // ./docs/iban.pdf
};
};
const profile: VendorProfile = {
companyName: 'Anchor Forge Ltd.',
crNumber: '1010XXXXXX',
taxNumber: '3XX-XX-XXXXXX',
iban: 'SA44 2000 0000 0000 0000 0000',
contactName: 'Idan Raman',
contactEmail: 'ops@anchorbrowser.io',
contactPhone: '+9665XXXXXXXX',
docs: {
crPdf: './docs/cr.pdf',
taxCertPdf: './docs/tax.pdf',
ibanLetterPdf: './docs/iban.pdf',
},
};
async function main() {
const anchor = new Anchor({
apiKey: process.env.ANCHOR_API_KEY!,
region: process.env.ANCHOR_REGION || 'me-central-1',
defaults: {
extraStealth: true,
proxyUrl: process.env.PROXY_URL, // optional
recordVideo: false,
snapshotOnError: true,
},
});
// 1) Start a verified browser session
const session = await anchor.sessions.start({
label: 'etimad-vendor-registration',
keepAliveSeconds: 900, // extend if needed during OTP
metadata: { portal: 'etimad', flow: 'vendor-registration' },
});
// 2) Attach Playwright to the Anchor session
const wsEndpoint = await session.playwright.connectWsEndpoint();
const browser = await chromium.connectOverCDP(wsEndpoint);
const context = browser.contexts()[0] || await browser.newContext({
viewport: { width: 1280, height: 800 },
userAgent: await session.fingerprint.userAgent(), // humanized UA
});
const page = await context.newPage();
// Helpers
const safeClick = (p: Page, sel: string) =>
p.getByRole('button', { name: sel }).or(p.getByText(sel, { exact: true })).click();
const typeByLabel = async (p: Page, label: string, value: string) => {
const ctl = p.getByLabel(label).first();
await ctl.waitFor({ state: 'visible', timeout: 15000 });
await ctl.fill(value, { timeout: 15000 });
};
// 3) Login flow (email + password + OTP)
await page.goto('https://login.etimad.sa/', { waitUntil: 'domcontentloaded', timeout: 90000 });
// Example selectors—replace with the current labels on the portal
await typeByLabel(page, 'Email', process.env.ETIMAD_EMAIL!);
await typeByLabel(page, 'Password', process.env.ETIMAD_PASSWORD!);
await safeClick(page, 'Sign in');
// OTP (human-in-the-loop or mailbox/SMS poll)
const code = await anchor.human.prompt({
title: 'Enter Etimad OTP',
description: 'Check registered device/email and paste the 6-digit code.',
mask: false,
timeoutSeconds: 180,
});
await typeByLabel(page, 'One-time code', code.value);
await safeClick(page, 'Verify');
// 4) Navigate to: Vendor Registration
await page.waitForURL(/dashboard|home/, { timeout: 120000 });
await page.getByRole('link', { name: /Vendors?/ }).click();
await page.getByRole('link', { name: /Register/i }).click();
// 5) Fill company details
await typeByLabel(page, 'Company Name (English)', profile.companyName);
await typeByLabel(page, 'Commercial Registration Number', profile.crNumber);
await typeByLabel(page, 'VAT / Tax Number', profile.taxNumber);
await typeByLabel(page, 'IBAN', profile.iban);
// 6) Contact details
await typeByLabel(page, 'Contact Person Name', profile.contactName);
await typeByLabel(page, 'Contact Email', profile.contactEmail);
await typeByLabel(page, 'Contact Phone', profile.contactPhone);
// 7) Upload docs (robust uploads with retry)
const uploadWithRetry = async (label: string, path: string) => {
for (let i = 0; i < 3; i++) {
try {
const input = page.getByLabel(label).or(page.locator('input[type="file"]').nth(0));
await input.setInputFiles(path, { timeout: 30000 });
await page.getByText(/uploaded|success/i).first().waitFor({ timeout: 10000 });
return;
} catch (e) {
if (i === 2) throw e;
await page.waitForTimeout(1500 + i * 1000);
}
}
};
await uploadWithRetry('Commercial Registration PDF', profile.docs.crPdf);
await uploadWithRetry('Tax Certificate PDF', profile.docs.taxCertPdf);
await uploadWithRetry('IBAN Letter PDF', profile.docs.ibanLetterPdf);
// 8) Consent & submit
await page.getByLabel(/I confirm/i).check({ timeout: 10000 });
await safeClick(page, 'Submit');
// 9) Evidence & audit
const receipt = await page.locator('text=Reference No').first().textContent().catch(() => null);
await session.audit.log('etimad.vendor_registration.submitted', {
reference: receipt ?? 'pending',
fieldsTouched: [
'companyName','crNumber','taxNumber','iban',
'contactName','contactEmail','contactPhone'
],
});
await session.artifacts.saveScreenshot(page, { label: 'post-submit' });
await session.artifacts.saveDomSnapshot(page, { label: 'post-submit-html' });
await session.complete({ status: 'success', note: 'Vendor registration submitted' });
await browser.close();
}
main().catch(async (err) => {
console.error(err);
// Rich failure evidence
// (Anchor automatically adds last screenshot/snapshot when snapshotOnError=true)
process.exit(1);
});Troubleshooting (quick)
- OTP delays/timeouts → increase
keepAliveSeconds, allow manual pause viaanchor.human.confirm(). - Upload fails / 413 → ensure file size under limit; fall back to chunked upload if portal supports it; retry with backoff.
- Selector breaks after UI update → prefer
getByLabel,getByRole, and text-based locators; avoid brittle#ids. - 429 / anti-bot friction → use Anchor verified sessions, humanized timings (
page.waitForTimeout(±random)), stable IP/proxy, and retry on 429 with jitter.
Compliance & audit
- Respect the portal’s terms and applicable procurement regulations.
- Store only business data; keep OTP/secrets in your vault.
- Enable Anchor’s action log + artifact snapshots for audits.
FAQ (short)
Can this run headless? Yes; verified sessions work headful or headless. Prefer headful for flaky flows.
How do we pass OTP without a human? Use your SMS/Email provider API; feed the code into typeByLabel (or keep human-approval).
Will this survive UI changes? Use label/role selectors and keep a nightly smoke test; update selectors via a small map.
CTA: Want this as a ready template (with current selectors)? Spin up an Etimad Vendor Registration session template in Anchor Browser and plug in your env vars.