Synchronizing
Architectural Primitives

Webhooks and callbacks

The integration quickstart connects registration, event subscriptions and a signed receiver to your application workflow.

Receive application progress, decisions and information-request prompts on your backend. Use signed events to trigger follow-up and reconcile your application records.

Integration setup

  1. Prepare a public HTTPS receiver that preserves raw request bytes, verifies signatures and durably queues accepted events.
  2. Obtain a key with webhooks:manage. Register the receiver and store its one-time secret separately from the API key.
  3. Use the test endpoint to check connectivity and signature verification.
  4. Exercise a real application submission and decision on your test tenant. Check duplicate delivery, ordering, failure recovery and secret rotation before enabling client workflows.

Webhooks call your backend with HTTP POST. They are not browser redirects, and there is no callbackUrl parameter on application creation. For browser handoff use the exact loginUrl returned by the API; redirectPath selects a permitted page inside the Aegis portal. Read API authentication and errors before using the management routes.

Management endpoint directory

Method and pathPurpose
POST /v1/webhooksRegister an endpoint and capture its signing secret once.
GET /v1/webhooksList the tenant endpoint registrations without secrets.
GET /v1/webhooks/{id}Read one registration without its secret.
PATCH /v1/webhooks/{id}Update a destination, subscription, description or enabled state.
DELETE /v1/webhooks/{id}Delete an endpoint registration.
POST /v1/webhooks/{id}/rotate-secretIssue a new signing secret with an overlap for receiver rollout.
POST /v1/webhooks/{id}/testSend a synchronous signed webhook.test event with no applicant data.
GET /v1/webhooks/{id}/deliveriesInspect delivery outcomes and dead letters.

Every route requires webhooks:manage. All example identifiers, domains and secrets are placeholders. The signing secret is returned only when you register an endpoint or rotate its secret.

POST /v1/webhooks

Register an endpoint and capture its signing secret once.

Required scope: webhooks:manage.

Request. Send a JSON body.

FieldTypeDescription
urlrequiredstringPublic HTTPS hostname, port 443, without user information or fragment. IP literals, private/special addresses and redirects are not supported.
eventsrequiredstring[]Nonempty exact event names, or one supported wildcard entry such as application.*, application.decision.* or *. Unknown filters are refused.
descriptionstringOptional human-readable label.
Example request
POST /v1/webhooks HTTP/1.1
Host: aegis-api.theauroraedge.com
Authorization: Bearer aegis_<key-id>_<secret>
Content-Type: application/json

{
  "url": "https://integrations.example.com/aegis/events",
  "events": [
    "application.status.changed",
    "application.info_request.raised"
  ],
  "description": "Onboarding integration"
}

Response. JSON body.

Example response
{
  "id": "wh_123",
  "url": "https://integrations.example.com/aegis/events",
  "events": [
    "application.status.changed",
    "application.info_request.raised"
  ],
  "status": "enabled",
  "description": "Onboarding integration",
  "createdAt": "2026-09-10T09:00:00.000Z",
  "secret": "<one-time-signing-secret>"
}
FieldTypeDescription
idstringWebhook endpoint ID. Store it for management and delivery-history requests.
urlstringRegistered public HTTPS destination.
eventsstring[]Subscribed exact types or supported wildcard filter.
statusstringenabled or disabled.
descriptionstringHuman-readable label supplied at registration.
createdAtstringRFC 3339 UTC creation timestamp.
secretstringReturned only by create and rotate-secret. Store immediately in your secret manager; reads never return it.
  • Default cap is five endpoints per tenant. A staff-configured bounded override may apply. Exceeding the effective cap returns 400 failed-precondition; invalid URL or subscription returns 400 invalid-argument.
  • Do not blindly retry after an ambiguous create failure. List endpoints and reconcile the registration before creating another one.

GET /v1/webhooks

List the tenant endpoint registrations without secrets.

Required scope: webhooks:manage.

Request. No request body.

Example request
GET /v1/webhooks HTTP/1.1
Host: aegis-api.theauroraedge.com
Authorization: Bearer aegis_<key-id>_<secret>

Response. JSON body.

Example response
{
  "data": [
    {
      "id": "wh_123",
      "url": "https://integrations.example.com/aegis/events",
      "events": [
        "application.status.changed",
        "application.info_request.raised"
      ],
      "status": "enabled",
      "description": "Onboarding integration",
      "createdAt": "2026-09-10T09:00:00.000Z"
    }
  ]
}
FieldTypeDescription
dataarrayUnpaged endpoint records using the fields below; [] when none exist.
idstringWebhook endpoint ID. Store it for management and delivery-history requests.
urlstringRegistered public HTTPS destination.
eventsstring[]Subscribed exact types or supported wildcard filter.
statusstringenabled or disabled.
descriptionstringHuman-readable label supplied at registration.
createdAtstringRFC 3339 UTC creation timestamp.
  • No secret, signature or recoverable signing material in list responses.

GET /v1/webhooks/{id}

Read one registration without its secret.

Required scope: webhooks:manage.

Request. No request body.

FieldTypeDescription
idrequiredstring (path)Webhook endpoint ID returned by registration.
Example request
GET /v1/webhooks/{id} HTTP/1.1
Host: aegis-api.theauroraedge.com
Authorization: Bearer aegis_<key-id>_<secret>

Response. JSON body.

Example response
{
  "id": "wh_123",
  "url": "https://integrations.example.com/aegis/events",
  "events": [
    "application.status.changed",
    "application.info_request.raised"
  ],
  "status": "enabled",
  "description": "Onboarding integration",
  "createdAt": "2026-09-10T09:00:00.000Z"
}
FieldTypeDescription
idstringWebhook endpoint ID. Store it for management and delivery-history requests.
urlstringRegistered public HTTPS destination.
eventsstring[]Subscribed exact types or supported wildcard filter.
statusstringenabled or disabled.
descriptionstringHuman-readable label supplied at registration.
createdAtstringRFC 3339 UTC creation timestamp.
  • Use the id returned on registration. When an endpoint is disabled after a receiver failure, inspect delivery history before re-enabling it.

PATCH /v1/webhooks/{id}

Update a destination, subscription, description or enabled state.

Required scope: webhooks:manage.

Request. Send a JSON body.

FieldTypeDescription
idrequiredstring (path)Webhook endpoint ID returned by registration.
urlstringPublic HTTPS hostname, port 443, without user information or fragment. IP literals, private/special addresses and redirects are not supported.
eventsstring[]Nonempty exact event names, or one supported wildcard entry such as application.*, application.decision.* or *. Unknown filters are refused.
statusstringenabled or disabled. Re-enabling clears the failure counter.
descriptionstringHuman-readable label.
Example request
PATCH /v1/webhooks/{id} HTTP/1.1
Host: aegis-api.theauroraedge.com
Authorization: Bearer aegis_<key-id>_<secret>
Content-Type: application/json

{
  "status": "disabled"
}

Response. JSON body.

Example response
{
  "id": "wh_123",
  "url": "https://integrations.example.com/aegis/events",
  "events": [
    "application.status.changed",
    "application.info_request.raised"
  ],
  "status": "disabled",
  "description": "Onboarding integration",
  "createdAt": "2026-09-10T09:00:00.000Z"
}
FieldTypeDescription
idstringWebhook endpoint ID. Store it for management and delivery-history requests.
urlstringRegistered public HTTPS destination.
eventsstring[]Subscribed exact types or supported wildcard filter.
statusstringenabled or disabled.
descriptionstringHuman-readable label supplied at registration.
createdAtstringRFC 3339 UTC creation timestamp.
  • Send only fields being changed. URL validation runs again on updates. No signing secret is returned.
  • Fix the receiver before re-enabling. Enabling does not by itself replay dead deliveries.

DELETE /v1/webhooks/{id}

Delete an endpoint registration.

Required scope: webhooks:manage.

Request. No request body.

FieldTypeDescription
idrequiredstring (path)Webhook endpoint ID returned by registration.
Example request
DELETE /v1/webhooks/{id} HTTP/1.1
Host: aegis-api.theauroraedge.com
Authorization: Bearer aegis_<key-id>_<secret>

Response 204. Empty body. Do not parse as JSON.

  • No request body and no response body. Disable an endpoint before retiring the receiver, and continue acknowledging requests already in flight.

POST /v1/webhooks/{id}/rotate-secret

Issue a new signing secret with an overlap for receiver rollout.

Required scope: webhooks:manage.

Request. Send a JSON body.

FieldTypeDescription
idrequiredstring (path)Webhook endpoint ID returned by registration.
gracePeriodSecondsintegerDefault 86400 (24 hours), range 0 to 604800 (7 days). Zero ends the old secret immediately. Outside the range: 400 invalid-argument.
Example request
POST /v1/webhooks/{id}/rotate-secret HTTP/1.1
Host: aegis-api.theauroraedge.com
Authorization: Bearer aegis_<key-id>_<secret>
Content-Type: application/json

{
  "gracePeriodSeconds": 86400
}

Response. JSON body.

Example response
{
  "id": "wh_123",
  "secret": "<new-one-time-signing-secret>",
  "previousSecretExpiresAt": "2026-09-11T09:00:00.000Z"
}
FieldTypeDescription
idstringEndpoint ID.
secretstringNew secret, returned once. Store it before completing the receiver rollout.
previousSecretExpiresAtstringRFC 3339 UTC expiry of the previous secret.
  • During overlap, deliveries carry one v1 signature per active secret. Accept a match against either secret. At most two secrets are active; another rotation expires the oldest immediately.
  • Coordinate rotation with receiver deployment. Do not retry it blindly if the response is lost; another rotation changes the overlap.

POST /v1/webhooks/{id}/test

Send a synchronous signed webhook.test event with no applicant data.

Required scope: webhooks:manage.

Request. Send a JSON body.

FieldTypeDescription
idrequiredstring (path)Webhook endpoint ID returned by registration.
Example request
POST /v1/webhooks/{id}/test HTTP/1.1
Host: aegis-api.theauroraedge.com
Authorization: Bearer aegis_<key-id>_<secret>
Content-Type: application/json

{}

Response. JSON body.

Example response
{
  "delivered": true,
  "httpStatus": 204,
  "latencyMs": 83,
  "responseSnippet": "",
  "sentHeaders": {
    "Content-Type": "application/json",
    "User-Agent": "Aegis-Webhooks/1.0",
    "Aegis-Webhook-Id": "evt_test_123",
    "Aegis-Webhook-Event": "webhook.test",
    "Aegis-Webhook-Timestamp": "1789030800",
    "Aegis-Webhook-Attempt": "1",
    "Aegis-Webhook-Signature": "v1=<signature>"
  }
}
FieldTypeDescription
deliveredbooleanWhether the receiver returned a successful 2xx response. Check this even when the API request succeeds.
httpStatusnumberHTTP status returned by the receiver, when it responded.
latencyMsnumberTime taken in milliseconds.
responseSnippetstringUp to the first 200 bytes of the test receiver response. Returned for diagnostics, not persisted.
sentHeadersobjectExact outgoing headers for signature diagnostics. Do not write signature values to routine logs.
  • Uses the same signature format as real events. The test does not increment consecutiveFailures or trigger automatic disabling.
  • A successful test proves connectivity and signing, not that a real application event or retry has been delivered.

GET /v1/webhooks/{id}/deliveries

Retrieve delivery history for an endpoint, including deliveries that exhausted their retries. Required scope: webhooks:manage. Replace id with your registered endpoint ID and send no body. Use the history to investigate receiver failures and identify events needing reconciliation or staff replay.

Example request
GET /v1/webhooks/wh_123/deliveries
Authorization: Bearer aegis_<key-id>_<secret>

Delivery history excludes signing secrets and signatures. Staff also have access to delivery diagnostics. Re-enabling an endpoint does not replay dead deliveries; arrange replay with your support contact and reconcile the application through the API.

Event catalogue

A transition can emit both a specific event and application.status.changed, with distinct event IDs and sequence numbers. Subscribe only to the information your workflow needs. There is no application.decision.needs_info event; that condition is reported through info_request.raised and a status change to needs_info.

EventWhenAdditional data fields
application.submittedApplicant submission changes from not true to true.Common application fields only.
application.status.changedAny application status change.reason: decision, info_request, pair_change or repair.
application.decision.proposedApproval proposed; status becomes pending_approval.action: approve; actorRole; comment (string or null).
application.decision.approvedSecond approver countersigns; status becomes active.action: approved; actorRole; comment.
application.decision.proposal_withdrawnProposal returned; status becomes pending.action: proposal_rejected; actorRole; comment.
application.decision.rejectedCase rejected.action: reject; actorRole; comment; rejectionReason.
application.decision.holdCase placed on hold.action: hold; actorRole; comment.
application.info_request.raisedRequest created, items added or answered items reopened.requestId, requestSeq, round, openItems, comment; items: {itemId, type, prompt}[]. Types: question, field, document.
application.info_request.answeredApplicant completes a response round.requestId, requestSeq, round, answeredItems; actorRole: customer.
application.info_request.closedAll request items accepted.requestId, requestSeq, round, actorRole.
application.info_request.cancelledRequest withdrawn or automatically cancelled by a decision or intake change.requestId, requestSeq, reason, actorRole.
webhook.testExplicit test send.data contains endpointId only; no application fields.

Envelope and payload fields

Example decision event
{
  "id": "evt_example_123",
  "type": "application.decision.rejected",
  "apiVersion": "v1",
  "occurredAt": "2026-09-10T09:00:00.000Z",
  "tenantId": "tenant_example",
  "idempotencyKey": "evt_example_123",
  "data": {
    "clientId": "a1b2c3d4-1111-4222-8333-123456789abc",
    "externalRef": "ORDER-88213",
    "status": "rejected",
    "previousStatus": "pending",
    "sequence": 12,
    "action": "reject",
    "actorRole": "analyst",
    "comment": "Unable to complete onboarding.",
    "rejectionReason": "Required evidence was not provided."
  }
}
FieldTypeDescription
idstringStable event ID across retries and every subscribed endpoint. Not a delivery-attempt ID.
type, apiVersionstringCatalogue event type and payload API version, initially v1.
occurredAtstringRFC 3339 UTC time of the state change, for display. Read the application for its current state.
tenantIdstringTenant routing identity. Validate it against the tenants your receiver expects.
idempotencyKeystringEquals id. Deduplication identity, distinct from your outbound API Idempotency-Key header.
data.clientId, externalRefstring, string | nullApplication ID and your reference. Present on application events only.
data.status, previousStatusstring, string | nullResulting status and preceding status. previousStatus is null on submitted, or equals status when no status change occurred.
data.sequenceintegerStrictly increasing event allocation number per application. Delayed event creation can make it differ from the chronological order of state changes. Filtered subscriptions can have gaps.
data.actorRolestring, where specifiedRole such as analyst, mlro, admin, super_admin or customer. Never a staff email.
data.requestSeqnumber, request eventsInformation-request sequence, distinct from the application event sequence. Other additions are in the catalogue.
Example information request data
{
  "clientId": "a1b2c3d4-1111-4222-8333-123456789abc",
  "externalRef": "ORDER-88213",
  "status": "needs_info",
  "previousStatus": "pending",
  "sequence": 14,
  "requestId": "request_123",
  "requestSeq": 1,
  "round": 1,
  "openItems": 1,
  "comment": "Please provide the current licence.",
  "items": [{ "itemId": "item_123", "type": "document", "prompt": "Upload the current trade licence." }]
}

The information-request example is the data object inside the same envelope, with type application.info_request.raised. webhook.test instead has data containing only endpointId. Payloads exclude risk ratings, screening/sanctions/adverse-media results, form answers, applicant responses, files and filenames. Staff prompts and applicant-visible comments are included.

Verify the signature

Example delivery headers
Content-Type: application/json
User-Agent: Aegis-Webhooks/1.0
Aegis-Webhook-Id: evt_example_123
Aegis-Webhook-Event: application.decision.rejected
Aegis-Webhook-Timestamp: 1789030800
Aegis-Webhook-Attempt: 1
Aegis-Webhook-Signature: v1=<lowercase-hex> v1=<overlap-signature>

The timestamp is Unix seconds for the delivery attempt, not occurredAt. Attempt starts at 1. The signature header contains space-separated v1 values, one per active secret during rotation. Compute HMAC-SHA256 over timestamp, a literal dot and the exact raw body bytes. Do not parse and reserialize JSON before verification.

Signing input
hex(HMAC_SHA256(secret, timestamp + "." + rawBody))
  1. Capture raw bytes before JSON middleware. Read and validate the timestamp as integer seconds.
  2. Reject timestamps more than 300 seconds before or after your clock.
  3. Calculate the HMAC for each active secret you hold and compare v1 values in constant time.
  4. After verification, parse the signed body and check id and type match their headers and tenantId is expected.
  5. Atomically deduplicate and persist or enqueue the event before acknowledging. A previously accepted event gets 2xx without duplicate work.
  6. Return 2xx quickly, then perform business processing asynchronously. Return failure if you could not persist the event.

Use the secret returned by registration or rotation directly as an opaque UTF-8 string. Do not base64-decode it, hex-decode it or strip a prefix. Pass your active secret strings to this Node.js helper. Verify signatures before parsing JSON; then check the envelope and durably accept the event.

Node.js signature verification
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyAegis(rawBody, timestamp, signature, secrets, now = Date.now()) {
  if (!Buffer.isBuffer(rawBody) || typeof timestamp !== 'string' || !/^\d+$/.test(timestamp)) return false;
  const seconds = Number(timestamp);
  if (!Number.isSafeInteger(seconds) || Math.abs(now / 1000 - seconds) > 300) return false;
  const candidates = String(signature ?? '').split(/\s+/)
    .filter(value => /^v1=[0-9a-f]{64}$/.test(value))
    .map(value => Buffer.from(value.slice(3), 'hex'));
  return secrets.some(secret => {
    if (typeof secret !== 'string' || !secret) return false;
    const expected = createHmac('sha256', secret)
      .update(timestamp + '.', 'utf8').update(rawBody).digest();
    return candidates.some(candidate =>
      candidate.length === expected.length && timingSafeEqual(candidate, expected));
  });
}

Retries, ordering and acknowledgement

BehaviorContract
DeliveryAt least once, with duplicates possible and no ordering guarantee, even for one application.
SuccessAny HTTP 200 to 299. Redirects are failures and are not followed.
Timeout5-second connection limit and 10-second total timeout. Persist and acknowledge promptly.
Default attempts12 attempts including the initial send, bounded by 24 hours from the first attempt. Your service configuration may specify a different bounded attempt limit.
BackoffNominal retry delays: 30s, 1m, 2m, 4m, 8m, 16m, 32m, 64m, 128m, 256m, then 6h. Each delay is randomly selected between half and all of its nominal value.
Elapsed timeNominal total 14h 31m 30s for 12 attempts; jitter usually shortens it. The 24-hour bound covers delayed delivery scheduling.
Retry-AfterReceiver 429 and 503 can supply Retry-After. A valid value is honored within the delivery cap; otherwise the normal retry schedule applies.
Dead letterExhausted deliveries become dead, visible to staff and tenant. No automatic replay after exhaustion.
Receiver 410Immediately disables the endpoint, with a reason explicitly identifying HTTP 410.
Other automatic disabling20 consecutive deliveries reach dead, or at least one dead delivery with no success for 24 hours. Staff are notified.
Use events to trigger reconciliation, not to overwrite current status
Deduplicate by the verified event ID and process every unseen event. A higher sequence identifies a later allocation, not necessarily a newer status: an event can be generated after the underlying state has changed again. Fetch GET application for current status. Serialize this reconciliation per clientId so concurrent reads cannot overwrite each other out of order.

Store deduplication durably across receiver restarts. If multiple endpoints serve independent workflows, scope deduplication by the workflow or endpoint as well as event ID. Do not assume a redelivery arrives within the five-minute signature window; the delivery timestamp is distinct from the event time. Retain deduplication records for the agreed retry and manual-replay horizon.

Receiver operations and recovery

After a failure, inspect delivery history, fix the receiver, test a signed request and re-enable if needed. Arrange staff replay for dead deliveries. Read known applications after an outage and use the application list with updatedSince to sweep a window of changes. Overlap windows and deduplicate by clientId.

Use a public HTTPS hostname on port 443. No IP-literal URL, user information, fragment or private/special address is accepted. The destination is validated at registration, update and every delivery. Receiver response bodies are not persisted for real events; only the test endpoint returns a short diagnostic snippet. Log event IDs and outcomes instead of signatures, secrets or payload contents.

For rotation, capture the new secret, deploy verification with overlap and remove the old key only after previousSecretExpiresAt. API-key rotation has no such webhook grace period. Track contract changes in the changelog.