Create an application, give its owner and teammates access, and keep your records up to date with webhooks. Follow one company through the flow, then reuse it for each application in your tenant.
JavaScript examples use the backend helper below. Each step also has a cURL request and explains what to keep from the response. For complete field definitions and errors, use the API reference.
1. Prepare your backend
Arrange your tenant, intake and API key with Aurora Edge. The key needs applications:read, applications:write and webhooks:manage. Save the helper below as aegis-client.mjs, set AEGIS_API_KEY in your server environment, and initialize it once:
JavaScript
import { createAegisClient } from './aegis-client.mjs';
const aegis = createAegisClient(process.env.AEGIS_API_KEY);
const { data: intakes } = await aegis.listIntakes();
const intake = intakes.find(item => item.labelId === 'corporate');
if (!intake) throw new Error('Select an intake configured for your tenant');
Replace corporate with a label returned for your tenant. An empty list requires intake configuration before you can create an application. cURL examples use Bash with AEGIS_API_KEY, CLIENT_ID and other named values supplied by your backend or copied from your own test responses.
Authenticate your own user and check that they may manage the requested application before issuing links or changing members. The tenant API key can access multiple applications; it does not authorize your individual website users. Keep API keys, signing secrets and sign-in URLs out of browser storage and routine logs.
2. Create the application and owner link
Use the primary owner's email and a stable reference from your database. This single call creates the application and returns its first sign-in link.
JavaScript
const externalRef = 'ORDER-88213';
const ownerEmail = 'cfo@acme.example';
const session = await aegis.createApplicationWithLink({
email: ownerEmail,
companyName: 'Acme Partner FZ-LLC',
externalRef,
labelId: intake.labelId,
redirectPath: '/dashboard'
});
const clientId = session.clientId;
// Save externalRef and clientId in your database before handing off the owner.
// Redirect that owner to session.loginUrl or send it in your private invitation.
// Show session.expiresAt as the link expiry. Do not log the session.
Aegis does not send the invitation email for this call. Links are single use and live three days; use expiresAt rather than calculating expiry. Keep link-preview services from opening them. Reuse externalRef for the same case, and keep it within 200 characters. A different email with the same reference returns 409. To create without a link, use POST applications.
3. Reissue a link for the owner or a member
Use the existing application's clientId. Omit email to select its current owner, or supply the email of an existing member. These examples are alternatives; each call mints a fresh link.
JavaScript
const ownerSession = await aegis.issueLink(clientId);
// Privately deliver ownerSession.loginUrl to the current owner.
const memberSession = await aegis.issueLink(clientId, 'legal@acme.example');
// Privately deliver memberSession.loginUrl to this existing member.
Show the cURL request
cURL (Bash)
# Current owner. To select a member, add "email":"legal@acme.example" to the body.
curl --silent --show-error "https://aegis-api.theauroraedge.com/v1/applications/$CLIENT_ID/login-sessions" \
-H "Authorization: Bearer $AEGIS_API_KEY" -H 'Content-Type: application/json' \
--data '{"redirectPath":"/dashboard"}'
The response has the same clientId, loginUrl and expiresAt fields as creation. Add a person as a member before requesting their link. Apply this operation to any application your backend is authorized to manage within the tenant.
4. Invite, inspect and remove teammates
An invitation has two API steps: add membership, then issue a link for that member. Your system delivers the link.
JavaScript
const invitation = await aegis.inviteMember(clientId, 'legal@acme.example');
const memberUid = invitation.member.uid;
// Save memberUid. Privately deliver invitation.loginUrl to legal@acme.example.
const { members } = await aegis.getApplication(clientId);
// Use members[].uid and members[].email to present your access-management UI.
Show the cURL request
cURL (Bash)
# Add membership. Save uid from this response.
curl --silent --show-error "https://aegis-api.theauroraedge.com/v1/applications/$CLIENT_ID/members" \
-H "Authorization: Bearer $AEGIS_API_KEY" -H 'Content-Type: application/json' \
--data '{"email":"legal@acme.example"}'
# Issue the invitation link only after membership succeeds.
curl --silent --show-error "https://aegis-api.theauroraedge.com/v1/applications/$CLIENT_ID/login-sessions" \
-H "Authorization: Bearer $AEGIS_API_KEY" -H 'Content-Type: application/json' \
--data '{"email":"legal@acme.example","redirectPath":"/dashboard"}'
When your user chooses to remove a teammate, use the saved member UID:
Remove a teammate
await aegis.removeMember(clientId, memberUid); // 204, no response body.
Show the cURL request
cURL (Bash)
# MEMBER_UID is the uid of the teammate being removed.
curl --silent --show-error -X DELETE \
"https://aegis-api.theauroraedge.com/v1/applications/$CLIENT_ID/members/$MEMBER_UID" \
-H "Authorization: Bearer $AEGIS_API_KEY"
The add response contains uid, email, role and joinedAt. Adding an existing member preserves membership and joinedAt. If adding succeeds but link issuance fails, read the roster and retry only link issuance after resolving the error. The application can have up to 20 members.
Removing the current owner
Name another existing member as the new owner. Save the successful handover in your database: the application read does not expose ownerUid, and role labels do not identify the current owner after a handover.
JavaScript
await aegis.removeMember(clientId, currentOwnerUid, replacementMemberUid);
// The helper sends ?newOwnerUid=<replacementMemberUid> with DELETE.
// Future issueLink(clientId) calls target the replacement owner.
A repeat removal returns 400 once the member is gone. After a timeout, inspect the roster before retrying.
5. Retrieve status or all applications
JavaScript
const application = await aegis.getApplication(clientId);
const { status, decision, rejectionReason } = application;
// Save this current snapshot against your own application record.
// The helper follows every cursor and skips duplicate clientIds within this sweep.
for await (const item of aegis.listApplications()) {
// Upsert item against your own record using item.clientId.
}
// Optional alternatives: recover one reference or reconcile a time window.
for await (const item of aegis.listApplications({ externalRef: 'ORDER-88213' })) {
// Recover the clientId associated with your reference.
}
for await (const item of aegis.listApplications({ updatedSince: '2026-09-01T00:00:00.000Z' })) {
// Reconcile applications changed in this window.
}
Show the cURL request
cURL (Bash)
# One application, including status, latest decision and members.
curl --silent --show-error "https://aegis-api.theauroraedge.com/v1/applications/$CLIENT_ID" \
-H "Authorization: Bearer $AEGIS_API_KEY"
# First page. Continue using nextCursor while hasMore is true.
curl --silent --show-error --get 'https://aegis-api.theauroraedge.com/v1/applications' \
-H "Authorization: Bearer $AEGIS_API_KEY" --data-urlencode 'limit=100'
# Next page, with CURSOR set to the exact nextCursor from the previous response.
curl --silent --show-error --get 'https://aegis-api.theauroraedge.com/v1/applications' \
-H "Authorization: Bearer $AEGIS_API_KEY" --data-urlencode 'limit=100' --data-urlencode "cursor=$CURSOR"
Lists return data, nextCursor and hasMore. The final page has nextCursor: null and hasMore: false. Keep filters unchanged while paging. Concurrent updates can move records between pages, so overlap recovery windows and save your checkpoint only after the whole sweep succeeds. Fetch the individual application when you need its full current snapshot.
Status
What your integration should do
pending
Wait for review. Submission can leave this status unchanged.
needs_info
Help the applicant return to their outstanding requests.
hold
Keep the application unresolved while it is on hold.
pending_approval
Wait for the second approver.
active
Treat the application as approved.
rejected
Record rejection and the applicant-visible rejectionReason.
Preserve unknown statuses without treating them as approval. A successful HTTP request is not an onboarding approval.
6. Register and update status webhooks
Prepare the receiver below, then register its public HTTPS URL on port 443. Create one registration for the workflow, not one for every applicant. Store the returned endpoint ID and its one-time signing secret.
JavaScript
const endpoint = await aegis.registerWebhook({
url: 'https://integrations.example.com/aegis/events',
events: [
'application.submitted',
'application.status.changed',
'application.info_request.raised'
],
description: 'Onboarding updates'
});
const endpointId = endpoint.id;
// Save endpointId in configuration and endpoint.secret in your secret manager.
// Configure the receiver with this secret before sending a test.
const test = await aegis.testWebhook(endpointId);
if (!test.delivered) throw new Error('Check receiver connectivity and signature verification');
Show the cURL request
cURL (Bash)
curl --silent --show-error 'https://aegis-api.theauroraedge.com/v1/webhooks' \
-H "Authorization: Bearer $AEGIS_API_KEY" -H 'Content-Type: application/json' \
--data '{"url":"https://integrations.example.com/aegis/events","events":["application.submitted","application.status.changed","application.info_request.raised"],"description":"Onboarding updates"}'
# After securely configuring the receiver with the returned secret:
curl --silent --show-error "https://aegis-api.theauroraedge.com/v1/webhooks/$ENDPOINT_ID/test" \
-H "Authorization: Bearer $AEGIS_API_KEY" -H 'Content-Type: application/json' --data '{}'
Submission is a separate event because it need not change status. Status events keep outcomes in sync. Information-request events tell you when to bring an applicant back. The test response's delivered field confirms receiver success; HTTP success of the management call alone is insufficient.
Supply the complete desired events array when changing subscriptions. Update and list responses omit the secret. Use status: disabled to pause a registration and status: enabled to resume it. After changing the receiver, test again. See the event catalogue for additional subscriptions.
7. Accept events and refresh current status
Connect a public POST route in your server to handleWebhook below. Pass the untouched request body as a Buffer and delivery headers as a Headers object, then send its returned status with an empty body. Capture bytes before JSON middleware and configure request size limits at your server.
The receiver verifies the signature and tenant, commits the event to your queue, and returns 204. Your worker then reads the application and saves the latest state. Duplicate delivery and out-of-order events are normal; event sequence numbers must not decide which application status is current.
Copy the Node.js receiver and worker functions
aegis-webhooks.mjs
import { createHmac, timingSafeEqual } from 'node:crypto';
// acceptEvent must atomically deduplicate and durably queue the event.
// Resolve only after commit, including for an already accepted duplicate.
export function createWebhookHandler({ tenantId, secrets, acceptEvent }) {
if (!tenantId || !Array.isArray(secrets) || !secrets.length ||
secrets.some(secret => typeof secret !== 'string' || !secret) ||
typeof acceptEvent !== 'function') throw new Error('Configure tenant, secrets and durable acceptance');
return async function handleWebhook(rawBody, headers) {
// headers is a standard Headers object. rawBody is the original Buffer.
const timestamp = headers.get('Aegis-Webhook-Timestamp');
if (!Buffer.isBuffer(rawBody) || !/^\d+$/.test(timestamp ?? '')) return { status: 401 };
const seconds = Number(timestamp);
if (!Number.isSafeInteger(seconds) || Math.abs(Date.now() / 1000 - seconds) > 300) {
return { status: 401 };
}
const signatures = (headers.get('Aegis-Webhook-Signature') ?? '').split(/\s+/)
.filter(value => /^v1=[0-9a-f]{64}$/.test(value))
.map(value => Buffer.from(value.slice(3), 'hex'));
const valid = secrets.some(secret => {
const digest = createHmac('sha256', secret)
.update(timestamp + '.', 'utf8').update(rawBody).digest();
return signatures.some(signature => timingSafeEqual(signature, digest));
});
if (!valid) return { status: 401 };
let event;
try { event = JSON.parse(rawBody.toString('utf8')); } catch { return { status: 400 }; }
if (!event || typeof event.id !== 'string' || !event.id ||
typeof event.type !== 'string' || !event.type ||
event.tenantId !== tenantId ||
event.id !== headers.get('Aegis-Webhook-Id') ||
event.type !== headers.get('Aegis-Webhook-Event')) return { status: 400 };
if (event.type.startsWith('application.') &&
(typeof event.data?.clientId !== 'string' || !event.data.clientId)) return { status: 400 };
try {
await acceptEvent(event);
return { status: 204 };
} catch {
return { status: 503 }; // No acknowledgement when durable acceptance fails.
}
};
}
// Call this from your durable queue worker. Retry the job if processing throws.
export function createReconciler({ aegis, withApplicationLock, saveApplication }) {
return async function reconcile(event) {
if (!event.type.startsWith('application.')) return; // Includes webhook.test.
const clientId = event.data.clientId;
await withApplicationLock(event.tenantId + ':' + clientId, async () => {
const application = await aegis.getApplication(clientId);
await saveApplication(application);
});
};
}
Connection you supply
Required behavior
tenantId and secrets
Use your configured Aegis tenant ID and active secret strings. Use secrets directly as UTF-8; do not decode or strip prefixes.
acceptEvent(event)
In one database transaction, insert an event keyed by tenantId and event.id and create its durable processing job. A duplicate resolves successfully. A commit failure throws.
withApplicationLock(key, work)
Serialize work for one application across all workers, holding the lock until both the read and save finish.
saveApplication(application)
Upsert the current API snapshot by clientId. Resolve only after the database write commits.
These are adapter functions for your database and queue. An in-memory set cannot provide durable deduplication. Mark a queue job complete only after reconciliation succeeds; retry failed jobs. The same event can reach multiple registered endpoints, so use a separate deduplication namespace for independent workflows.
Connect your storage adapters
import { createWebhookHandler, createReconciler } from './aegis-webhooks.mjs';
// Supply acceptEvent, withApplicationLock and saveApplication from your backend.
const handleWebhook = createWebhookHandler({
tenantId: process.env.AEGIS_TENANT_ID,
secrets: [process.env.AEGIS_WEBHOOK_SECRET],
acceptEvent
});
const processJob = createReconciler({ aegis, withApplicationLock, saveApplication });
// POST route: call handleWebhook(rawBody, headers), send its returned status.
// Queue worker: await processJob(event), then acknowledge the job.
Test valid events, invalid signatures, duplicates and receiver failures. A webhook.test event has no application ID and requires no application read. During an outage, use an overlapping updatedSince sweep to recover. Delivery history, replay and secret rotation are covered in the webhook reference.
8. Bring the applicant back for more information
After application.info_request.raised, retrieve the requests and issue a link to the intended current member. Do this in your background workflow after durable event acceptance.
JavaScript
const { data: requests } = await aegis.getInfoRequests(clientId);
const outstanding = requests.filter(request => request.openItems > 0);
if (outstanding.length > 0) {
const followUp = await aegis.issueLink(clientId, undefined, '/requests');
// Privately send followUp.loginUrl to the current owner.
// Pass an existing member email instead of undefined to target that member.
}
Track notification work durably by event ID so worker retries do not send duplicate invitations. Applicants supply answers and documents in the portal. A responded request still needs staff review; it is not approval.
Reusable JavaScript backend helper
Copy this module once and use the step examples from your own authenticated backend handlers. The methods follow the API paths and preserve diagnostics. They do not automatically retry writes that might create a new link or endpoint.
Honor Retry-After on 429 and use bounded backoff for transient failures. A timeout or 5xx can occur after a write committed: read the application or list registrations before repeating the operation. All workers share the per-key budget of 60 requests per minute. See errors and retry guidance.