✈
    TravelmodeDevelopers
    🔎/
    🔑Manage API Keys
    Feature
    🌦️Weather🛂Visa🧩Platform🧭Trips🤖Agent Runs📅Events

    Visa Intelligence API · v1

    📖Overview🧪API Reference (Try It)
    Guides
    🚀Getting Started🔐Authentication & Scopes⏱️Rate Limits🔔Changes & Webhooks⚠️Errors
    ⬇️Download openapi.yaml
    Developers / Visa / Changes & Webhooks

    Changes & Webhooks

    Last verified: 2026-06-20 (Task #389 — Visa Intelligence API docs).

    The change-detection feed surfaces edits the crawler detects on official sources — fee schedule updates, new requirements, removed documents, reclassifications. You can consume it two ways: poll the change feed, or subscribe with a signed webhook. Both require the visa:changes scope.

    Polling the change feed

    GET /v1/visa/changes returns a cursor-paginated stream of change events, newest first. By default only pending and reviewed events appear — dismissed events are excluded unless you ask for them with reviewStatus=dismissed.

    curl -s "https://api.travelmode.ai/v1/visa/changes?countryCode=FR&severity=high&limit=20" \
      -H "X-Visa-Intel-Key: vsk_live_..."
    
    {
      "items": [
        {
          "id": 9001,
          "countryCode": "FR",
          "countryName": "France",
          "sourceId": 42,
          "sourceAuthorityName": "France-Visas",
          "changeType": "content_changed",
          "kind": "updated",
          "severity": "medium",
          "summary": "Short-stay fee schedule page updated.",
          "llmSummary": "The published fee table changed for several visa categories.",
          "detectedBy": "crawler",
          "payload": { "diffRatio": 0.12 },
          "affectedDocumentIds": [555],
          "reviewStatus": "reviewed",
          "occurredAt": "2026-06-19T22:14:00.000Z"
        }
      ],
      "nextCursor": "eyJpZCI6OTAwMX0="
    }
    

    Paging

    Pass the returned nextCursor back via ?cursor= until it comes back null:

    async function* allChanges(key, params = "") {
      let cursor = null;
      do {
        const qs = new URLSearchParams(params);
        if (cursor) qs.set("cursor", cursor);
        const res = await fetch(
          `https://api.travelmode.ai/v1/visa/changes?${qs}`,
          { headers: { "X-Visa-Intel-Key": key } },
        );
        if (!res.ok) throw new Error(`changes failed: ${res.status}`);
        const page = await res.json();
        yield* page.items;
        cursor = page.nextCursor;
      } while (cursor);
    }
    

    Useful filters

    ParamNotes
    countryCodeISO-2 destination filter.
    severityOne or more of low,medium,high,critical (legacy info,minor,major still appear on old rows). Repeat the param or comma-separate.
    changeTypeOne or more change types.
    kindadded, updated, removed, reclassified.
    reviewStatusDefaults to pending,reviewed; pass dismissed to include dismissed events.
    sourceId / snapshotIdScope to a single source or crawl snapshot.
    since / untilISO-8601 bounds on occurredAt.

    Subscribing with webhooks

    For production you usually want push delivery instead of polling. Webhook management is customer-scoped: it requires a vsk_live_… customer key with visa:changes. The development master key is rejected (visa_webhook_master_key_forbidden).

    Register an endpoint

    The request body is snake_case (camelCase aliases are also accepted):

    curl -s -X POST "https://api.travelmode.ai/v1/visa/webhooks" \
      -H "X-Visa-Intel-Key: vsk_live_..." \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://example.com/hooks/visa",
        "description": "EU high-severity changes",
        "country_codes": ["FR", "DE", "ES"],
        "min_severity": "high"
      }'
    
    {
      "webhook_id": "wh_8c0f3c2c",
      "url": "https://example.com/hooks/visa",
      "description": "EU high-severity changes",
      "country_codes": ["FR", "DE", "ES"],
      "min_severity": "high",
      "status": "active",
      "secret": "whsec_a1b2c3d4e5f6...",
      "secret_prefix": "whsec_a1b2",
      "created_at": "2026-06-20T12:00:00.000Z"
    }
    

    The secret is returned exactly once. Store it now — only its hash is persisted, and subsequent list/get calls return only secret_prefix.

    Body fields:

    FieldRequiredNotes
    urlyesMust be https:// and resolve to a public address. Private/loopback targets are rejected with visa_webhook_url_blocked.
    descriptionno1–500 chars.
    country_codesno1–250 ISO-2 codes; omit/null to receive every country.
    min_severitynolow (default), medium, high, or critical. Events below this are not delivered.
    metadatanoArbitrary JSON echoed back to you.

    Manage endpoints

    # List your webhooks (no secrets, only secret_prefix)
    curl -s "https://api.travelmode.ai/v1/visa/webhooks" \
      -H "X-Visa-Intel-Key: vsk_live_..."
    
    # Fetch one
    curl -s "https://api.travelmode.ai/v1/visa/webhooks/wh_8c0f3c2c" \
      -H "X-Visa-Intel-Key: vsk_live_..."
    
    # Disable one (soft-delete; history retained)
    curl -s -X DELETE "https://api.travelmode.ai/v1/visa/webhooks/wh_8c0f3c2c" \
      -H "X-Visa-Intel-Key: vsk_live_..."
    

    Debugging delivery

    When an endpoint isn't receiving events, inspect recent attempts:

    curl -s "https://api.travelmode.ai/v1/visa/webhooks/wh_8c0f3c2c/attempts?limit=20" \
      -H "X-Visa-Intel-Key: vsk_live_..."
    

    Each attempt records status, response_status, response_time_ms, a response_body_excerpt, error_message, the request_signature / request_timestamp we sent, and next_retry_at if a retry is pending. Repeated failures eventually disable the endpoint (disabled_reason is set, and a visa.webhook_endpoint_disabled event is recorded).

    Verifying the signature

    Every delivery is signed. Recompute an HMAC over the timestamp and raw body with your stored secret and compare it (constant-time) against the signature header before trusting the payload. Reject deliveries whose timestamp is too old to blunt replay attacks. Event types you may receive: visa.change_detected, visa.webhook_delivery_failed, and visa.webhook_endpoint_disabled.

    Polling vs. webhooks

    Polling GET /changesWebhooks
    Best forBackfill, ad-hoc queries, reconciliationReal-time reaction
    CostCounts against your hourly quotaPush — no polling spend
    Delivery guaranteeYou control itAt-least-once with retries

    A common pattern: subscribe with a webhook for live reaction, and run a periodic GET /changes sweep (using since) to reconcile anything a delivery might have missed.

    Previous
    ← Rate Limits
    Next
    Errors →