Skip to main content

Confirm the destination before dispatch

Delivery Intelligence lets a merchant propose a destination, ask the recipient to confirm or correct it, and receive a dependable result before fulfilment.

End-to-end flow​

  1. Your backend creates a confirmation request.
  2. In Production, Alkax emails the recipient a secure, time-limited link. Sandbox records a suppressed notification in its sink; open the returned link manually.
  3. The recipient reviews the proposed destination without needing an Alkax account.
  4. They keep it, select another active Alkax place, or create a missing place.
  5. They can add a private entrance photograph, which remains unavailable until moderation approves it.
  6. Alkax records the confirmed or corrected snapshot.
  7. Your backend retrieves the result or receives a signed webhook.

Before you start​

Create a project in the Developer Console and issue an environment-specific credential with the required scopes:

  • delivery:write to create, cancel, resend, and replay;
  • delivery:read to retrieve results.

Sandbox credentials have the form ak_test_<key-id>.<secret> and Production credentials use ak_live_<key-id>.<secret>. Store the full value in a secret manager. It is shown once and must never be placed in browser or mobile code.

export ALKAX_BASE_URL="https://api.alkax.io"
export ALKAX_API_KEY="ak_test_<key-id>.<secret>"

1. Prepare your webhook​

Use a public HTTPS endpoint that returns 2xx only after the event has been durably accepted:

POST https://merchant.example/webhooks/alkax

Alkax rejects non-HTTPS, loopback, and private-network destinations and does not follow redirects.

2. Create a confirmation​

Replace expiresAt with a future UTC timestamp and recipientContact with a test inbox you control before running this request. The payload template below is not executable until those values are supplied.

Generate one idempotency key for each logical order-address attempt. Reuse it only when retrying the identical request.

The Sandbox example does not send mail. Production can send mail, so obtain recipient consent before invoking it.

curl --request POST "$ALKAX_BASE_URL/api/v1/address-confirmations" \
--header "X-Alkax-Key: $ALKAX_API_KEY" \
--header "Idempotency-Key: order-1042-address-v1" \
--header "Content-Type: application/json" \
--header "Accept-Language: fr" \
--data '{
"merchantReference": "ORDER-1042",
"recipientContact": "[email protected]",
"locale": "fr",
"expiresAt": "<UTC timestamp in the future>",
"proposedAddress": {
"placeId": null,
"title": "Bonaberi depot",
"plusCode": "6GCR+8J",
"settlement": "Douala",
"locality": "Bonaberi",
"country": "CM",
"buildingName": null,
"floor": null,
"unit": null,
"latitude": 4.08,
"longitude": 9.66
},
"callbackUrl": "https://merchant.example/webhooks/alkax"
}'

The initial 201 response includes:

  • id, the Alkax request identifier;
  • recipientUrl, returned only on initial creation;
  • webhookSigningSecret, returned only on initial creation when Alkax generates the callback secret.

Store the signing secret immediately. Alkax cannot return it again.

3. Read the lifecycle​

curl "$ALKAX_BASE_URL/api/v1/address-confirmations/<request-id>" \
--header "X-Alkax-Key: $ALKAX_API_KEY" \
--header "Accept-Language: en"
StateMeaning
PendingCreated; the recipient has not opened the active link
OpenedThe recipient opened the confirmation experience
ConfirmedThe recipient accepted the destination
CorrectedThe recipient selected or created a different destination
ExpiredThe time-limited request ended without completion
CancelledThe merchant ended the request

Use the immutable confirmed snapshot for fulfilment. Do not rebuild it from display text or the original proposal. When evaluated, the response can also include confidenceScore, confidenceBand, and confidencePolicyVersion.

4. Verify webhook signatures​

Every webhook includes:

X-Alkax-Event-Id: <UUID>
X-Alkax-Timestamp: <Unix seconds>
X-Alkax-Signature: v1=<hex HMAC-SHA256>

The signed bytes are:

<timestamp>.<raw HTTP request body>

Node.js example:

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyAlkaxWebhook({ rawBody, timestamp, signature, secret }, nowSeconds = Math.floor(Date.now() / 1000)) {
if (!Buffer.isBuffer(rawBody) || typeof timestamp !== "string" || !/^\d{1,12}$/.test(timestamp) ||
Math.abs(nowSeconds - Number(timestamp)) > 300 || typeof signature !== "string" ||
!/^v1=[a-f0-9]{64}$/.test(signature) || typeof secret !== "string" || !secret) return false;
const expected = createHmac("sha256", secret)
.update(`${timestamp}.`, "utf8")
.update(rawBody)
.digest();
const supplied = Buffer.from(signature.slice(3), "hex");
return supplied.length === expected.length && timingSafeEqual(supplied, expected);
}

Process the event in this order:

  1. Reject timestamps outside a five-minute freshness window.
  2. Verify the signature against the unmodified raw body, then compare parsed eventId to the header.
  3. Persist the environment/event identity and raw-body hash with a unique constraint.
  4. If the identity already exists with the same hash, return 2xx without repeating effects. A different hash is an integrity failure; apply no state changes.
  5. Commit the event and your business change before returning 2xx.

Alkax retries timeouts, 408, 425, 429, and 5xx. Event ID and payload remain stable across retries and manual replay.

Operational actions​

ActionEndpointRequired scope
CancelPOST /api/v1/address-confirmations/{id}/cancelwrite
Resend recipient linkPOST /api/v1/address-confirmations/{id}/resendwrite
Replay a dead-letter webhookPOST /api/v1/address-confirmations/webhooks/{eventId}/replaywrite

Resending rotates the recipient token and is rate limited. A cancelled, completed, or expired request cannot send an actionable notification.

Errors and retries​

  • 400: correct the request; do not retry it unchanged.
  • 401: the credential is missing, malformed, invalid, expired, or revoked.
  • 403: the credential does not have the required scope or production access.
  • 404: the request does not exist in this merchant tenant.
  • 409: inspect the lifecycle or idempotency conflict.
  • 429: honour Retry-After and add jittered backoff.
  • 5xx: retry with exponential backoff and the same idempotency key.

Human-readable errors support English, French, Arabic, Portuguese, and Kiswahili. Persist machine-readable codes rather than matching translated text.

Go-live checklist​

  • Keep project credentials and signing secrets in a secret manager.
  • Redact recipient contacts, recipient URLs, private instructions, and entrance media from logs.
  • Preserve the raw webhook body.
  • Deduplicate on event ID.
  • Alert on sustained 429/5xx responses and webhook backlog.
  • Test confirmation, correction, expiry, cancellation, resend, and duplicate webhook delivery.

Recoverable callback contract​

For a recoverable create, generate and durably encrypt a per-attempt secret before calling Alkax. Set optional callbackSigningSecret to exactly 64 lowercase hexadecimal characters produced from 32 random bytes; it requires callbackUrl. The secret is used as UTF-8 text for HMAC and is never echoed. If omitted, Alkax generates a secret and returns it once as webhookSigningSecret.

Set callbackSchemaVersion to v2 to receive the explicit minimal callback below. Omit it or select v1 to retain the legacy callback with PascalCase nested properties and numeric status. HTTP confirmation responses use camelCase properties and string statuses; these three shapes are distinct. The effective version is persisted, and replay preserves the queued event ID and exact payload bytes.

Identical create retries retain the same request and return null one-time fields, including after the original link deadline. A new operation still requires a future expiry. Keep the exact original payload and idempotency key; changed options conflict. Each new attempt needs its own opaque merchant reference within the project/environment. The canonical private response includes deliveryInstructions, createdAt and cancelledAt; it is not a public task projection.

In the callback below, match eventId to the header, validate the raw signature and freshness, durably deduplicate, then fetch the authenticated canonical result. The minimal callback does not contain coordinates, instructions or capabilities. Project subscription events remain a separate channel. Their create/cancel identities are stable per confirmation and transition; this does not eliminate semantic duplicates in the legacy per-confirmation callback channel.

{
"schemaVersion": "v2",
"eventId": "11111111-1111-4111-8111-111111111111",
"type": "address_confirmation.confirmed",
"occurredAt": "2026-10-25T12:00:00+00:00",
"request": {
"id": "22222222-2222-4222-8222-222222222222",
"merchantReference": "opaque-attempt-reference",
"status": "Confirmed",
"expiresAt": "2026-10-26T12:00:00+00:00"
}
}

Sandbox records recipient notifications in a sink and never sends recipient email. Test the returned link manually; Production activation requires hosted acceptance.

Deploy the compatible provider release before using the additive options. Stored canonical status can remain Pending or Opened past the deadline until lifecycle expiry detection runs; background expiry reconciliation remains a release gate. The link deadline does not invalidate an already completed snapshot. Custom pin changes can still retain Confirmed when Place IDs are unchanged: compare the exact canonical snapshot and maintain a destination version, rather than trusting classification alone. The recovery contract alone does not implement VolXpr consent or disclosure. The private hosted presentation is enabled separately by the context option below.

Private community visits​

For a private home visit, add these fields to the normal create request:

{ "presentationContext": "private_visit", "locale": "fr" }

presentationContext is optional; omission preserves the standard delivery experience. Only private_visit is accepted when supplied. It is persisted with the confirmation and participates in idempotency: changing it requires a new attempt and reference. Authenticated create/GET responses echo this server-bound value, allowing an integration to reject an older provider that ignores the option.

The private visit page explains that the home remains private, hides public Place creation and photo collection, and allows an exact custom pin or an optional existing public meeting point. Completion never creates or updates a public Place. The server rejects entranceImageReference for private visits, including requests made outside the UI.

The public response includes effectiveLocale, normalized to en, fr, ar, pt or sw (English fallback). Private-visit email and cold-loaded page use that same requested locale; the page permits switching language without resetting input and supports Arabic RTL. Temporary network/server failures offer retry rather than declaring a capability invalid.

Until the original link expiry, retrying completion with a consumed private-visit token returns only a completion receipt: proposedAddress, confirmedAddress and media are null. GET stays unavailable after completion; the receipt cannot reopen the location or change the result. Other lifecycle and concurrent terminal-transition limitations remain as documented above.