Skip to content

Payment Notification API

Use this endpoint when your payment system needs to tell Traveln that a booking payment has completed, failed, been cancelled, or been refunded.

This is a partner-to-Traveln server callback. Traveln uses it to:

  • validate the notification against the original invoice
  • record the incoming payment event for audit and idempotency
  • update the Traveln invoice status

Endpoint

POST https://integrations.app.traveln.ai/api/v1/payments/notify/

Authentication

Send your tenant partner API key in the X-API-KEY header:

X-API-KEY: <partner_api_key>
Content-Type: application/json

This endpoint does not use Authorization: Api-Key <sso_hmac_secret>.

When to call it

Call this endpoint after your backend has received and validated the final payment result from your gateway or PSP.

Typical flow:

  1. Traveln creates an invoice for the booking flow.
  2. Traveln calls your configured payment API endpoint and hands your system the payment context.
  3. Your system creates the payment session and sends the traveler to your hosted payment flow or gateway page.
  4. Once your backend has a definitive payment outcome, it calls Traveln POST /api/v1/payments/notify/.
  5. Traveln records the event and updates the invoice status.

Use your backend for this handoff. Do not call this endpoint from browser code.

Request body

Field Type Required Description
invoice_id string (UUID) Yes Traveln invoice identifier for the booking payment
transaction_id string Yes Your payment transaction reference or gateway transaction ID
status string Yes Payment outcome. Use paid, failed, cancelled, or refunded
amount string or number Yes Must exactly match the Traveln invoice amount
currency string Yes Must exactly match the Traveln invoice currency
gateway string Yes Must match the tenant payment gateway. Supported values in the current implementation: telr, moyasar

Additional JSON fields are allowed. Traveln stores the full payload for audit purposes, but only the fields above are used for validation and invoice status updates.

Status mapping

Traveln currently maps incoming payment statuses as follows:

Incoming status Traveln invoice status
paid paid
failed failed
cancelled failed
refunded refunded

If you send any other status string, the event is still stored, but the invoice status is not transitioned by this endpoint.

Idempotency

Traveln deduplicates notifications using this tuple:

  • transaction_id
  • status
  • gateway

If the same combination is sent again, Traveln responds with:

{
  "status": "duplicate"
}

This lets your integration safely retry the same notification without creating duplicate audit events or applying the same invoice transition twice.

Validation rules

Traveln accepts the notification only if all of the following are true:

  • the X-API-KEY belongs to an active tenant
  • the invoice_id exists
  • the invoice belongs to the authenticated tenant
  • the gateway matches the tenant's configured payment gateway
  • the amount matches the original invoice amount
  • the currency matches the original invoice currency

On accepted notifications, Traveln also stores transaction_id as the invoice gateway_reference.

Example request

curl "https://integrations.app.traveln.ai/api/v1/payments/notify/" \
  -X POST \
  -H "X-API-KEY: <partner_api_key>" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "invoice_id": "2f9db4bb-8cf6-4c84-a27d-5db8ca7cf0af",
    "transaction_id": "pay_01JQ5V6D4W8VXZ9Q8K53Q0N1B7",
    "status": "paid",
    "amount": "1499.00",
    "currency": "AED",
    "gateway": "moyasar",
    "gateway_payload": {
      "provider_status": "paid"
    }
  }'
const res = await fetch("https://integrations.app.traveln.ai/api/v1/payments/notify/", {
  method: "POST",
  headers: {
    "X-API-KEY": "<partner_api_key>",
    Accept: "application/json",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    invoice_id: "2f9db4bb-8cf6-4c84-a27d-5db8ca7cf0af",
    transaction_id: "pay_01JQ5V6D4W8VXZ9Q8K53Q0N1B7",
    status: "paid",
    amount: "1499.00",
    currency: "AED",
    gateway: "moyasar",
    gateway_payload: {
      provider_status: "paid",
    },
  }),
})

const payload = await res.json()
console.log(payload)
import requests

res = requests.post(
    "https://integrations.app.traveln.ai/api/v1/payments/notify/",
    headers={
        "X-API-KEY": "<partner_api_key>",
        "Accept": "application/json",
        "Content-Type": "application/json",
    },
    json={
        "invoice_id": "2f9db4bb-8cf6-4c84-a27d-5db8ca7cf0af",
        "transaction_id": "pay_01JQ5V6D4W8VXZ9Q8K53Q0N1B7",
        "status": "paid",
        "amount": "1499.00",
        "currency": "AED",
        "gateway": "moyasar",
        "gateway_payload": {
            "provider_status": "paid",
        },
    },
    timeout=30,
)

payload = res.json()
print(payload)

Example responses

Success

{
  "status": "success"
}

Duplicate notification

{
  "status": "duplicate"
}

Error responses

HTTP status Example body Meaning
401 {"error": "Missing API key"} X-API-KEY header was not sent
401 {"error": "Unauthorized"} API key is invalid or the tenant is inactive
400 {"error": "Missing invoice_id"} A required field is missing
404 {"error": "Invoice not found"} invoice_id does not match a Traveln invoice
403 {"error": "Forbidden"} The invoice belongs to a different tenant
400 {"error": "Gateway mismatch"} The provided gateway does not match the tenant configuration
400 {"error": "Amount mismatch"} The amount does not match the original invoice
400 {"error": "Currency mismatch"} The currency does not match the original invoice

Integration notes

  • Your integration must retain the Traveln invoice_id for the payment being reported. This callback cannot work without it.
  • If your current payment-initiation handoff does not already persist invoice_id in your payment record, add that before enabling this callback flow.
  • Send a stable transaction_id for retries. That is what makes duplicate retries safe.
  • Send only final or meaningfully state-changing outcomes. Re-sending the same status is safe, but unnecessary.
  • When status is paid, Traveln marks the invoice as paid and sets paid_at.
  • When status is refunded, Traveln marks the invoice as refunded.