Developers
Apply Handoff API
Prefill your own application form from an AvPosts candidate profile. Version 1.
What it does
When a signed-in AvPosts candidate clicks Apply on one of your postings, AvPosts hands their profile to your apply page so the form is already filled in when they land. The candidate opts in on every click, the handoff is a single-use token that lives ten minutes, and your page redeems it with one GET. You keep your own form, your own ATS and your own domain; AvPosts only shortens the typing.
How it works
- A signed-in candidate with a profile sees Send my AvPosts profile with this application next to Apply, checked by default. Unchecking it makes Apply a plain link.
- On click, AvPosts mints a random 32-byte token, stores only its SHA-256 plus a frozen JSON snapshot of the profile, and redirects to your apply URL with
?avposts_handoff=<token>appended, alongside the usualutm_*parameters. - Your page — via the drop-in script or your own code — calls
GET https://avposts.io/api/v1/handoff/<token>. The first call returns the snapshot; every later call returns410. - You fill your form from the response and drop the token from the URL. The candidate reviews, edits, attaches their résumé and submits to you as usual.
Option A — drop-in script
One tag, anywhere on the apply page. It reads the token, redeems it, fills any input, select or textarea whose name is one of the canonical keys — firstName, lastName, email, phone, city, province, linkedinUrl, workAuth, yearsExperience, totalHours, picHours, multiHours, turbopropHours, medical — dispatches input and change events so React and Vue controlled fields pick the values up, then strips the token from the address bar. No dependencies, plain ES2017, about 4 KB.
<script src="https://avposts.io/apply.js"></script>Fields with other names take a data-avposts attribute naming a dotted path into the payload:
<!-- Matched by name: nothing to add -->
<input name="firstName">
<input name="lastName">
<input name="email">
<input name="phone">
<input name="linkedinUrl">
<!-- Any other field: point it at a path in the payload -->
<input name="tt" data-avposts="applicant.hours.total">
<input name="pic" data-avposts="applicant.hours.pic">
<select name="med" data-avposts="applicant.medical">…</select>Forms that hold their state in JavaScript can ignore the DOM filling and listen for the avposts:handoff event instead — the full payload is in event.detail. The same payload is available as window.AvPosts.handoff, a promise that resolves to the payload or null.
useEffect(() => {
function onHandoff(e) {
const a = e.detail.applicant;
setForm((f) => ({
...f,
firstName: a.firstName ?? f.firstName,
lastName: a.lastName ?? f.lastName,
email: a.email,
phone: a.phone ?? f.phone,
totalHours: a.hours.total ?? f.totalHours,
licences: a.licences.join(", "),
}));
}
window.addEventListener("avposts:handoff", onHandoff);
return () => window.removeEventListener("avposts:handoff", onHandoff);
}, []);
// Or await it directly:
// const payload = await window.AvPosts.handoff; // null when there is no handoffOption B — call the API yourself
Read avposts_handoff from the query string and GET the endpoint from the browser or your server. No API key: the token is the credential. CORS is open (Access-Control-Allow-Origin: *) and every response is Cache-Control: no-store.
curl -s https://avposts.io/api/v1/handoff/${token} \
-H "Origin: https://greatnorthairlines.com"{
"schema": "avposts.handoff/1",
"jobId": 1042,
"applicant": {
"firstName": "Jane",
"lastName": "Doe",
"email": "jane.doe@example.com",
"phone": "+1 403 555 0100",
"city": "Calgary",
"province": "AB",
"linkedinUrl": "https://linkedin.com/in/janedoe",
"workAuth": "Canadian citizen",
"languages": ["en", "fr"],
"yearsExperience": 9,
"willRelocate": true,
"licences": ["ATPL"],
"medical": "Cat 1",
"hours": { "total": 3200, "pic": 1200, "multi": 900, "turboprop": 400 },
"types": [{ "family": "CRJ", "kind": "rating" }],
"roles": ["pilot"]
},
"issuedAt": "2026-08-31T14:02:11.412Z",
"expiresAt": "2026-08-31T14:12:11.412Z"
}Field reference
Every key is always present. A fact the candidate has not entered is null (or an empty array for lists) — never missing, never undefined.
| Field | Type | Example | Notes |
|---|---|---|---|
| schema | string | "avposts.handoff/1" | Payload version. Breaking changes bump it. |
| jobId | number | 1042 | The AvPosts posting the candidate applied from. Also in utm_campaign. |
| applicant.firstName | string | null | "Jane" | From the profile, or split from the account display name. |
| applicant.lastName | string | null | "Doe" | As above. A single-word display name gives a first name only. |
| applicant.email | string | "jane.doe@example.com" | The signed-in account address. Always present. |
| applicant.phone | string | null | "+1 403 555 0100" | Free text as the candidate typed it. Not normalised. |
| applicant.city | string | null | "Calgary" | |
| applicant.province | string | null | "AB" | Two-letter Canadian province or territory code. |
| applicant.linkedinUrl | string | null | "https://linkedin.com/in/janedoe" | Always http(s); a bare host is normalised on save. |
| applicant.workAuth | string | null | "Canadian citizen" | One of: Canadian citizen, Permanent resident, Work permit. |
| applicant.languages | string[] | ["en", "fr"] | ISO 639-1 codes. Empty array when unstated. |
| applicant.yearsExperience | number | null | 9 | Whole years. |
| applicant.willRelocate | boolean | true | Never null; false when unstated. |
| applicant.licences | string[] | ["ATPL"] | CPL, ATPL, AME M1, AME M2, AME E, AME S. |
| applicant.medical | string | null | "Cat 1" | Cat 1 or Cat 3. |
| applicant.hours.total | number | null | 3200 | Total time, hours. |
| applicant.hours.pic | number | null | 1200 | Pilot-in-command hours. |
| applicant.hours.multi | number | null | 900 | Multi-engine hours. |
| applicant.hours.turboprop | number | null | 400 | Turboprop hours. |
| applicant.types[].family | string | "CRJ" | Aircraft family, in AvPosts vocabulary. |
| applicant.types[].kind | string | "rating" | rating (pilot), endorsement (AME), trained (cabin crew). |
| applicant.roles | string[] | ["pilot"] | pilot, ame, cabin, ops, corporate. |
| issuedAt | string | "2026-08-31T14:02:11Z" | ISO 8601, UTC. When the candidate clicked Apply. |
| expiresAt | string | "2026-08-31T14:12:11Z" | issuedAt + 10 minutes. |
Errors
Treat every non-200 as “no handoff” and show the empty form. The drop-in script does exactly that, silently.
| Status | Body | Meaning |
|---|---|---|
| 400 | { "error": "malformed_token" } | Not a token shape at all. Nothing was looked up. |
| 404 | { "error": "unknown_token" } | No handoff with that token was ever issued. |
| 410 | { "error": "expired" } | Issued more than 10 minutes ago and never redeemed. |
| 410 | { "error": "already_redeemed" } | Redeemed once already. Tokens are single use. |
Privacy & security
- The candidate opts in per click. Nothing is sent unless the checkbox next to Apply is on, and the choice is theirs every time.
- Ten minutes, single use. A token is valid for ten minutes from the click and is dead after its first redemption. AvPosts stores only its hash.
- A snapshot, not a live link. The payload is frozen at click time. It gives no access to the AvPosts account and cannot be used to fetch anything else.
- Do not store the token. Redeem it, use the payload, and drop it from your URL and your logs. Keep the applicant data under your own privacy policy, exactly as if they had typed it.
- No résumé in v1. The handoff carries structured profile fields only. Files stay with your own upload.
- No user ids. The payload contains what an application form asks for and nothing internal to AvPosts.
Attribution
The handoff rides alongside the existing attribution parameters, which are unchanged: utm_source=avposts, utm_medium=job_board and utm_campaign=<jobId> are appended to every off-site apply URL that does not already carry a utm_source. A visit with a token is always also a visit with utm parameters; the reverse is not true.
Changelog
- 2026-08-31 — v1. Schema
avposts.handoff/1, drop-inapply.js, single-use ten-minute tokens.
Looking for the careers-page widget instead? Embed your live openings →