Get your free splitforms access key
Sign up at splitforms.com, verify your email, and copy your access key from the dashboard. No credit card required.
Contact form · Angular
Reactive Forms give you typed, synchronous validation; HttpClient gives you a typed Observable POST. Wire them to the splitforms endpoint and ship a contact form with no service, no controller, and no backend. Standalone component, Angular 17+ — works in NgModule apps too.

No server, API route, or SDK. Your Angular form posts straight to one endpoint.
Every submission is emailed to you and saved to a searchable dashboard — spam filtered before it reaches you.
It's your own Angular markup and styles. Splitforms is only the backend, so nothing constrains how the form looks.
Copy-paste ready
Replace YOUR_ACCESS_KEY with the key from your dashboard — that's the whole integration. No SDK to install, no build step, just the typescript you already write.
// contact-form.component.ts — Angular 17+ standalone component
import { Component, inject, signal } from "@angular/core";
import { CommonModule } from "@angular/common";
import { ReactiveFormsModule, FormBuilder, Validators } from "@angular/forms";
import { HttpClient } from "@angular/common/http";
@Component({
selector: "app-contact-form",
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="submit()">
<input type="hidden" formControlName="access_key" />
<label>
Name
<input type="text" formControlName="name" />
<small *ngIf="form.controls.name.touched && form.controls.name.invalid">
Name is required.
</small>
</label>
<label>
Email
<input type="email" formControlName="email" />
<small *ngIf="form.controls.email.touched && form.controls.email.invalid">
A valid email is required.
</small>
</label>
<label>
Message
<textarea formControlName="message"></textarea>
<small *ngIf="form.controls.message.touched && form.controls.message.invalid">
Please add a message.
</small>
</label>
<!-- Honeypot — invisible to humans -->
<input type="checkbox" formControlName="botcheck" hidden tabindex="-1" />
<button type="submit" [disabled]="status() === 'loading'">
{{ status() === 'loading' ? 'Sending…' : 'Send' }}
</button>
<p *ngIf="status() === 'ok'">Thanks — we'll be in touch.</p>
<p *ngIf="status() === 'err'">Something went wrong. Try again?</p>
</form>
`,
})
export class ContactFormComponent {
private fb = inject(FormBuilder);
private http = inject(HttpClient);
status = signal<"idle" | "loading" | "ok" | "err">("idle");
form = this.fb.group({
access_key: ["YOUR_ACCESS_KEY"],
name: ["", Validators.required],
email: ["", [Validators.required, Validators.email]],
message: ["", Validators.required],
botcheck: [false],
});
submit() {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
this.status.set("loading");
// Build a real multipart FormData so file fields (if you add them) work too.
const fd = new FormData();
Object.entries(this.form.getRawValue()).forEach(([k, v]) =>
fd.append(k, String(v))
);
this.http
.post<{ success: boolean; message?: string }>(
"https://splitforms.com/api/submit",
fd
)
.subscribe({
next: (data) => {
this.status.set(data.success ? "ok" : "err");
if (data.success) this.form.reset({ access_key: "YOUR_ACCESS_KEY" });
},
error: () => this.status.set("err"),
});
}
}How to add it
To add a contact form to a Angular website you need three things: a free splitforms access key, the typescript snippet above, and your key pasted into it. No backend, server, or SDK — the form posts to one URL and every submission lands in your inbox and dashboard.
Sign up at splitforms.com, verify your email, and copy your access key from the dashboard. No credit card required.
Copy the Angular code example into your project and replace YOUR_ACCESS_KEY with the key from step 1.
Submissions arrive in the splitforms dashboard within seconds. Free includes owner email notification attempts; Pro adds Slack, Discord, Sheets, or any signed webhook URL.
Where submissions go
Every submission is emailed to you and saved to a searchable dashboard — spam filtered before it ever reaches you. Search, export to CSV, or forward it to a webhook or Slack on Pro.

No backend needed
Your Angular form posts standard FormData to one URL. Splitforms validates the access key, runs its spam checks, and forwards it to your email — so there's no server, API route, or database for you to build or maintain.

Best practices
The difference between a form that works in the demo and one that survives launch traffic — the production-tested defaults, in priority order.

How SplitForms works
splitforms receives the submission, filters spam, saves it to your dashboard and emails you — then, on paid plans, sends it on to your webhooks and integrations. No backend infrastructure to build.

Point your form to your unique SplitForms endpoint. That's it.

We instantly capture and organize every submission in your inbox.

Send data to email, spreadsheets, CRMs, webhooks, and 7,000+ apps.

No credit card required. Set up in under 60 seconds.
Connect & automate
On paid plans, submissions can also go to Slack, Discord, Google Sheets, Notion, Airtable, Zapier or any signed webhook — and splitforms works on the platforms you build on, from WordPress and Shopify to Next.js. With Stripe connected, your forms can take payments too.

Questions
Build a FormGroup with FormBuilder, then on (ngSubmit) append each control's value (plus access_key) to a FormData and call httpClient.post('https://splitforms.com/api/submit', fd).subscribe(…). Inspect data.success in next; handle 4xx in error.
Reactive Forms (FormBuilder, formControlName). They're synchronous, fully typed, unit-testable, and scale to cross-field validators. Template-driven forms with ngModel work for tiny forms but get unwieldy and harder to test. splitforms is form-agnostic — it only cares about the multipart body.
HttpClient routes any non-2xx response to the error callback of .subscribe({ next, error }) (or to catchError in the pipe). Inspect err.error?.message for splitforms' user-facing message. 2xx responses with success: false (e.g. spam flagged) still arrive in next — always check data.success there too.
Yes. The component above is standalone (standalone: true, imports ReactiveFormsModule). No NgModule changes. If you're on a NgModule-based app, just declare the component and provide HttpClient once via provideHttpClient() (or import HttpClientModule) in your app config.
You can't fully — Angular compiles to client-side JavaScript, so anything the component reads ends up in the bundle. The right mitigation is domain-locking the key in the splitforms dashboard (Settings → Allowed domains). For true server-side secrecy, proxy through an Angular Universal server route that injects the key from process.env server-side.
Yes, but POST from the browser, not during server rendering. Guard the HttpClient call with isPlatformBrowser(platformId) or keep it inside the (ngSubmit) handler (which only fires client-side). The form markup can render on the server; the submission must originate from the browser.
Angular's HttpClient inspects the body argument: pass a FormData instance and it sets Content-Type: multipart/form-data; boundary=… automatically. If you manually add a Content-Type: multipart/form-data header in the options (a common copy-paste from older AngularJS tutorials), Angular doesn't know the boundary string and the request body can't be parsed — splitforms returns 400 malformed body. Same rule as fetch and jQuery: leave the header alone when the body is FormData.
Unlike fetch, Angular's HttpClient treats any non-2xx status as an error and routes it to the error callback of .subscribe({ next, error }) (or to catchError in the pipe). That means splitforms' 4xx responses (invalid key, spam flagged, rate-limit) hit error, NOT next — so data.success is never checked for those cases and you'll miss the user-facing message entirely. Read err.error?.message in the error handler, or normalize both paths with catchError. Remember: 2xx with success: false still lands in next, so check data.success there too.
Bootstrap-style 'show errors after submit' isn't automatic in Angular — a control's errors are present in form.controls.email.invalid but the *ngIf template guard usually checks .touched too, so nothing displays until the user blurs the field. In submit(), call this.form.markAllAsTouched() before returning early on this.form.invalid, or users click Send on an empty form and see nothing happen. Also prefer the strict this.fb.group over fb.group (pre-14) which returned FormGroup<any> and let typos like form.controls.emial compile.
Simple pricing
Free gives you 500 submissions for $0 — no card, no time limit. Pro is $5/month for 5,000 submissions a month, and Annual is $49/year for 15,000 a month with every Pro feature.
For side projects and indie devs.
Every paid feature, for agencies and growing products.
Every Pro feature with triple the volume — $0.27 per 1,000 at full use.
No credit card required on Free • Cancel anytime • Full pricing and FAQ