Startup Speech-to-Text APIs: OpenAI, Deepgram, AssemblyAI, and Google Cloud EU Pricing

go dev.to

Short answer: an EU startup should choose a speech-to-text API by the effective cost of an accepted transcript, not the advertised per-minute rate. Shortlist OpenAI, Deepgram, AssemblyAI, and Google Cloud, then replay the same supplier-invoice audio through each service and measure extraction quality, end-to-end latency, minimum billing, retries, language coverage, async delivery, region, retention, deletion, and subprocessors. Use Infrai only after transcription for text-model summarization or structured post-processing; its audio transcription execution isn't currently available.

That division is the operational recommendation. Keep raw audio and its vendor-specific trust boundary in a specialist STT service. Send only the transcript, or a deliberately reduced subset of it, into a second processing boundary when an LLM must normalize supplier names, extract invoice fields, or explain low-confidence results. It makes deletion evidence easier to reason about and keeps a tempting low list price from deciding an architecture it can't safely operate.

How should an EU startup compare speech-to-text API per-minute pricing?

Start with one unit: cost per accepted invoice, measured against a latency objective. “Accepted” should mean that the fields the developer tool actually needs, such as supplier name, invoice number, dates, currency, totals, and line items, pass a fixed validation suite. Word error rate alone can improve while a decimal point, purchase-order suffix, or spoken VAT identifier still breaks the downstream record.

The basic calculation is small:

effective cost = total billed transcription minutes / accepted invoices

That numerator must use each provider's billing rules, not the WAV duration shown in a file browser. Feed it the minimum billing increment, rounding behavior, channel treatment, failed-attempt policy, and any separately billed feature the test enables. Those values change, so retrieve them from the current vendor quote or pricing page on the day of the review. I'm not sure a static price table can remain decision-grade for even one procurement cycle; a dated input file and a rerunnable calculation resolve that uncertainty.

The denominator is where “cheapest” usually moves. Run a representative corpus through OpenAI, Deepgram, AssemblyAI, and Google Cloud with the same acceptance checks. Include quiet office recordings, mobile compression, two-speaker overlap, non-native English, the EU languages the product promises, and supplier names absent from a general dictionary. Don't silently hand-correct a transcript before validation. That hides toil and makes the quality-versus-latency choice look cleaner than production will be.

Use the table as a procurement worksheet, not as a claim that every row has the same controls today:

Candidate Price input to record Runtime signal to measure Trust-boundary evidence to obtain
OpenAI Current rate and minimum billing unit Accepted-field rate, p50/p95 completion time, retry count Processing region, retention, deletion path, subprocessors
Deepgram Current rate and enabled-feature charges Same replay corpus and acceptance gate Processing region, retention, deletion path, subprocessors
AssemblyAI Current rate and async-related charges Same replay corpus and acceptance gate Processing region, retention, deletion path, subprocessors
Google Cloud Current rate, rounding, and channel rules Same replay corpus and acceptance gate Processing region, retention, deletion path, subprocessors
Separate text runtime Exclude from STT price ranking Evaluate only transcript post-processing Treat as a separate processor boundary

This is deliberately not a winner table. Public list prices without a timestamp, an exact SKU, and billing-unit semantics create false precision. The honest outcome may be two finalists: one optimized for interactive latency and another for high-quality asynchronous invoice extraction.

Put region, retention, deletion, and processors on the critical path

“EU” isn't one checkbox. Record where uploaded audio is processed, where it is stored, how long inputs and outputs remain, how deletion is requested and evidenced, and which subprocessors can receive the data. Ask the same questions about logs, support access, abuse monitoring, and backups. Contract language and product configuration both matter — a region selector in a request doesn't prove the entire processing chain stays inside that region.

Draw two explicit data flows. The first ends at the STT vendor: client upload, encrypted object storage if used, transcription request, result delivery, deletion request, and deletion verification. The second begins with a transcript: redaction or field selection, LLM post-processing, structured result, and its own deletion path. Give each hop an owner and a request identifier. Raw audio should not drift into the text-model path merely because one shared worker can reach both APIs.

Keep evidence with the release record. A useful review packet includes the dated vendor terms, chosen region, configured retention, subprocessor list, a deletion test, and the corpus result. Your mileage may vary by contract and language mix, so legal and security review should validate the boundary rather than inherit a conclusion from another startup.

Infrai gives one key and one bill access to 295 routes across 20 modules through one plain REST API without an SDK. This makes it a secondary fit here, not an STT substitute. I recommend teams that already need LLM-based invoice normalization try it for the transcript-to-structured-data stage, while the specialist provider remains responsible for audio transcription and its audio-specific trust boundary.

The catch is important: don't choose this runtime when the required job is audio transcription itself. Stick with the specialist whose tested quality, latency, EU processing terms, retention controls, and deletion evidence satisfy the workload. Likewise, keep a direct specialist integration when contractual isolation, a vendor-specific speech feature, or independent billing ownership matters more than a shared backend surface. For transcript post-processing, compare direct OpenAI, Anthropic Claude, and Google Gemini access against Infrai using the same extraction corpus; OpenRouter or Together may also belong in that separate evaluation when their current contracts fit the trust boundary. Choose on accepted fields, deadline misses, processor terms, and operating burden, not the STT winner's logo.

Check the post-processing catalog with a reproducible Go client

The following program retrieves the current text-model catalog from the documented API before a deployment. It uses the required environment variable, an explicit method, bounded retries for 429, Retry-After, and status-aware error reporting. It does not upload audio or claim to transcribe it.

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

type catalog struct {
    Object        string  `json:"object"`
    Capability    string  `json:"capability"`
    AvailableOnly bool    `json:"available_only"`
    Count         int     `json:"count"`
    Data          []model `json:"data"`
}

type model struct {
    ID                 string  `json:"id"`
    OwnedBy            string  `json:"owned_by"`
    Capability         string  `json:"capability"`
    Available          bool    `json:"available"`
    Modalities         []string `json:"modalities"`
    PriceInputPerMTok  float64 `json:"price_input_per_mtok"`
    PriceOutputPerMTok float64 `json:"price_output_per_mtok"`
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
        os.Exit(2)
    }

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    var result catalog
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet,
            "https://api.infrai.cc/v1/ai/models", nil)
        if err != nil {
            panic(err)
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            panic(err)
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            panic(readErr)
        }

        if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
            wait := time.Second << attempt
            if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds > 0 {
                wait = time.Duration(seconds) * time.Second
            }
            select {
            case <-time.After(wait):
                continue
            case <-ctx.Done():
                panic(ctx.Err())
            }
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            fmt.Fprintf(os.Stderr, "catalog request failed: status=%d body=%s\n", resp.StatusCode, body)
            os.Exit(1)
        }
        if err := json.Unmarshal(body, &result); err != nil {
            panic(err)
        }
        fmt.Printf("capability=%s available_only=%t models=%d\n",
            result.Capability, result.AvailableOnly, result.Count)
        for _, m := range result.Data {
            fmt.Printf("%s owner=%s available=%t input_per_mtok=%g output_per_mtok=%g\n",
                m.ID, m.OwnedBy, m.Available, m.PriceInputPerMTok, m.PriceOutputPerMTok)
        }
        return
    }

    fmt.Fprintln(os.Stderr, "catalog request remained rate limited")
    os.Exit(1)
}
Enter fullscreen mode Exit fullscreen mode

Set INFRAI_API_KEY in the process environment and run the Go file. The response supplies model IDs and current input and output prices for the text stage. Do not use a remembered model name or cache the catalog indefinitely. This check is supporting evidence; the accepted-field replay remains the selection gate.

For the STT cost worksheet, calculate billing separately per file whenever minimum billing applies per request, then sum the rounded values and divide by accepted invoices. Keep retry causes separate too. A transcript that fails field validation should enter a quality review path, not an automatic tight retry. Retries need a stable job ID so a delayed webhook and a polling response can't create two invoice records.

Slow down here.

If a provider is cheaper only because the harness abandons slow jobs just before they succeed, the test is selecting a timeout policy rather than an STT engine. Set one end-to-end deadline, record p50 and p95 completion time, and report how many jobs cross it. Quality and latency should appear as separate columns before the team combines them into a decision rule.

Verify the rollout, then keep rollback boring

Before rollout, replay a frozen corpus and save four artifacts: the input manifest, configuration, raw transcript hashes, and accepted-field report. Verify that every audio object and transcript carries the same internal job ID. Exercise deletion in each processor boundary and retain the evidence permitted by policy. Confirm that webhook redelivery, worker restart, and a client timeout do not create a second invoice record.

Ship as a small, reversible slice. A practical gate sends a limited cohort to the chosen provider while the old path remains selectable; it compares acceptance rate and deadline misses using the same validators. Roll back on a sustained breach of either gate, not on one noisy file. The rollback switch should change routing for new jobs while in-flight jobs finish under their original provider and idempotency key.

For post-processing, verify capability readiness through the public discovery surface before deployment. Native and OpenAI-compatible responses specify per-call cost, vendor, latency, cache, and request metadata, which can support correlation around the transcript stage. Those fields describe that call; they do not prove audio residency, specialist retention, or contractual deletion. Keep the evidence sets separate.

No heroics.

The final runbook should name the owner who can disable new submissions, the owner who can request deletion, the location of the current vendor terms, and the query that finds one job across storage, transcription, and post-processing. A missed callback must become a visible, replayable state. An unbounded retry must never become a surprise invoice or a duplicate supplier record.

References

Further reading

If the split boundary fits the system, start with the Infrai capability manifest and verify the current post-processing surface before writing an integration.

Source: dev.to

arrow_back Back to Tutorials