Meeting Helper (Long-Audio Async)

A team meeting recording (an hour or two of audio) becomes a clean transcript and a structured summary with decisions, action items and open questions. This recipe also doubles as the canonical example of the asynchronous transcription pattern, which you need any time your audio is too long for a synchronous request.

The workflow at a glance

┌──────────────┐    ┌────────────┐    ┌──────────┐
│ Meeting tool │    │ Your app   │    │ xander   │
│ (Teams, Zoom)│    │            │    │          │
└──────┬───────┘    └─────┬──────┘    └─────┬────┘
       │ recording        │                 │
       │─────────────────>│                 │
       │                  │ 1. upload       │
       │                  │────────────────>│
       │                  │   audioId       │
       │                  │<────────────────│
       │                  │                 │
       │                  │ 2. start async  │
       │                  │    transcribe   │
       │                  │────────────────>│
       │                  │   202 + jobId   │
       │                  │<────────────────│
       │                  │                 │
       │                  │ 3. poll jobs/   │
       │                  │    {jobId}      │
       │                  │ ──────────────> │
       │                  │   202 (running) │
       │                  │ <────────────── │
       │                  │   ... wait ...  │
       │                  │ ──────────────> │
       │                  │   200 + result  │
       │                  │ <────────────── │
       │                  │                 │
       │                  │ 4. summarise    │
       │                  │────────────────>│
       │                  │   meeting notes │
       │                  │<────────────────│

Why async?

The synchronous endpoints (POST /prompt/audioTranscription and POST /prompt/audioTranscription-large) hold the HTTP connection open for the entire duration of the transcription. For short audio that's fine. For an hour-long meeting it means a single request that takes minutes, proxies, load balancers, your own framework's request timeout, browser timeouts and HTTP/2 idle kicks all become risks. The async pattern avoids all of that by returning immediately and letting you poll for the result on your own schedule.

Rule of thumb: if your audio is over 10 minutes, prefer async. If it's over 30 minutes, async is mandatory in practice, synchronous calls of that length will time out somewhere in the network path even though they technically work end-to-end.

Step 1: Upload the audio file and get an ID

For long audio you don't want to base64-encode the entire file into the JSON request body. Instead, upload it once via POST /audiostore/upload as multipart form-data and use the returned numeric ID in the transcription request.

curl -X POST "$XANDER_BASE_URL/audiostore/upload" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -F "audio=@team-meeting-2026-04-11.m4a;type=audio/mp4"

Response is a single integer (the audio ID):

1234

Step 2: Start the async transcription job

Call POST /prompt-async/audioTranscription-large with the audio ID and a segment duration. The endpoint returns immediately with HTTP 202 Accepted, a Location header pointing at the polling endpoint, and a JSON body containing the jobId.

curl -i -X POST "$XANDER_BASE_URL/prompt-async/audioTranscription-large" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "AudioInput": { "AudioId": 1234 },
    "SegmentDurationSeconds": 140,
    "Options": {
      "PromptIdentification": {
        "ProjectId": "your-project-id",
        "Promptname": "meeting-helper/audio_transcription"
      },
      "SessionMetadata": {
        "UseCaseName": "meeting-helper",
        "UseCaseSessionId": "<your meeting id>"
      }
    },
    "TranscriptionOptions": {
      "AudioLanguage":  { "LanguageCodeIso639": "de" },
      "TargetLanguage": { "LanguageCodeIso639": "de" }
    }
  }'

Response:

HTTP/1.1 202 Accepted
Location: https://<host>/prompt-async/audioTranscription-large/jobs/abc-123-def

{ "jobId": "abc-123-def" }

Choosing the segment duration

SegmentDurationSeconds tells xander how to chop the recording up internally before sending the segments to the speech-to-text model. Sensible values:

  • 60-140 seconds for typical meetings, good balance of throughput and segment quality.
  • 10-30 seconds if speaker overlap is heavy or the audio quality is poor, smaller segments give the model less to confuse, but slightly reduce overall throughput.
  • 240+ seconds if audio is clean and you want maximum throughput.

Step 3: Poll for the result

Call GET /prompt-async/audioTranscription-large/jobs/{jobId} repeatedly until you get 200 OK. While the job is still running you'll get 202 Accepted with a status field.

curl -i "$XANDER_BASE_URL/prompt-async/audioTranscription-large/jobs/abc-123-def" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

While running:

HTTP/1.1 202 Accepted
Content-Type: application/json

{ "jobId": "abc-123-def", "status": "Running" }

When complete:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "transcription": "Begrüssung. Heute besprechen wir die Roadmap für Q3 ...",
  "gptResult":      { "plainResponse": "..." },
  "metaInformation": { "retries": 0, "appliedModel": "..." }
}

Recommended polling pattern

Poll at a fixed 3-second interval, with no exponential backoff. xander's transcription jobs are typically faster than network round-trip jitter, so backoff hurts more than it helps. Implement a hard timeout (e.g. 4× the expected duration) so a stuck job doesn't hang your worker forever.

// Pseudo-code
async function pollTranscriptionJob(jobId, timeoutMs = 30 * 60 * 1000) {
  const start = Date.now();
  while (Date.now() - start < timeoutMs) {
    const response = await fetch(
      `${XANDER_BASE_URL}/prompt-async/audioTranscription-large/jobs/${jobId}`,
      { headers: { Authorization: `Bearer ${accessToken}` } }
    );
    if (response.status === 200) {
      return (await response.json()).transcription;
    }
    if (response.status !== 202) {
      throw new Error(`Unexpected status ${response.status}`);
    }
    await new Promise(resolve => setTimeout(resolve, 3000));
  }
  throw new Error('Transcription timed out');
}

Step 4: Summarise the transcript

Now that you have a clean transcript, generate a structured meeting summary with POST /prompt/informationextraction. The exact shape of the summary depends on your prompt template, typical content is decisions, action items (with owner and due date), open questions, attendees and a one-paragraph TL;DR.

curl -X POST "$XANDER_BASE_URL/prompt/informationextraction" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "UserQuery": {
      "UserQueryText": "<the transcript from step 3>"
    },
    "QueryParameter": [],
    "Options": {
      "PromptIdentification": {
        "ProjectId": "your-project-id",
        "Promptname": "meeting-helper/meeting_notes_summary"
      },
      "SessionMetadata": {
        "UseCaseName": "meeting-helper",
        "UseCaseSessionId": "<same meeting id>"
      }
    }
  }'

If you want strictly structured output (e.g. action items as a JSON array your task tracker can ingest directly), use POST /prompt/informationextraction-structured instead and pass an OutputStructure with a JSON schema. See the PDF Document Extraction recipe for an example of how to design the schema.

Gotchas and tips

  • The job lives on the server, not in your client. If your worker process crashes between step 2 and step 3, you can still recover by storing the jobId in your own persistence layer and resuming the polling from a new process. The job continues running on our side regardless of whether you're polling.
  • Don't poll too aggressively. Sub-second polling is wasteful. 3 seconds is a good default. The real bottleneck is the transcription itself, not polling latency.
  • Don't poll too slowly either. 60-second polling means a 10-second job is reported back to you 50 seconds late on average. Find the right balance for your use case.
  • Rate limits apply to polling. Each poll is a billed API call. If you're running 100 concurrent transcriptions and polling each every 3 seconds, that's a sustained 33 requests per second just for polling. Watch the rate-limit headers and space out your polls if you're approaching the limit. See Limits & Quotas.
  • Don't use the synchronous endpoint instead. Yes, technically POST /prompt/audioTranscription-large exists and accepts long audio too. But it holds the HTTP connection open the whole time, which is a recipe for timeouts in your network path. Use the async endpoint for anything over 10 minutes.
  • Speaker diarisation. The current transcription does not output speaker labels (Speaker 1, Speaker 2, …). If you need them, mention it to your xander contact, it's a feature we can prioritise based on demand.

Next

For shorter audio (under 20 minutes), you can use the synchronous transcription endpoint shown in Field Service Voice Intake step 1, which is simpler because it has no polling.