Build Form I-9 and E-Verify into your own system
One REST API to start I-9s, read their status, and keep every record in sync with your HR system, with our compliance team handling everything behind it.
# Start an I-9 the moment the offer is acceptedPOST /api/NewHire/create{ "firstName": "Jordan", "lastName": "Lee", "email": "jordan.lee@example.com", "dateOfHire": "2026-11-02", "applicationId": "A-20417", "unitNumber": "DAL-01"}200 { "status": "Success" } GET /api/NewHire/status?applicationId=A-20417{ "i9Status": 1 } # Section 1 signed
Start
Overview Quickstart I-9 deadlines to build around Six steps for any HR systemConcepts
Locations, IDs, hire types Statuses ErrorsReference
Endpoints Embedding the I-9Ship it
Best practices Go-live checklist Get helpOverview
Your system tells us about the people it hires, changes, and lets go. We run the Form I-9 and E-Verify work for each of them, and you read the result back whenever you need it.
| Item | Value |
|---|---|
| Base URL | https://api.i-9intelligence.com |
| Format | JSON over HTTPS. Send Content-Type: application/json. |
| Auth | JWT bearer token from POST /api/Auth/api-login, valid for 60 minutes |
| Dates | YYYY-MM-DD, for example 2026-11-02 |
| Rate limit | 1,000 requests a minute, with a 500-request queue processed oldest first. Over the limit returns 429. |
Quickstart
You'll authenticate, start an I-9 for a test hire, read its status, and clean up, which takes about 30 minutes.
Every email this API sends goes to a real inbox, because the API runs against production. A test hire with a real person's address sends that person an I-9 invite.
Ask support for a demo location to test in, use your own email address for test hires, and delete each test record when you're done. If you'd rather test away from production, ask us about a separate sandbox.
-
Get your credentials
To get started, email support@i-9intelligence.com or open a ticket for a Client ID and Client Secret, and ask for your location codes (
unitNumber). Treat the secret like a password and keep it out of your code repository. -
Get a token
Exchange your credentials for a bearer token that lasts 60 minutes.
curl -X POST https://api.i-9intelligence.com/api/Auth/api-login \ -H "Content-Type: application/json" \ -d '{"clientId":"YOUR_CLIENT_ID","clientSecret":"YOUR_CLIENT_SECRET"}'const res = await fetch("https://api.i-9intelligence.com/api/Auth/api-login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId: process.env.I9_CLIENT_ID, clientSecret: process.env.I9_CLIENT_SECRET, }), }); const { token } = await res.json();import os, requests res = requests.post( "https://api.i-9intelligence.com/api/Auth/api-login", json={ "clientId": os.environ["I9_CLIENT_ID"], "clientSecret": os.environ["I9_CLIENT_SECRET"], }, ) token = res.json()["token"]Send it on every other call as
Authorization: Bearer YOUR_TOKEN, and keep the wordBearerand the space after it, because the API requires both. -
Start an I-9 for a test hire
Use your own email address, and set a hire date of today or later.
curl -X POST https://api.i-9intelligence.com/api/NewHire/create \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "firstName": "Test", "lastName": "Hire", "email": "you@yourcompany.com", "dateOfHire": "2026-11-02", "applicationId": "TEST-0001", "unitNumber": "YOUR_UNIT", "employeeSSN": "Pending" }'const created = await fetch("https://api.i-9intelligence.com/api/NewHire/create", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ firstName: "Test", lastName: "Hire", email: "you@yourcompany.com", dateOfHire: "2026-11-02", applicationId: "TEST-0001", unitNumber: "YOUR_UNIT", employeeSSN: "Pending", }), }).then((r) => r.json());headers = {"Authorization": f"Bearer {token}"} created = requests.post( "https://api.i-9intelligence.com/api/NewHire/create", headers=headers, json={ "firstName": "Test", "lastName": "Hire", "email": "you@yourcompany.com", "dateOfHire": "2026-11-02", "applicationId": "TEST-0001", "unitNumber": "YOUR_UNIT", "employeeSSN": "Pending", }, ).json()200 response{ "status": "Success", "employeeId": null, "applicationId": "TEST-0001", "message": "Information has been successfully saved, and an email has been sent to the new hire.", "errors": [] }The Section 1 invite should be in your inbox within a minute or two.
-
Read the status
bashcurl "https://api.i-9intelligence.com/api/NewHire/status?applicationId=TEST-0001&unitNumber=YOUR_UNIT" \ -H "Authorization: Bearer YOUR_TOKEN"
You'll get
"i9Status": 0(Section 1 initiated) until the test hire signs Section 1. The statuses section covers every value and what to do with it. -
Clean up
bashcurl -X DELETE "https://api.i-9intelligence.com/api/NewHire/delete?applicationId=TEST-0001&unitNumber=YOUR_UNIT" \ -H "Authorization: Bearer YOUR_TOKEN"
Delete is a soft delete. We keep the record for the audit trail, and the API returns
404for that ID from then on. You're ready to wire up real events, and the best practices are the place to start.
I-9 deadlines to build around
Your integration decides when each I-9 starts, so it decides whether your company makes the three deadlines that come up in every build.
| Step | Deadline | What it means for your build |
|---|---|---|
| Section 1 | By the first day of work for pay. The hire can complete it any time after accepting the offer. | Call create when the offer's accepted, not on day one. |
| Section 2 | Within 3 business days of the first day of work for pay. It can also be done before day one, once the offer's accepted. | Send dateOfHire as the real first day, and update it when the date moves. |
| E-Verify case | No later than the third business day after the hire starts work for pay. | We create the case. Your job is keeping the start date accurate. |
Don't hold pay or delay a start because an E-Verify case is open. While a mismatch is being resolved, employers can't terminate, suspend, delay training, or withhold pay because of it. Use I-9 status for reminders and reporting, and leave every decision about the person to HR.
Sources: USCIS, Completing Section 1 and Completing Section 2. E-Verify, Verification process and Mismatches.
Six steps for any HR system integration
Whether you run Workday, SAP, Oracle, ADP, or something you built, every integration with us comes down to the same six decisions, and they're worth settling before you write code.
Turn off your HR system's own I-9 module
Two I-9 workflows for one hire means two sets of invites and two records that disagree. Turn off reverification too, if your platform keeps it as a separate setting.
Turn off its automatic E-Verify submission
An HR system that creates E-Verify cases on its own gives the same person two cases, so confirm it's off before your first live I-9.
Start the I-9 here on the hire event
Send the hire to NewHire/create with the details that route it correctly, including the location, the hire type, and whether our team does Section 2 by video.
Decide where status shows up
Our dashboard and email alerts always show where every I-9 stands. Writing status back into your HR system is optional and differs by company. It depends on the API or webhook access your HR plan includes, and on whether a status field there could set off that system's own I-9 steps, so scope it on purpose.
Don't put other onboarding steps behind the I-9
Benefits, payroll, and IT accounts should keep moving while the I-9 finishes. Section 2 has three business days after the start date, and an open E-Verify case can't hold up pay or training.
Plan for a failed write-back
Tokens expire, APIs change, and rate limits hit at the worst time. Decide what your integration does when a call fails, how it retries, and who gets alerted, before go-live.
Locations, IDs, and hire types
Locations (unitNumber)
Every I-9 belongs to a location, identified by its unitNumber. The location decides which of your users can see and work that I-9, and its address fills in the employer address in Section 2. Locations roll up into a Level 2, usually a region, business unit, or legal entity, and your API key reaches every location under its Level 2.
Map unitNumber to the worker's physical work site in your HR system (Location in Workday and SAP SuccessFactors, Work Location in ADP), not to a department or cost center. We send the list of codes at setup, and most teams keep a small mapping table from their codes to ours.
Your IDs
Each record needs at least one of your identifiers, and you look it up by the same one later.
| Field | Use it when |
|---|---|
applicationId | The person is still an applicant or candidate in your ATS, before an employee ID exists. |
employeeId | The person exists in your HR system as an employee. Add it later with update-employee-id. |
Both must be unique within your company among active records. Terminated records don't count, which is how rehires work.
Hire types
| Send | Workflow | Email to hire |
|---|---|---|
"newHireTypeId": 2 | Remote (the default). Section 1 from anywhere, and Section 2 by your rep or by our team over video. | Yes |
"newHireTypeId": 1"s1FromHome": true | Section 1 from home, Section 2 on site with your staff. | Yes |
Fully in-person hires, where both sections happen at the worksite, aren't created through the API. Start those from the location's self-start link or in the i9 Intelligence web app.
Add "forceRemoteVerification": true to send the hire's Section 2 to our US-based compliance team by video.
Duplicate protection
Create fails with 400 if an active record already has the same SSN, employeeId, or applicationId. That stops double I-9s, and it's why a returning employee needs their old record terminated before you create a new one.
Statuses
GET /api/NewHire/status returns the I-9 status as a number in i9Status, a plain-English i9StatusDescription, and the E-Verify and reverification fields. GET /api/NewHire/get returns the form's progress as a code, such as SECTION_2_SIGNED. For separations, read status or terminationDate: a terminated hire's get still shows the last form step.
| i9Status | Code | Meaning | Suggested action in your system |
|---|---|---|---|
0 | SECTION_1_INITIATED | I-9 created, hire invited | Remind the hire if day one is close. |
1 | SECTION_1_SIGNED | Hire signed Section 1 | Section 2 is next. Nothing to do if our team has it. |
2 | SECTION_2_INITIATED | Document review started | None. |
3 | SECTION_2_SIGNED | Section 2 signed | None. |
4 | I9_COMPLETED | Form I-9 complete | Mark the I-9 step done. Keep watching E-Verify fields until the case closes. |
5 | TERMINATED | Separation recorded | Stop polling. Retention is tracked on our side. |
6 | DELETED | Onboarding cancelled | Stop polling. After a delete, lookups by that ID return 404. |
7 | INVALID | Record in an error state | Alert HR and contact support. |
E-Verify and reverification fields
| Field | What it tells you |
|---|---|
eVerifyStatusCode, eVerifyStatusText | Where the E-Verify case stands |
eVerifyActionRequired | true when someone needs to act on the case. Route it to HR. |
eVerifyCaseClosureReason | Set once the case is closed |
hasSupplementB, supplementBSignatureStatus, supplementBSignatureDate | Whether a reverification (Supplement B) exists and when it was signed. The last two are null when there isn't one. |
terminationDate, deletedDate | Set when the record is terminated or deleted |
{
"i9Status": 3,
"i9StatusDescription": "The employer has completed and signed Section 2 of the I-9 form, verifying the employee's documentation and confirming eligibility.",
"terminationDate": null,
"deletedDate": null,
"eVerifyStatusCode": "…",
"eVerifyStatusText": "…",
"eVerifyActionRequired": false,
"eVerifyCaseClosureReason": null,
"hasSupplementB": false,
"supplementBSignatureStatus": null,
"supplementBSignatureDate": null,
"errors": []
}Errors
Errors come back in one of two shapes. Field validation errors, like a missing email or a past hire date, list each failing field under errors. Business rule errors, like a duplicate ID or a record that isn't found, return one errorMessage with an errorMessageKey and the field in additionalInfo. A missing or expired token returns 401 with an empty body.
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"DateOfHire": ["The date must be today or a future date."]
},
"traceId": "00-a165ec45e310d5776e06821cb4d2f2a4-80a9f1698161213b-01"
}{
"statusCode": 400,
"errorMessage": "The Employee ID must be unique. An I9 record with this Employee Identifier already exists for this company.",
"errorMessageKey": "VALIDATION_ERROR",
"additionalInfo": { "AttributeName": "EmployeeId" }
}| HTTP | Meaning | What to do |
|---|---|---|
200 | Success | Nothing to do. Bulk calls only return 200 when every row succeeded. |
400 | Validation failed | Fix the payload using the error. Don't retry unchanged. |
401 | Missing or expired token | Get a new token and retry once. |
404 | Record not found | Check the ID and unitNumber. |
429 | Rate limit | Back off and retry with increasing waits. |
500 | Server error | Retry once, then contact support with the request time. |
Common validation errors
| Error | Usual cause | Fix |
|---|---|---|
| Duplicate SSN or ID | An active record already exists | Read its status first. For a returning employee, terminate the old record, then create. |
| Invalid unit | unitNumber isn't one your key can reach | Check your location mapping. |
| “The date must be today or a future date.” | dateOfHire before today on create | Send the real start date, today or later. |
| Missing identifier | No employeeId or applicationId | Send at least one. |
| Can't update or delete | The record is terminated or deleted | Terminated and deleted records are read-only. |
Endpoints
Every endpoint below except login needs a bearer token, and you can click any of them for its fields and an example.
POST/api/Auth/api-loginGet a 60-minute bearer token
Send your Client ID and Client Secret, which only work in the environment they were issued for.
| Field | Type | Notes |
|---|---|---|
clientId required | string | From i9 Intelligence |
clientSecret required | string | Treat like a password |
POST/api/NewHire/createStart an I-9 for one hire
Creates the I-9 record, runs the duplicate check, and starts the workflow for the hire type. Remote and Section-1-from-home hires get an email invite.
| Field | Type | Notes |
|---|---|---|
firstName required | string | Legal first name |
lastName required | string | Legal last name |
email required | string | Where the hire gets their invite |
dateOfHire required | date | First day of work for pay. Today or later. |
unitNumber required | string | Location code your key can reach |
employeeId | string | One of employeeId or applicationId is required |
applicationId | string | Your applicant or candidate ID |
middleInitial | string | One character |
employeeSSN | string | XXX-XX-XXXX, or "Pending" if you don't have it yet |
newHireTypeId | integer | 2 remote (default), or 1 with s1FromHome: true. See hire types. |
s1FromHome | boolean | Send true with newHireTypeId: 1. Leave it out for remote hires. |
forceRemoteVerification | boolean | Send Section 2 to our team by video |
POST/api/NewHire/bulk-createStart I-9s for a batch of hires
Send an array of create objects, and know that one row failing validation means nothing is created, so fix that row and resend the whole batch.
{
"status": "Success",
"message": "All 2 new hire records created successfully.",
"totalRecords": 2,
"successfulRecords": 2,
"results": [
{ "index": 0, "employeeId": null, "applicationId": "A-20417", "success": true, "message": "New hire record created successfully and email sent to employee.", "errorCode": null },
{ "index": 1, "employeeId": null, "applicationId": "A-20418", "success": true, "message": "New hire record created successfully and email sent to employee.", "errorCode": null }
],
"errors": []
}GET/api/NewHire/statusI-9 and E-Verify status
Query by employeeId or applicationId, plus unitNumber when the same ID could exist at more than one location. For what each field in the response means, see Statuses.
GET/api/NewHire/getFull record for one hire
This takes the same query parameters as status and returns name, email, hire date, location, termination date, the I-9 status as a code, and a masked SSN.
PUT/api/NewHire/updateChange hire date, location, or details
Identify the record with employeeId or applicationId, and send only the fields that changed, on any record that isn't terminated or deleted. Updatable fields are firstName, lastName, middleInitial, email, employeeSSN, dateOfHire, and unitNumber (which moves the record to another location).
{
"employeeId": "EMP-10042",
"dateOfHire": "2026-11-09",
"unitNumber": "AUS-02"
}PUT/api/NewHire/bulk-updateUpdate a batch, all or nothing
Send an array of update objects, but know that one row failing validation means nothing changes, so fix the bad rows and resend the whole batch.
PUT/api/NewHire/update-employee-idAdd or change the employee ID
Use this when an applicant becomes an employee in your HR system, and send unitNumber and newEmployeeId, plus applicationId or oldEmployeeId to find the record.
{
"unitNumber": "DAL-01",
"applicationId": "A-20417",
"newEmployeeId": "EMP-10042"
}PUT/api/NewHire/update-application-idAdd or change the applicant ID
Send unitNumber and newApplicationId, plus employeeId or oldApplicationId to find the record.
POST/api/NewHire/terminateRecord a separation
Sets the status to TERMINATED, records the date for retention, and closes any open E-Verify case, while the record stays available for audits.
{
"employeeId": "EMP-10042",
"unitNumber": "DAL-01",
"terminationDate": "2027-06-30"
}POST/api/NewHire/bulk-terminateRecord separations in a batch
Send an array of terminate objects, and like the other bulk calls it's all or nothing, so one row that can't be found or is already terminated fails the batch.
DELETE/api/NewHire/deleteCancel a hire who never started
For rescinded offers, no-shows, and failed background checks before day one. Query by ID and unitNumber. It's a soft delete: we keep the record for audit purposes, and the API returns 404 for that ID afterward. Once someone has started, use terminate instead.
DELETE/api/NewHire/bulk-deleteCancel a batch, all or nothing
Body is an array of { "unitNumber", "employeeId" or "applicationId" }, and if any row fails, none of them are deleted.
GET/api/UnitLink/getSection 1 link for a hire, framed or not
Parameters are unitNumber, employeeId or applicationId, and usesIFRAME=true when you'll open it in a frame. The response includes linkUrl, and Embedding the I-9 shows how to use it.
GET/api/UnitLink/s2Section 2 link for the person reviewing documents
Parameters are unitNumber, userName (the i9 Intelligence user completing Section 2), the hire's employeeId or applicationId, and an optional redirectURL to send them back to your tool when they're done.
POST/api/UnitLink/create?unitNumber=Reusable self-start link for a location
Creates one link per location that any new hire can open to start Section 1. It never expires, though creating a new one turns off the old one, and it's what powers kiosk mode and the QR codes at hiring sites.
Embedding the I-9 Enterprise Package
Your portal asks for a link, opens it in a frame, and reads the status when the hire's done, so new hires never leave your site.
UnitLink/get with the hire's ID and usesIFRAME=true.linkUrl in an iframe next to the rest of onboarding.NewHire/status and moves the hire to the next step.curl "https://api.i-9intelligence.com\ /api/UnitLink/get\ ?unitNumber=DAL-01\ &applicationId=A-20417\ &usesIFRAME=true" \ -H "Authorization: Bearer YOUR_TOKEN"
{
"status": "Success",
"linkUrl": "https://app.i-9intelligence.com/…",
"unitNumber": "DAL-01",
"locationName": "Dallas Plant",
"errors": []
}<iframe src="LINK_URL_FROM_YOUR_SERVER" title="Form I-9, Section 1" style="width:100%;min-height:900px;border:0"> </iframe>
Section 2 for your own reviewers
Managers and HR reps who complete Section 2 get their own link from UnitLink/s2. Request it with their i9 Intelligence userName and the hire's ID, then add redirectURL if you'd like us to send them back to your tool after they sign.
Ask for a fresh link each time you render the page. Requesting a new link can turn off the previous one, so a cached URL can stop working. Keep the API call on your server, where your token lives.
If you're on Workday, note that its onboarding screens don't support embedded forms from outside vendors. Workday customers use our pre-built connector, and the hire completes Section 1 from an email link instead.
Best practices
What we recommend after building and supporting integrations with Workday, SAP, and homegrown systems.
Start the I-9 when the offer's accepted
Create with applicationId as soon as the candidate accepts. The hire can finish Section 1, and Section 2 can be done before day one. Swap in the employee ID later with update-employee-id.
Map each HR event to one call
Keep the integration boring and predictable.
| Event in your system | Call |
|---|---|
| Offer accepted | POST /NewHire/create |
| Employee ID assigned | PUT /NewHire/update-employee-id |
| Start date, location, name, or email changes | PUT /NewHire/update |
| Offer rescinded or no-show | DELETE /NewHire/delete |
| Separation | POST /NewHire/terminate |
| Rehire | Terminate the old record if it's still active, then POST /NewHire/create |
Make create safe to retry
Sometimes a create succeeds but the response never reaches you, so your system retries. Before creating, check status by your employee ID. If you get the duplicate-ID 400, look the record up and carry on instead of failing the job.
Cache your token
Get one token per worker and reuse it, refreshing at about 55 minutes or whenever you get a 401. Logging in on every request wastes your rate limit.
Poll only open records
Check open I-9s every 15 to 60 minutes. Once i9Status reaches 4 (I-9 completed), check it daily until eVerifyCaseClosureReason is set, then stop. Stop right away at 5 (terminated) or when a lookup returns 404 after a delete.
Delete before day one, terminate after
Delete is for people who never started, and anyone who worked even one day needs a terminate call so their I-9 is kept for the retention period.
Keep start dates accurate
Section 2 and E-Verify deadlines count from dateOfHire, so send the update the same day the start date moves in your system.
Validate a batch before you send it
Every bulk endpoint is all or nothing, so one bad row fails the whole batch and nothing changes. Check rows on your side first, and when a batch fails, fix the row the error points to and resend.
Back off on 429
Wait and retry with increasing delays, such as 1, 2, then 4 seconds. Use bulk endpoints for hiring events instead of hundreds of single calls.
Handle SSNs carefully
Send "Pending" if you don't have the SSN yet, and never log request bodies that contain one, since our API only ever returns it masked.
Log what support will ask for
Keep the request time, unitNumber, your ID, the HTTP code, and the errors array. With those, we can trace any call.
Go-live checklist
- Your HR system's automatic E-Verify submission is off
- Your HR system's own I-9 and reverification steps are off
- Credentials stored as secrets, not in code
- Your location codes mapped to our
unitNumbervalues - Create fires on offer accepted, with the right hire type
- Test hire created with your own email, and the invite arrived
- Status written to the worker's profile in your system
- Start date and location changes send an update
- Employee ID swapped in once your HR system assigns it
- Rescinded offers call delete, and separations call terminate
- Duplicate
400handled without failing the job 401and429retried correctlyeVerifyActionRequiredrouted to HR- Payroll, benefits, and IT steps don't wait on the I-9
- Failed calls retry, and someone gets alerted
- All test records deleted
Get help
Our support team answers API questions by email and phone, and our team can also work hands-on with your developers.
Support
support@i-9intelligence.com
(713) 668-6200, Monday to Friday, 8 to 5 Central. We answer tickets within 2 business hours. Include the details from your logs.
Help with your build
We can walk your developer through how your fields map to the I-9, help you connect, and check your first live I-9s. We also build custom integrations and embedded I-9s.
Talk to our team →