Create a Job from a URL
Use this request to pay for one transcription of an audio or video file that has a public URL. You request a quote, pay it from your wallet with an x402 client, and the service starts the job. To send a local file instead, see Upload a File.
Endpoint
POST https://api.speechischeap.com/v2/x402/jobs/
Authentication
There's no API key. The payment authorizes the job. You pay in native USDC on Base with the x402 exact scheme (see Network and Asset). A standard x402 client does these steps for you:
- Send the request without payment. The response is
402 Payment Requiredwith a base64-encodedPAYMENT-REQUIREDheader and the same quote as JSON. A quote is free and valid for 300 seconds. - Sign the quoted requirement with your wallet, within the price limit you set.
- Send the same request again with the signed proof in the
PAYMENT-SIGNATUREheader. The proof keeps the quote'sresource.url, which carries thepurchase_idthe service issued. The authorization'svalidBeforemust be 6 to 360 seconds after the service's clock when the request arrives. Standard clients set it 300 seconds ahead. - The service settles the payment on Base and answers
202 Acceptedwith aPAYMENT-RESPONSEheader. Confirming the payment takes a few seconds, so the first answer usually has no jobid. Send the same request and proof again until it has one. A replay never charges you again. See Recover a Pending Purchase.
The decoded quote for the example request below has this shape. Always pay the payTo address from the live response:
{
"x402Version": 2,
"resource": {
"url": "https://api.speechischeap.com/v2/x402/jobs/?purchase_id=00000000-1111-4222-8333-444444444444",
"description": "One asynchronous Speech is Cheap transcription",
"mimeType": "application/json"
},
"accepts": [{
"scheme": "exact",
"network": "eip155:8453",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amount": "10000",
"payTo": "<PAY_TO_ADDRESS>",
"maxTimeoutSeconds": 300,
"extra": {
"name": "USD Coin",
"version": "2",
"assetTransferMethod": "eip3009"
}
}]
}
amount is in USDC atomic units, so 10000 is $0.01. See How the Price Is Computed.
Request
Send a JSON body with Content-Type: application/json. The body can be at most 16 KB.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
input_url | string | - | The URL of the audio or video file to transcribe (required). It must be an HTTP or HTTPS URL without credentials, at most 2,048 characters, and publicly reachable. |
input_duration | number | - | The length of the file in seconds, from 6 to 86,400 (required). You supply it, and the service prices it before it fetches the file. A longer file fails and is refunded. See How the Price Is Computed. |
input_type | string | url | url for this request. Use upload to upload a file. |
can_label_audio | boolean | false | When enabled, includes an audio classification label in the transcription. See Output and Add-ons for its output and pricing for its price. |
can_parse_speakers | boolean | false | When enabled, adds speaker_id to each segment based on the speaker's voice. See Output and Add-ons for its output and pricing for its price. |
can_parse_words | boolean | false | When enabled, includes a timecode for every word in the transcription. See Output and Add-ons for its output and pricing for its price. |
can_stream_output | boolean | false | Must be false. x402 jobs are asynchronous, so true fails validation before you pay. |
is_private | boolean | false | When enabled, redacts the original input_url and deletes the segments 12 hours after the job completes. See Private Jobs. |
is_split_channel | boolean | false | When enabled, parses speakers from multi-channel audio by assigning one speaker per channel. Useful for multi-track recordings. See Split Channels. |
language | string | "" | A code from Supported Languages, such as en, in lowercase and at most 16 characters. If omitted, the language is detected for each speech segment. An unsupported code fails the job after payment, and the service refunds it in full. |
minimum_confidence | number | 0.5 | Filter out segments that fall below this confidence threshold, from 0 to 1. Send a JSON number, not a string. See Minimum Confidence. |
output_format | string | "" | Format of the transcript sent to your webhook: srt, vtt, or webvtt (an alias for vtt). See SRT and VTT Output. |
segment_duration | integer | 30 | The maximum duration of a transcription segment in seconds. Send a positive integer. The service adjusts values outside 6 to 30 to that range. |
user_agent | string | Set by the API | The User-Agent header the service sends when it fetches input_url, at most 256 characters. |
webhook_url | string | "" | The URL that receives the job's outcome and refund updates. It must be an HTTP or HTTPS URL without credentials, at most 2,048 characters. See Webhooks. |
Unknown parameters fail validation with a 400, including max_input_duration. The service sets it from what you pay.
Example Requests
Request a Quote
curl --include --request POST \
--url https://api.speechischeap.com/v2/x402/jobs/ \
--header 'Content-Type: application/json' \
--data '{
"input_url": "https://example.com/audio-file.mp3",
"input_duration": 200,
"webhook_url": "https://your-domain.com/webhook"
}'
The response is the 402 quote. A request with a signature needs a wallet, so use an x402 client for the paid request.
Pay with the Example Client
x402-client.mjs is a standalone Node.js 22 script built on @x402/fetch. Install the pinned packages and set SIC_X402_WALLET_MODULE to the path of a module that exports signer, an EVM account with address and signTypedData, such as a viem account. Keep wallet keys in your wallet setup, not in the request JSON or the script.
npm install @x402/core@2.28.0 @x402/evm@2.28.0 @x402/fetch@2.28.0 viem@2.57.7
A minimal wallet module, wallet.mjs, that reads a private key from the environment:
import { privateKeyToAccount } from 'viem/accounts';
export const signer = privateKeyToAccount(process.env.WALLET_PRIVATE_KEY);
Save the request body as request.json, create a private directory for state files, then run the script with a new state file and a price ceiling in US dollars for the whole purchase:
mkdir -p -m 700 "$HOME/.local/state/sic"
SIC_X402_WALLET_MODULE=./wallet.mjs node x402-client.mjs request.json "$HOME/.local/state/sic/purchase-001.json" 0.01
The script signs only a native USDC quote on Base that is within your ceiling. It saves the proof to the state file before it sends the paid request, then replays the same proof every five seconds, for up to five minutes, while the payment is pending. It prints the accepted response as JSON and keeps any upload token in the state file instead of printing it.
To resume after a crash or a lost response, pass - instead of the request file. The script reads the original request from the state file and replays the same proof:
SIC_X402_WALLET_MODULE=./wallet.mjs node x402-client.mjs - "$HOME/.local/state/sic/purchase-001.json" 0.01
- The saved ceiling still applies, so a replay can't accept a changed price.
- If the service declines the proof, the script deletes it, because the service took no payment. Fix the cause, then run the same command again to sign a new quote.
- After a
404or410, the service took no payment. Start a new purchase with a new state file. - After a crash, read the PID in the
.lockfile next to the state file. Confirm that process stopped, then remove only that stale lock. - Never print the state file, a reusable proof, or an upload token.
Response
The service uses standard HTTP status codes to indicate the outcome of your request.
| Status Code | Description |
|---|---|
| 202 Accepted | The service has your proof. The body is a pending payment, the accepted job, or a purchase that ended without a job. |
| 400 Bad Request | Malformed JSON, an invalid option, or a malformed payment proof, with a plain-text message |
| 402 Payment Required | Without a signature: the quote. With a signature: the service rejected the proof or declined the settlement and took no payment. See Declined Payments. |
| 404 Not Found | The quote is unknown, or it expired unpaid and the service removed it. The service took no payment. |
| 409 Conflict | The request or proof conflicts with the original purchase |
| 410 Gone | The quote expired, or the payment authorization ended without settlement. The service took no payment. |
| 413 Content Too Large | The body is larger than 16 KB |
| 429 Too Many Requests | Too many unpaid quote requests from one IP address, about 20 per minute. Wait for the Retry-After delay. Paid requests are never limited. |
| 503 Service Unavailable | x402 payments are switched off, not configured, or temporarily unavailable |
Replaying the same paid request never charges you again. Keep the proof, and replay it after an error or a timeout.
Pending Payment
When the payment isn't confirmed yet, the 202 has no job id. This is the usual first answer to a paid request. Send the same request and proof again.
{
"purchase_id": "00000000-1111-4222-8333-444444444444",
"status": "PENDING",
"payment_status": "stripe_pending"
}
Once the transfer succeeds, the response carries a PAYMENT-RESPONSE header with the settlement, including the transaction hash, even while the payment confirmation is pending. The possible payment_status values are in States.
Accepted Job
Once the payment is confirmed, the same request returns the job:
{
"id": "00000000-1111-4222-8333-555555555555-sic",
"output": {},
"status": "PENDING",
"purchase_id": "00000000-1111-4222-8333-444444444444"
}
Read status rather than assuming PENDING. Use the id to get the job or to cancel it. The service starts the transcription only after the payment is confirmed.
Purchase That Ended Without a Job
The service checks input_url only after you pay. If it can't fetch the file, the purchase fails without a job id, and the service refunds the payment in full:
{
"purchase_id": "00000000-1111-4222-8333-444444444444",
"status": "FAILED",
"admission_expires_at": "2026-09-30T12:05:00.000Z",
"output": {
"error": "Input media could not be reached"
}
}
There's no job to read, so replay the request or use your webhook to see this outcome.
Errors
The errors for this endpoint are listed under x402 API errors. The most likely are a 400 for an invalid input_duration, a 429 when you request too many unpaid quotes, and a 402 with a fresh PAYMENT-REQUIRED header when the service declines your proof. Failures after the service accepts a job are listed under failures after a job is accepted.
Notes
- Save the request, the quote, and the signed proof before you send the paid request, and keep them private. Recover a Pending Purchase explains how to resume.
- Send the same request fields in the paid request as in the quote request. A custom client must also sign the quoted requirement unchanged and keep its
resourceandacceptedfields. Otherwise the service answers409. - The
@x402SDK refuses payments above $1 unless you raise its spend limit withsetSpendControls. A long file or extra add-ons can cost more. - Send the paid request right after you sign it, from a machine with an accurate clock. A
validBeforeoutside the allowed window gets a402with a freshPAYMENT-REQUIREDheader and no payment. - A file shorter than
input_durationcan cost less, and the service refunds the difference. See Excess Refunds. - A
503can mean three things.x402 payments are not enabledmeans x402 is switched off. The service refuses new quotes and first payments until it's switched back on, so don't retry in a loop. A purchase you already paid for continues.x402 payment configuration is incompleteis a service-side problem. Try again later, and email support@speechischeap.com if it lasts. A JSONmessagethat says payment admission is temporarily unavailable is transient: retry the original request and any proof, and don't pay again.