Run Visa Copy from your own code
Visa Copy translates a Chinese official document - a bank deposit certificate, an employment, income or retirement certificate, a business licence, a household register page and the like - into English for a visa file. Its one metered task, translate, is available over HTTP. It takes the document as the page has read it - the English document type (doc_type), the date style (date_style), the original one numbered line at a time (L1, L2 ...) and the facts the page read for certain for free (every date and amount with its English form, every ID, account and phone number, every name in passport-style pinyin, the standard English for the terms it knows, and its own warnings) - and returns an English title, one English entry per line, one note per real problem, a verdict (ready or check) and a note. The run is on gpt-terra and metered per run; /estimate is free.
The division of labour first: the page reads the document for free - dates, amounts, ID and account numbers, names, known terms - and the run writes the English. The page reads up to 8,000 characters and sends at most 120 lines a run. After the reply it checks that every line sent is translated exactly once and in order, that every date and amount it read appears in its English form in the English of its own line (with the English words for an amount written in capital numerals), that every ID, account and phone number is copied digit for digit, that every name uses its spelling, that no line brings in a number the original line does not have, that no Chinese stays outside parentheses, that every warning it raised is carried as a note, and that the verdict follows the notes. The translation is shown beside the original, line by line, and prints as a two-page packet - the original, then the English with a translator's statement - or is copied or downloaded as .txt, .md or .json. Your code can do the same with the engine files the page loads (see "Build the input yourself"). Not a certified translation service, not legal or immigration advice, and not affiliated with any embassy or visa office (or with Visa Inc.); the translator's statement is yours to sign, and some visa offices ask for a certified or sworn translation instead.
Base URL and the response shape
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
The request example throughout is the deposit sample's exact wire input (the first worked example below), saved as input.json. The token is minted for this app (the guest call takes {"slug":"visa-copy"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required key (task, doc_type, date_style, lines, facts) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: the translate run is metered and needs a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field, the input and the rules
Every run sends task: translate (the one task is translate); a missing or different one is still answered as translate. Every value is a string. What each key holds, in the words of the system prompt, with the first sample's value (its first line where it has several):
| Key | Required | What it holds |
|---|---|---|
task | yes | always translate. |
doc_type | yes | the English document type the page recognised, or unknown. First sample: Certificate of Deposit |
date_style | yes | D Month YYYY (5 March 2024) or Month D, YYYY (March 5, 2024). First sample: D Month YYYY |
lines | yes | the original, one line each, numbered L1., L2., ... First sample: L1. 存款证明 ... |
facts | yes | what the page read for certain: every date and amount with its English form, every ID, account and phone number, every name in passport-style pinyin, the standard English for known terms, and the page's own warnings. First sample: Document type: 存款证明 -> Certificate of Deposit. ... |
retry_note | no | Only on a reformat retry: why the previous reply was unusable. |
The paste has four keys (DocKit.form): text (the document's Chinese text, line by line as printed), dest (the visa the translation is for), spellings (optional lines such as 吴正业 = WU Zhengye: the spelling on the passport, which wins over the page's pinyin) and translator (a name for the translator's statement on the print; it is never sent). dest sets only the date style; an unknown one reads as other:
| dest | Visa for | date_style |
|---|---|---|
uk | United Kingdom | D Month YYYY |
us | United States | Month D, YYYY |
ca | Canada | D Month YYYY |
au | Australia | D Month YYYY |
schengen | Schengen area | D Month YYYY |
other | Other | D Month YYYY |
What the page reads for free
Lines. Windows line breaks read as plain ones, an ideographic space becomes a space, every line has its runs of white space squeezed to one, and empty lines are dropped. The text is read up to 8,000 characters and the first 120 lines are sent (L1, L2 ...); the page warns about the rest. A text with fewer than 10 Chinese characters is not sent at all.
Document type (DocKit.docTypeOf): the first title found in the first three lines (else anywhere) gives doc_type; none gives unknown and the run names it from the text.
| Key | Chinese titles | Sent as |
|---|---|---|
deposit | 存款证明, 存款证实书, 存单证明, 资信证明 | Certificate of Deposit |
statement | 交易明细, 流水, 对账单, 账户明细 | Bank Statement |
income | 收入证明, 薪资证明, 工资证明 | Certificate of Income |
employment | 在职证明, 工作证明, 任职证明, 在职收入证明 | Certificate of Employment |
retirement | 退休证明, 退休证, 离休证 | Certificate of Retirement |
property | 不动产权证书, 房屋所有权证, 房产证, 房产证明 | Real Estate Title Certificate |
license | 营业执照 | Business License |
hukou | 户口簿, 常住人口登记卡, 户籍证明 | Household Register |
birth | 出生医学证明, 出生证明 | Birth Certificate |
marriage | 结婚证, 婚姻登记 | Marriage Certificate |
divorce | 离婚证 | Divorce Certificate |
record | 无犯罪记录证明, 无犯罪证明 | Certificate of No Criminal Record |
degree | 学位证书 | Degree Certificate |
diploma | 毕业证书 | Graduation Certificate |
enrolment | 在读证明, 在校证明, 学籍证明 | Certificate of Enrolment |
leave | 准假证明, 请假证明, 同意出国 | Leave Approval Letter |
Dates (DocKit.datesIn): 2024年3月5日 (or 号), 二〇二四年三月五日, 2024.03.05, 2024-3-5, 2024/3/5, an eight-digit 20240305 (only when it is a real date and not part of a longer number) and month-only forms such as 2024年3月 (March 2024). Each is checked against the calendar and written in the date style; one that cannot exist is sent as NOT A REAL DATE and raises bad_date.
Amounts (DocKit.amountsIn): capital numerals such as 伍拾叁万陆仟元整 (with 角 and 分), and figures with a currency mark before them (¥, RMB, CNY, 人民币, USD, $, 美元, HKD, EUR, GBP, JPY ...) or 元, 万元 or 万 after them, so years, IDs and counts are never read as money. 万 multiplies by 10,000 (34.2万元 is CNY 342,000.00). Each is written as a currency code, thousands commas and two decimals; one in capital numerals also gets its English words (Five Hundred Thirty-Six Thousand Yuan Only, ... Yuan Seven Jiao Eight Fen). An amount in capital numerals and a figure of the same currency on the same or the next line must agree.
Numbers (DocKit.numbersIn, DocKit.residentId): an 18-character mainland resident ID has its ISO 7064 MOD 11-2 check character recomputed and its birth date and sex read (the 17th digit is odd for male); the birth date and sex are compared with what the document states. An 11-digit number starting 13 to 19 is a phone number; any other run of 10 to 30 digits is an account or reference number. Digits that belong to a date or an amount are not counted again.
Names (DocKit.namesIn, DocKit.nameToPinyin): a name of two to four characters after a label (姓名, 户名, 存款人, 账户名称, 申请人, 持卡人, 权利人, 户主, 联系人, 法定代表人 and others) or after 兹证明, 兹有 or 证明, spelled SURNAME Given in passport style: surname readings (曾 ZENG, 单 SHAN), two-character surnames (欧阳 OUYANG) and ü written yu after l and n (吕 LYU). A character with more than one reading raises name_check; one with no reading raises name_missing. A spelling typed in spellings wins and is marked (as on the passport).
Terms (DocKit.glossaryIn): the standard English for 82 terms and the major Chinese banks, longest match first.
The 82 terms
| Chinese | English |
|---|---|
| 中国工商银行 | Industrial and Commercial Bank of China |
| 中国农业银行 | Agricultural Bank of China |
| 中国建设银行 | China Construction Bank |
| 中国银行 | Bank of China |
| 交通银行 | Bank of Communications |
| 招商银行 | China Merchants Bank |
| 中国邮政储蓄银行 | Postal Savings Bank of China |
| 中信银行 | China CITIC Bank |
| 中国民生银行 | China Minsheng Bank |
| 兴业银行 | Industrial Bank |
| 中国光大银行 | China Everbright Bank |
| 上海浦东发展银行 | Shanghai Pudong Development Bank |
| 平安银行 | Ping An Bank |
| 华夏银行 | Hua Xia Bank |
| 广发银行 | China Guangfa Bank |
| 股份有限公司 | Co., Ltd. |
| 有限责任公司 | Co., Ltd. |
| 有限公司 | Co., Ltd. |
| 分行 | Branch |
| 支行 | Sub-branch |
| 定期存款 | time deposit |
| 活期存款 | demand deposit |
| 整存整取 | lump-sum deposit and withdrawal |
| 存期 | deposit term |
| 起息日 | value date |
| 到期日 | maturity date |
| 冻结 | frozen |
| 止付 | stop payment |
| 余额 | balance |
| 账号 | account number |
| 户名 | account name |
| 币种 | currency |
| 利率 | interest rate |
| 存款人 | depositor |
| 兹证明 | This is to certify that |
| 特此证明 | This certificate is hereby issued. |
| 盖章 | seal |
| 公章 | official seal |
| 人力资源部 | Human Resources Department |
| 职务 | position |
| 职位 | position |
| 部门 | department |
| 入职 | joined the company |
| 月薪 | monthly salary |
| 年收入 | annual income |
| 税前 | before tax |
| 税后 | after tax |
| 劳动合同 | employment contract |
| 法定代表人 | Legal Representative |
| 统一社会信用代码 | Unified Social Credit Code |
| 注册资本 | Registered Capital |
| 成立日期 | Date of Establishment |
| 营业期限 | Business Term |
| 经营范围 | Business Scope |
| 住所 | Domicile |
| 登记机关 | Registration Authority |
| 退休 | retired |
| 养老金 | pension |
| 基本养老保险 | basic pension insurance |
| 社会保险 | social insurance |
| 退休金 | pension |
| 派出所 | Police Station |
| 公安局 | Public Security Bureau |
| 户主 | head of household |
| 籍贯 | place of origin |
| 民族 | ethnicity |
| 汉族 | Han |
| 出生地 | place of birth |
| 身份证 | resident ID card |
| 公民身份号码 | citizen ID number |
| 性别 | sex |
| 不动产权证书 | Real Estate Title Certificate |
| 权利人 | right holder |
| 共有情况 | co-ownership |
| 坐落 | location |
| 建筑面积 | floor area |
| 用途 | use |
| 住宅 | residential |
| 本证明仅用于 | This certificate is only for |
| 签证 | visa |
| 探亲 | family visit |
| 旅游 | tourism |
Findings. What the page raises on its own (form.findings). Every warn goes into facts under Page checks: and must come back as a note on its line; info is shown on the page only.
| Code | Level | Raised by |
|---|---|---|
bad_date | warn | a date that is not on the calendar (30 February, month 13) |
id_checksum | warn | an 18-character resident ID whose check character is wrong - usually one misread digit |
name_missing | warn | a character in a name the pinyin table has no reading for (type the passport spelling) |
name_check | info | a character in a name with more than one reading - match the passport |
garbled | warn | a line with a replacement character (U+FFFD), two or more □ boxes in a row, or three or more question marks |
capital_mismatch | warn | an amount in capital numerals that disagrees with the figure of the same currency on the same or the next line |
id_sex | warn | a line whose 性别 (sex) disagrees with the sex a valid resident ID encodes |
id_birth | warn | a line with 出生 (birth) whose first date disagrees with the birth date a valid resident ID encodes |
DocKit.buildInput writes lines (one line each, L1. and the line as read) and facts (DocKit.factLines): the document type, then one block each - only when it has entries - for the dates, the amounts, the numbers, the names, the terms on one line, and the page checks, each entry with its line id, the original and its English form.
The model's rules
As the system prompt states them, in order:
- Translate every line, in order, one entry per line (
L1,L2, ...). Never merge, split, drop or reorder lines. A line that is only a heading, a stamp or a signature still gets its entry. - Faithful and formal: translate what is written, in the register of an official certificate. Do not add facts, explanations, titles of address the original does not use, or anything about the applicant's purpose that the text does not say. Do not improve or summarise.
- Every date in
factsis written exactly in its given English form. Every amount is written exactly in its given form (currency code, thousands commas, two decimals). An amount written in capital numerals comes with its English words infacts; write both, the figure first:CNY 123,456.78 (in words: One Hundred Twenty-Three Thousand Four Hundred Fifty-Six Yuan Seven Jiao Eight Fen). A line that also states the figure does not repeat it. - ID, account, phone and reference numbers are copied digit for digit.
- Names use the spelling in
facts(SURNAME Given). The first time a person's name appears, follow it with the Chinese in parentheses:WU Zhengye (吴正业). Organisations use the English infactsterms where given; otherwise translate the name and keep the Chinese in parentheses once:Beijing Huaxin Technology Co., Ltd. (北京华信科技有限公司). No other Chinese characters appear in your English. - Seals and signatures: write them in square brackets where they appear:
[Seal: Industrial and Commercial Bank of China Beijing Haidian Sub-branch],[Signature]. A line marked (盖章) or (签字) is a seal or signature line. titleis the document's English title as it would head the translation (for exampleCertificate of Deposit).notes: one entry per real problem a reader must know about - a line that is garbled or unreadable, a figure that disagrees with its words, a date that cannot exist, a name the page could not read. Copy the page's warnings that apply. No notes about style.verdictischeckwhen there is at least one note, otherwiseready.noteis one plain sentence (at most 30 words) for the applicant about the translation as a whole.
And the house rules: use only the input. Never invent a number, date, name or place. No backticks and no input field names (task, doc_type, date_style, lines, facts) in any text you write.
The reply
One JSON object, in the shape the system prompt asks for (shown here with line breaks):
{
"task": "translate",
"title": "",
"lines": [
{
"line": "L1",
"en": ""
}
],
"notes": [
{
"line": "L1",
"note": ""
}
],
"verdict": "ready | check",
"note": ""
}
The page reads the reply with ReplyKit.parseReply (code fences are stripped, and a reply cut short is closed so what arrived can still be read) and ReplyKit.normalize: the verdict matched in upper or lower letters (anything other than ready or check reads as unknown), a line id read from its digits (3 or l3 reads as L3), white space squeezed, a line entry with no id and a note with no text dropped.
ReplyKit.trusted(reply, built) maps each line id to its English: an entry for a line that was not sent is left out, a second entry for the same line is ignored (the first is kept), and the lines are shown in the document's order whatever order they came in.
ReplyKit.check(reply, built) lists, under the page's check: a missing verdict, or one that does not follow the notes (check when there is at least one note, else ready); a missing title; a line not translated, translated twice, or not sent; and on every translated line, a date not written in its English form, an amount whose figure is missing, an amount in capital numerals without its English words, an ID, account or phone number not copied digit for digit, a name not spelled as the page (or the passport) spells it, a number the original line does not have (its own digits, its dates and amounts in every form and the values of the Chinese numerals it spells out are allowed), Chinese outside parentheses, and backticks or an input field name. Every warn finding of the page's own that is not carried as a note on its line is listed too. These are warnings and make the status flagged. As notes only: lines out of order, an amount written without its currency code, and a note past 40 words. Otherwise the status is kept. The verdict on the page reads "Ready to print" (ready) or "Check before you print" (check).
What the page builds from it: ReplyKit.translation(built, reply) is the title and every line in the document's order with its original and its English (a missing one marked); the page shows it beside the original and prints the two-page packet - the original (a photo that never leaves the browser, or the text), then the English under the title with the translator's statement: "I certify that the above is a true and accurate translation of the attached document from Chinese into English." with a name, a signature and a date line. ReplyKit.plain, ReplyKit.md and ReplyKit.json write the same as text, a Markdown table with the notes and what the page read, and JSON. Nothing is sent anywhere else: print, copy or download it yourself.
Worked examples - every sample
These are the free samples on the page (every name, ID, account and phone number in them is invented): the text as pasted, what the page reads from it, the exact input it sends, the reply that replays for free, and what the page does with it - the translation beside the original, the notes, the verdict and its check. Together they cover the verdicts ready and check.
A bank deposit certificate
Visa for uk (United Kingdom), so dates are written D Month YYYY. No passport spellings typed.
The text as pasted:
存款证明
编号:BJ2024-03817
兹证明吴正业(身份证号:110108196504120030)在我行存有定期存款,详情如下:
户名:吴正业 账号:6222020200112345678
币种:人民币 存款金额:人民币(大写)伍拾叁万陆仟元整(小写)¥536,000.00
存期:一年 起息日:2024年3月5日 到期日:2025年3月5日
上述存款在本证明有效期内已冻结,不得支取。
本证明有效期至2024年9月30日。
特此证明。
中国工商银行股份有限公司北京海淀支行(盖章)
二〇二四年三月十一日
The page reads 11 lines and recognises Certificate of Deposit (存款证明).
Dates:
| Line | Original | English |
|---|---|---|
| L6 | 2024年3月5日 | 5 March 2024 |
| L6 | 2025年3月5日 | 5 March 2025 |
| L8 | 2024年9月30日 | 30 September 2024 |
| L11 | 二〇二四年三月十一日 | 11 March 2024 |
Amounts:
| Line | Original | English | In words |
|---|---|---|---|
| L5 | 伍拾叁万陆仟元整 | CNY 536,000.00 | Five Hundred Thirty-Six Thousand Yuan Only (agrees with the figure) |
| L5 | ¥536,000.00 | CNY 536,000.00 | - |
Numbers:
| Line | Number | What it is |
|---|---|---|
| L3 | 110108196504120030 | resident ID, check character valid; born 1965-04-12, male |
| L4 | 6222020200112345678 | account or reference number |
Names:
| Line | Name | Spelled | Check |
|---|---|---|---|
| L3 | 吴正业 | WU Zhengye | - |
Known terms: 兹证明 = This is to certify that; 身份证 = resident ID card; 定期存款 = time deposit; 户名 = account name; 账号 = account number; 币种 = currency; 存期 = deposit term; 起息日 = value date; 到期日 = maturity date; 冻结 = frozen; 特此证明 = This certificate is hereby issued.; 中国工商银行 = Industrial and Commercial Bank of China; 股份有限公司 = Co., Ltd.; 支行 = Sub-branch; 盖章 = seal.
The page raises no findings.
Input:
{
"task": "translate",
"doc_type": "Certificate of Deposit",
"date_style": "D Month YYYY",
"lines": "L1. 存款证明\nL2. 编号:BJ2024-03817\nL3. 兹证明吴正业(身份证号:110108196504120030)在我行存有定期存款,详情如下:\nL4. 户名:吴正业 账号:6222020200112345678\nL5. 币种:人民币 存款金额:人民币(大写)伍拾叁万陆仟元整(小写)¥536,000.00\nL6. 存期:一年 起息日:2024年3月5日 到期日:2025年3月5日\nL7. 上述存款在本证明有效期内已冻结,不得支取。\nL8. 本证明有效期至2024年9月30日。\nL9. 特此证明。\nL10. 中国工商银行股份有限公司北京海淀支行(盖章)\nL11. 二〇二四年三月十一日",
"facts": "Document type: 存款证明 -> Certificate of Deposit.\nDates (write each exactly as given):\n- L6 \"2024年3月5日\" -> 5 March 2024\n- L6 \"2025年3月5日\" -> 5 March 2025\n- L8 \"2024年9月30日\" -> 30 September 2024\n- L11 \"二〇二四年三月十一日\" -> 11 March 2024\nAmounts (write each exactly as given):\n- L5 \"伍拾叁万陆仟元整\" -> CNY 536,000.00 (in words: Five Hundred Thirty-Six Thousand Yuan Only)\n- L5 \"¥536,000.00\" -> CNY 536,000.00\nNumbers (copy digit for digit):\n- L3 110108196504120030 resident ID number (checksum valid; born 1965-04-12; male)\n- L4 6222020200112345678 account or reference number\nNames (use these spellings):\n- L3 吴正业 -> WU Zhengye\nTerms: 兹证明 = This is to certify that; 身份证 = resident ID card; 定期存款 = time deposit; 户名 = account name; 账号 = account number; 币种 = currency; 存期 = deposit term; 起息日 = value date; 到期日 = maturity date; 冻结 = frozen; 特此证明 = This certificate is hereby issued.; 中国工商银行 = Industrial and Commercial Bank of China; 股份有限公司 = Co., Ltd.; 支行 = Sub-branch; 盖章 = seal."
}
Reply:
{
"task": "translate",
"title": "Certificate of Deposit",
"lines": [
{
"line": "L1",
"en": "Certificate of Deposit"
},
{
"line": "L2",
"en": "No.: BJ2024-03817"
},
{
"line": "L3",
"en": "This is to certify that WU Zhengye (吴正业) (Resident ID Card No.: 110108196504120030) has a time deposit with our bank, the details of which are as follows:"
},
{
"line": "L4",
"en": "Account Name: WU Zhengye. Account No.: 6222020200112345678."
},
{
"line": "L5",
"en": "Currency: Renminbi (RMB). Deposit Amount: CNY 536,000.00 (in words: Five Hundred Thirty-Six Thousand Yuan Only)."
},
{
"line": "L6",
"en": "Deposit Term: one year. Value Date: 5 March 2024. Maturity Date: 5 March 2025."
},
{
"line": "L7",
"en": "The above deposit has been frozen during the validity period of this certificate and may not be withdrawn."
},
{
"line": "L8",
"en": "This certificate is valid until 30 September 2024."
},
{
"line": "L9",
"en": "This certificate is hereby issued."
},
{
"line": "L10",
"en": "[Seal: Industrial and Commercial Bank of China Co., Ltd. Beijing Haidian Sub-branch]"
},
{
"line": "L11",
"en": "11 March 2024"
}
],
"notes": [],
"verdict": "ready",
"note": "This is a faithful line-by-line English translation of the certificate of deposit for WU Zhengye; all dates, amounts and ID numbers match the original with no discrepancies."
}
The translation as the page shows and prints it (ReplyKit.translation), under the title Certificate of Deposit:
| Line | Original | English |
|---|---|---|
| L1 | 存款证明 | Certificate of Deposit |
| L2 | 编号:BJ2024-03817 | No.: BJ2024-03817 |
| L3 | 兹证明吴正业(身份证号:110108196504120030)在我行存有定期存款,详情如下: | This is to certify that WU Zhengye (吴正业) (Resident ID Card No.: 110108196504120030) has a time deposit with our bank, the details of which are as follows: |
| L4 | 户名:吴正业 账号:6222020200112345678 | Account Name: WU Zhengye. Account No.: 6222020200112345678. |
| L5 | 币种:人民币 存款金额:人民币(大写)伍拾叁万陆仟元整(小写)¥536,000.00 | Currency: Renminbi (RMB). Deposit Amount: CNY 536,000.00 (in words: Five Hundred Thirty-Six Thousand Yuan Only). |
| L6 | 存期:一年 起息日:2024年3月5日 到期日:2025年3月5日 | Deposit Term: one year. Value Date: 5 March 2024. Maturity Date: 5 March 2025. |
| L7 | 上述存款在本证明有效期内已冻结,不得支取。 | The above deposit has been frozen during the validity period of this certificate and may not be withdrawn. |
| L8 | 本证明有效期至2024年9月30日。 | This certificate is valid until 30 September 2024. |
| L9 | 特此证明。 | This certificate is hereby issued. |
| L10 | 中国工商银行股份有限公司北京海淀支行(盖章) | [Seal: Industrial and Commercial Bank of China Co., Ltd. Beijing Haidian Sub-branch] |
| L11 | 二〇二四年三月十一日 | 11 March 2024 |
No notes.
The verdict: ready - "Ready to print"; it follows the notes.
The note: "This is a faithful line-by-line English translation of the certificate of deposit for WU Zhengye; all dates, amounts and ID numbers match the original with no discrepancies."
The page's check (ReplyKit.check): kept.
No notes.
An employment certificate for a US trip
Visa for us (United States), so dates are written Month D, YYYY. No passport spellings typed.
The text as pasted:
在职证明
兹证明曾乐女士(身份证号:310104199008230041)自2016年7月1日起在我公司工作至今,现任市场部高级经理,劳动合同期限至2027年6月30日。
其税前月薪为人民币28,500元,年收入约为人民币34.2万元。
我公司同意其于2024年5月18日至2024年6月2日赴美国旅游,假期结束后将按时返回工作岗位,并保留其职位。
特此证明。
公司名称:上海星联信息科技有限公司
地址:上海市徐汇区漕溪北路88号
联系人:人力资源部 王静 电话:13917654321
上海星联信息科技有限公司(盖章)
2024年4月22日
The page reads 10 lines and recognises Certificate of Employment (在职证明).
Dates:
| Line | Original | English |
|---|---|---|
| L2 | 2016年7月1日 | July 1, 2016 |
| L2 | 2027年6月30日 | June 30, 2027 |
| L4 | 2024年5月18日 | May 18, 2024 |
| L4 | 2024年6月2日 | June 2, 2024 |
| L10 | 2024年4月22日 | April 22, 2024 |
Amounts:
| Line | Original | English | In words |
|---|---|---|---|
| L3 | 人民币28,500元 | CNY 28,500.00 | - |
| L3 | 人民币34.2万元 | CNY 342,000.00 | from 万 (10,000) |
Numbers:
| Line | Number | What it is |
|---|---|---|
| L2 | 310104199008230041 | resident ID, check character valid; born 1990-08-23, female |
| L8 | 13917654321 | phone number |
Names:
| Line | Name | Spelled | Check |
|---|---|---|---|
| L2 | 曾乐 | ZENG Le | 乐 has more than one reading |
Known terms: 兹证明 = This is to certify that; 身份证 = resident ID card; 劳动合同 = employment contract; 税前 = before tax; 月薪 = monthly salary; 年收入 = annual income; 旅游 = tourism; 职位 = position; 特此证明 = This certificate is hereby issued.; 有限公司 = Co., Ltd.; 人力资源部 = Human Resources Department; 盖章 = seal.
The page's own findings:
infoname_check: L2: 曾乐 is read ZENG Le; 乐 can be read more than one way - match the passport.
Input:
{
"task": "translate",
"doc_type": "Certificate of Employment",
"date_style": "Month D, YYYY",
"lines": "L1. 在职证明\nL2. 兹证明曾乐女士(身份证号:310104199008230041)自2016年7月1日起在我公司工作至今,现任市场部高级经理,劳动合同期限至2027年6月30日。\nL3. 其税前月薪为人民币28,500元,年收入约为人民币34.2万元。\nL4. 我公司同意其于2024年5月18日至2024年6月2日赴美国旅游,假期结束后将按时返回工作岗位,并保留其职位。\nL5. 特此证明。\nL6. 公司名称:上海星联信息科技有限公司\nL7. 地址:上海市徐汇区漕溪北路88号\nL8. 联系人:人力资源部 王静 电话:13917654321\nL9. 上海星联信息科技有限公司(盖章)\nL10. 2024年4月22日",
"facts": "Document type: 在职证明 -> Certificate of Employment.\nDates (write each exactly as given):\n- L2 \"2016年7月1日\" -> July 1, 2016\n- L2 \"2027年6月30日\" -> June 30, 2027\n- L4 \"2024年5月18日\" -> May 18, 2024\n- L4 \"2024年6月2日\" -> June 2, 2024\n- L10 \"2024年4月22日\" -> April 22, 2024\nAmounts (write each exactly as given):\n- L3 \"人民币28,500元\" -> CNY 28,500.00\n- L3 \"人民币34.2万元\" -> CNY 342,000.00\nNumbers (copy digit for digit):\n- L2 310104199008230041 resident ID number (checksum valid; born 1990-08-23; female)\n- L8 13917654321 phone number\nNames (use these spellings):\n- L2 曾乐 -> ZENG Le\nTerms: 兹证明 = This is to certify that; 身份证 = resident ID card; 劳动合同 = employment contract; 税前 = before tax; 月薪 = monthly salary; 年收入 = annual income; 旅游 = tourism; 职位 = position; 特此证明 = This certificate is hereby issued.; 有限公司 = Co., Ltd.; 人力资源部 = Human Resources Department; 盖章 = seal."
}
Reply:
{
"task": "translate",
"title": "Certificate of Employment",
"lines": [
{
"line": "L1",
"en": "Certificate of Employment"
},
{
"line": "L2",
"en": "This is to certify that Ms. ZENG Le (曾乐) (resident ID card No.: 310104199008230041) has been working in our company since July 1, 2016 up to now, currently holding the position of Senior Manager of the Marketing Department, with the employment contract term valid until June 30, 2027."
},
{
"line": "L3",
"en": "Her monthly salary before tax is CNY 28,500.00, and her annual income is approximately CNY 342,000.00."
},
{
"line": "L4",
"en": "Our company agrees that she may travel to the United States for tourism from May 18, 2024 to June 2, 2024; after the vacation ends, she will return to her work post on schedule, and her position will be retained."
},
{
"line": "L5",
"en": "This certificate is hereby issued."
},
{
"line": "L6",
"en": "Company Name: Shanghai Xinglian Information Technology Co., Ltd. (上海星联信息科技有限公司)"
},
{
"line": "L7",
"en": "Address: No. 88 Caoxi North Road, Xuhui District, Shanghai"
},
{
"line": "L8",
"en": "Contact: Human Resources Department, WANG Jing (王静). Tel.: 13917654321"
},
{
"line": "L9",
"en": "[Seal: Shanghai Xinglian Information Technology Co., Ltd.]"
},
{
"line": "L10",
"en": "April 22, 2024"
}
],
"notes": [],
"verdict": "ready",
"note": "This translation matches the certificate of employment exactly as written; no unreadable text, mismatched figures or other issues were found."
}
The translation as the page shows and prints it (ReplyKit.translation), under the title Certificate of Employment:
| Line | Original | English |
|---|---|---|
| L1 | 在职证明 | Certificate of Employment |
| L2 | 兹证明曾乐女士(身份证号:310104199008230041)自2016年7月1日起在我公司工作至今,现任市场部高级经理,劳动合同期限至2027年6月30日。 | This is to certify that Ms. ZENG Le (曾乐) (resident ID card No.: 310104199008230041) has been working in our company since July 1, 2016 up to now, currently holding the position of Senior Manager of the Marketing Department, with the employment contract term valid until June 30, 2027. |
| L3 | 其税前月薪为人民币28,500元,年收入约为人民币34.2万元。 | Her monthly salary before tax is CNY 28,500.00, and her annual income is approximately CNY 342,000.00. |
| L4 | 我公司同意其于2024年5月18日至2024年6月2日赴美国旅游,假期结束后将按时返回工作岗位,并保留其职位。 | Our company agrees that she may travel to the United States for tourism from May 18, 2024 to June 2, 2024; after the vacation ends, she will return to her work post on schedule, and her position will be retained. |
| L5 | 特此证明。 | This certificate is hereby issued. |
| L6 | 公司名称:上海星联信息科技有限公司 | Company Name: Shanghai Xinglian Information Technology Co., Ltd. (上海星联信息科技有限公司) |
| L7 | 地址:上海市徐汇区漕溪北路88号 | Address: No. 88 Caoxi North Road, Xuhui District, Shanghai |
| L8 | 联系人:人力资源部 王静 电话:13917654321 | Contact: Human Resources Department, WANG Jing (王静). Tel.: 13917654321 |
| L9 | 上海星联信息科技有限公司(盖章) | [Seal: Shanghai Xinglian Information Technology Co., Ltd.] |
| L10 | 2024年4月22日 | April 22, 2024 |
No notes.
The verdict: ready - "Ready to print"; it follows the notes.
The note: "This translation matches the certificate of employment exactly as written; no unreadable text, mismatched figures or other issues were found."
The page's check (ReplyKit.check): kept.
No notes.
A retirement certificate with a smudge
Visa for au (Australia), so dates are written D Month YYYY. No passport spellings typed.
The text as pasted:
退休证明
姓名:欧阳建华 性别:男 出生日期:1955年1月7日
身份证号:440303195501070010
原工作单位:广州市□□机械厂
该同志于2015年1月31日办理退休手续,现每月领取基本养老金人民币6,830.50元。
特此证明。
广州市海珠区社会保险基金管理中心(盖章)
2024年2月26日
The page reads 8 lines and recognises Certificate of Retirement (退休证明).
Dates:
| Line | Original | English |
|---|---|---|
| L2 | 1955年1月7日 | 7 January 1955 |
| L5 | 2015年1月31日 | 31 January 2015 |
| L8 | 2024年2月26日 | 26 February 2024 |
Amounts:
| Line | Original | English | In words |
|---|---|---|---|
| L5 | 人民币6,830.50元 | CNY 6,830.50 | - |
Numbers:
| Line | Number | What it is |
|---|---|---|
| L3 | 440303195501070010 | resident ID, fails its check character (it should end in 6) |
Names:
| Line | Name | Spelled | Check |
|---|---|---|---|
| L2 | 欧阳建华 | OUYANG Jianhua | - |
Known terms: 退休 = retired; 性别 = sex; 身份证 = resident ID card; 养老金 = pension; 特此证明 = This certificate is hereby issued.; 社会保险 = social insurance; 盖章 = seal.
The page's own findings:
warnid_checksum: L3: ID number 440303195501070010 fails its checksum (the last character should be 6) - a digit was probably misread.warngarbled: L4 has unreadable characters - type that part from the original.
Input:
{
"task": "translate",
"doc_type": "Certificate of Retirement",
"date_style": "D Month YYYY",
"lines": "L1. 退休证明\nL2. 姓名:欧阳建华 性别:男 出生日期:1955年1月7日\nL3. 身份证号:440303195501070010\nL4. 原工作单位:广州市□□机械厂\nL5. 该同志于2015年1月31日办理退休手续,现每月领取基本养老金人民币6,830.50元。\nL6. 特此证明。\nL7. 广州市海珠区社会保险基金管理中心(盖章)\nL8. 2024年2月26日",
"facts": "Document type: 退休证明 -> Certificate of Retirement.\nDates (write each exactly as given):\n- L2 \"1955年1月7日\" -> 7 January 1955\n- L5 \"2015年1月31日\" -> 31 January 2015\n- L8 \"2024年2月26日\" -> 26 February 2024\nAmounts (write each exactly as given):\n- L5 \"人民币6,830.50元\" -> CNY 6,830.50\nNumbers (copy digit for digit):\n- L3 440303195501070010 resident ID number (FAILS its checksum)\nNames (use these spellings):\n- L2 欧阳建华 -> OUYANG Jianhua\nTerms: 退休 = retired; 性别 = sex; 身份证 = resident ID card; 养老金 = pension; 特此证明 = This certificate is hereby issued.; 社会保险 = social insurance; 盖章 = seal.\nPage checks:\n- L3: ID number 440303195501070010 fails its checksum (the last character should be 6) - a digit was probably misread.\n- L4 has unreadable characters - type that part from the original."
}
Reply:
{
"task": "translate",
"title": "Certificate of Retirement",
"lines": [
{
"line": "L1",
"en": "Certificate of Retirement"
},
{
"line": "L2",
"en": "Name: OUYANG Jianhua (欧阳建华) Sex: Male Date of Birth: 7 January 1955"
},
{
"line": "L3",
"en": "Resident ID Card No.: 440303195501070010"
},
{
"line": "L4",
"en": "Former Work Unit: Guangzhou □□ Machinery Factory"
},
{
"line": "L5",
"en": "The said comrade completed retirement formalities on 31 January 2015 and currently receives a basic pension of CNY 6,830.50 per month."
},
{
"line": "L6",
"en": "This certificate is hereby issued."
},
{
"line": "L7",
"en": "[Seal: Guangzhou Haizhu District Social Insurance Fund Management Center (广州市海珠区社会保险基金管理中心)]"
},
{
"line": "L8",
"en": "26 February 2024"
}
],
"notes": [
{
"line": "L3",
"note": "The resident ID card number fails its checksum (the last character should be 6); a digit may have been misread."
},
{
"line": "L4",
"note": "Part of the former work unit's name is unreadable in the original and could not be translated."
}
],
"verdict": "check",
"note": "The ID number fails its checksum and part of the work unit name is unreadable; please verify both against the original."
}
The translation as the page shows and prints it (ReplyKit.translation), under the title Certificate of Retirement:
| Line | Original | English |
|---|---|---|
| L1 | 退休证明 | Certificate of Retirement |
| L2 | 姓名:欧阳建华 性别:男 出生日期:1955年1月7日 | Name: OUYANG Jianhua (欧阳建华) Sex: Male Date of Birth: 7 January 1955 |
| L3 | 身份证号:440303195501070010 | Resident ID Card No.: 440303195501070010 |
| L4 | 原工作单位:广州市□□机械厂 | Former Work Unit: Guangzhou □□ Machinery Factory |
| L5 | 该同志于2015年1月31日办理退休手续,现每月领取基本养老金人民币6,830.50元。 | The said comrade completed retirement formalities on 31 January 2015 and currently receives a basic pension of CNY 6,830.50 per month. |
| L6 | 特此证明。 | This certificate is hereby issued. |
| L7 | 广州市海珠区社会保险基金管理中心(盖章) | [Seal: Guangzhou Haizhu District Social Insurance Fund Management Center (广州市海珠区社会保险基金管理中心)] |
| L8 | 2024年2月26日 | 26 February 2024 |
Notes:
- L3: The resident ID card number fails its checksum (the last character should be 6); a digit may have been misread.
- L4: Part of the former work unit's name is unreadable in the original and could not be translated.
The verdict: check - "Check before you print"; it follows the notes.
The note: "The ID number fails its checksum and part of the work unit name is unreadable; please verify both against the original."
The page's check (ReplyKit.check): kept.
No notes.
Build the input yourself
Load the engine in node (global.window = global; require("./pinyin.js"); require("./dockit.js"); require("./replykit.js")), then:
const K = DocKit, R = ReplyKit;
const form = K.form({ text: "收入证明\n兹证明李明先生自2019年3月1日起在我公司工作,税前月薪为人民币15,000元。\n特此证明。\n2024年6月3日",
dest: "ca", spellings: "李明 = LEE Ming", translator: "" });
// form.lines: [{id, text, dates, amounts, numbers}] - L1, L2 ... one per non-empty line
// form.docType: {key, zh, en} or null; form.style: dmy or mdy (from dest)
// form.dates, form.amounts, form.numbers, form.names, form.glossary: what the page read, each with its line
// form.findings: [{code, level, line, text}] - level warn goes into facts, info stays on the page
const built = K.buildInput(form); // built.ok is false, with built.note, when there is too little Chinese text
// built.warn: what the page warns about (a clipped text, lines past 120)
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output));
// R.check(reply, built) // {status: kept or flagged, notes}
// R.trusted(reply, built) // {byLine, missing, extra, dup, order}
// R.translation(built, reply) // {title, lines: [{id, zh, en, missing}]}
// R.plain(built, reply) // the English as text, with the translator's statement
That gives this wire input - an income certificate for a Canadian visa, two dates, one amount, and a name whose passport spelling overrides the page's pinyin (LI Ming):
{
"task": "translate",
"doc_type": "Certificate of Income",
"date_style": "D Month YYYY",
"lines": "L1. 收入证明\nL2. 兹证明李明先生自2019年3月1日起在我公司工作,税前月薪为人民币15,000元。\nL3. 特此证明。\nL4. 2024年6月3日",
"facts": "Document type: 收入证明 -> Certificate of Income.\nDates (write each exactly as given):\n- L2 \"2019年3月1日\" -> 1 March 2019\n- L4 \"2024年6月3日\" -> 3 June 2024\nAmounts (write each exactly as given):\n- L2 \"人民币15,000元\" -> CNY 15,000.00\nNames (use these spellings):\n- L2 李明 -> LEE Ming (as on the passport)\nTerms: 兹证明 = This is to certify that; 税前 = before tax; 月薪 = monthly salary; 特此证明 = This certificate is hereby issued.."
}
Paste keys: text (the document's Chinese text, line by line), dest (uk, us, ca, au, schengen or other; only us gives Month D, YYYY), spellings (lines of Chinese name = Passport Spelling, or empty) and translator (a name for the statement, or empty; never sent). The engine sends nothing anywhere: it only reads the text you give it.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://visa-copy.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://visa-copy.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// visa-copy.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://visa-copy.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://visa-copy.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// VisaCopy.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class VisaCopy {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://visa-copy.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://visa-copy.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://visa-copy.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://visa-copy.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"visa-copy"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "visa-copy"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "visa-copy" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "visa-copy"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "visa-copy"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "visa-copy" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "visa-copy"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "visa-copy" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra; the run is metered per run.
# input.json: the run input object - {"task": "translate", "doc_type": "Certificate of Deposit", "date_style": "D Month YYYY", "lines": "L1. 存款证明\nL2. 编号:BJ2024-03817\nL3. 兹证明吴正业(身份证号:110108196504120030)在我行存有定期存款,详情如下:\nL4. 户名:吴正业 账号:6222020200112345678\nL5. 币种:人民币 存款金额:人民币(大写)伍拾叁万陆仟元整(小写)¥536,000.00\nL6. 存期:一年 起息日:2024年3月5日 到期日:2025年3月5日\nL7. 上述存款在本证明有效期内已冻结,不得支取。\nL8. 本证明有效期至2024年9月30日。\nL9. 特此证明。\nL10. 中国工商银行股份有限公司北京海淀支行(盖章)\nL11. 二〇二四年三月十一日", "facts": "Document type: 存款证明 -> Certificate of Deposit.\nDates (write each exactly as given):\n- L6 \"2024年3月5日\" -> 5 March 2024\n- L6 \"2025年3月5日\" -> 5 March 2025\n- L8 \"2024年9月30日\" -> 30 September 2024\n- L11 \"二〇二四年三月十一日\" -> 11 March 2024\nAmounts (write each exactly as given):\n- L5 \"伍拾叁万陆仟元整\" -> CNY 536,000.00 (in words: Five Hundred Thirty-Six Thousand Yuan Only)\n- L5 \"¥536,000.00\" -> CNY 536,000.00\nNumbers (copy digit for digit):\n- L3 110108196504120030 resident ID number (checksum valid; born 1965-04-12; male)\n- L4 6222020200112345678 account or reference number\nNames (use these spellings):\n- L3 吴正业 -> WU Zhengye\nTerms: 兹证明 = This is to certify that; 身份证 = resident ID card; 定期存款 = time deposit; 户名 = account name; 账号 = account number; 币种 = currency; 存期 = deposit term; 起息日 = value date; 到期日 = maturity date; 冻结 = frozen; 特此证明 = This certificate is hereby issued.; 中国工商银行 = Industrial and Commercial Bank of China; 股份有限公司 = Co., Ltd.; 支行 = Sub-branch; 盖章 = seal."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "translate", "doc_type": "Certificate of Deposit", "date_style": "D Month YYYY", "lines": "L1. 存款证明\nL2. 编号:BJ2024-03817\nL3. 兹证明吴正业(身份证号:110108196504120030)在我行存有定期存款,详情如下:\nL4. 户名:吴正业 账号:6222020200112345678\nL5. 币种:人民币 存款金额:人民币(大写)伍拾叁万陆仟元整(小写)¥536,000.00\nL6. 存期:一年 起息日:2024年3月5日 到期日:2025年3月5日\nL7. 上述存款在本证明有效期内已冻结,不得支取。\nL8. 本证明有效期至2024年9月30日。\nL9. 特此证明。\nL10. 中国工商银行股份有限公司北京海淀支行(盖章)\nL11. 二〇二四年三月十一日", "facts": "Document type: 存款证明 -> Certificate of Deposit.\nDates (write each exactly as given):\n- L6 \"2024年3月5日\" -> 5 March 2024\n- L6 \"2025年3月5日\" -> 5 March 2025\n- L8 \"2024年9月30日\" -> 30 September 2024\n- L11 \"二〇二四年三月十一日\" -> 11 March 2024\nAmounts (write each exactly as given):\n- L5 \"伍拾叁万陆仟元整\" -> CNY 536,000.00 (in words: Five Hundred Thirty-Six Thousand Yuan Only)\n- L5 \"¥536,000.00\" -> CNY 536,000.00\nNumbers (copy digit for digit):\n- L3 110108196504120030 resident ID number (checksum valid; born 1965-04-12; male)\n- L4 6222020200112345678 account or reference number\nNames (use these spellings):\n- L3 吴正业 -> WU Zhengye\nTerms: 兹证明 = This is to certify that; 身份证 = resident ID card; 定期存款 = time deposit; 户名 = account name; 账号 = account number; 币种 = currency; 存期 = deposit term; 起息日 = value date; 到期日 = maturity date; 冻结 = frozen; 特此证明 = This certificate is hereby issued.; 中国工商银行 = Industrial and Commercial Bank of China; 股份有限公司 = Co., Ltd.; 支行 = Sub-branch; 盖章 = seal."}
# - see the input table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "translate", doc_type, date_style, lines, facts} (see the input table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses visa-copy:<task>:<inputHash>:a<attempt> (the one task is translate), and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="visa-copy:translate:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r '.title, .verdict, .note' reply.json
jq -r '.lines[]? | "\(.line): \(.en)"' reply.json # one per line of the original, in order
jq -r '.notes[]? | "note \(.line): \(.note)"' reply.json # empty when the verdict is ready
import hashlib, time
key = "visa-copy:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"))
print(reply.get("title", ""))
print(reply.get("verdict"))
print(reply.get("note", ""))
for x in reply.get("lines") or []: # one per line of the original, in order
print(x["line"] + ":", x["en"])
for x in reply.get("notes") or []: # empty when the verdict is ready
print("note", x["line"] + ":", x["note"])
import { createHash } from "node:crypto";
const key = `visa-copy:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated);
console.log(reply.title);
console.log(reply.verdict);
console.log(reply.note);
for (const x of reply.lines || []) console.log(`${x.line}: ${x.en}`); // one per line of the original, in order
for (const x of reply.notes || []) console.log(`note ${x.line}: ${x.note}`); // empty when the verdict is ready
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "visa-copy:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
fmt.Println(j["charged_credits"], j["truncated"])
fmt.Println(reply["title"])
fmt.Println(reply["verdict"])
fmt.Println(reply["note"])
lines, _ := reply["lines"].([]any) // one per line of the original, in order
for _, x := range lines {
l := x.(map[string]any)
fmt.Printf("%v: %v\n", l["line"], l["en"])
}
notes, _ := reply["notes"].([]any) // empty when the verdict is ready
for _, x := range notes {
n := x.(map[string]any)
fmt.Printf("note %v: %v\n", n["line"], n["note"])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "visa-copy:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits"));
System.out.println(reply.path("title").asText());
System.out.println(reply.path("verdict").asText());
System.out.println(reply.path("note").asText());
for (JsonNode x : reply.path("lines")) // one per line of the original, in order
System.out.println(x.path("line").asText() + ": " + x.path("en").asText());
for (JsonNode x : reply.path("notes")) // empty when the verdict is ready
System.out.println("note " + x.path("line").asText() + ": " + x.path("note").asText());
require "digest"
key = "visa-copy:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"]].inspect
puts reply["title"], reply["verdict"], reply["note"]
(reply["lines"] || []).each { |x| puts "#{x["line"]}: #{x["en"]}" } # one per line of the original, in order
(reply["notes"] || []).each { |x| puts "note #{x["line"]}: #{x["note"]}" } # empty when the verdict is ready
$key = "visa-copy:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], "\n";
echo $reply["title"] ?? "", "\n", $reply["verdict"] ?? "", "\n", $reply["note"] ?? "", "\n";
foreach ($reply["lines"] ?? [] as $x) echo $x["line"], ": ", $x["en"], "\n"; // one per line of the original, in order
foreach ($reply["notes"] ?? [] as $x) echo "note ", $x["line"], ": ", $x["note"], "\n"; // empty when the verdict is ready
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"visa-copy:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]}");
Console.WriteLine($"{reply!["title"]}");
Console.WriteLine($"{reply["verdict"]}");
Console.WriteLine($"{reply["note"]}");
foreach (var x in reply["lines"] as JsonArray ?? new JsonArray()) Console.WriteLine($"{x!["line"]}: {x["en"]}"); // one per line of the original, in order
foreach (var x in reply["notes"] as JsonArray ?? new JsonArray()) Console.WriteLine($"note {x!["line"]}: {x["note"]}"); // empty when the verdict is ready
Step 6 — POST /run-stream
The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: visa-copy:translate:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "translate", "doc_type": "Certificate of Deposit", "date_style": "D Month YYYY", "lines": "L1. 存款证明\nL2. 编号:BJ2024-03817\nL3. 兹证明吴正业(身份证号:110108196504120030)在我行存有定期存款,详情如下:\nL4. 户名:吴正业 账号:6222020200112345678\nL5. 币种:人民币 存款金额:人民币(大写)伍拾叁万陆仟元整(小写)¥536,000.00\nL6. 存期:一年 起息日:2024年3月5日 到期日:2025年3月5日\nL7. 上述存款在本证明有效期内已冻结,不得支取。\nL8. 本证明有效期至2024年9月30日。\nL9. 特此证明。\nL10. 中国工商银行股份有限公司北京海淀支行(盖章)\nL11. 二〇二四年三月十一日", "facts": "Document type: 存款证明 -> Certificate of Deposit.\nDates (write each exactly as given):\n- L6 \"2024年3月5日\" -> 5 March 2024\n- L6 \"2025年3月5日\" -> 5 March 2025\n- L8 \"2024年9月30日\" -> 30 September 2024\n- L11 \"二〇二四年三月十一日\" -> 11 March 2024\nAmounts (write each exactly as given):\n- L5 \"伍拾叁万陆仟元整\" -> CNY 536,000.00 (in words: Five Hundred Thirty-Six Thousand Yuan Only)\n- L5 \"¥536,000.00\" -> CNY 536,000.00\nNumbers (copy digit for digit):\n- L3 110108196504120030 resident ID number (checksum valid; born 1965-04-12; male)\n- L4 6222020200112345678 account or reference number\nNames (use these spellings):\n- L3 吴正业 -> WU Zhengye\nTerms: 兹证明 = This is to certify that; 身份证 = resident ID card; 定期存款 = time deposit; 户名 = account name; 账号 = account number; 币种 = currency; 存期 = deposit term; 起息日 = value date; 到期日 = maturity date; 冻结 = frozen; 特此证明 = This certificate is hereby issued.; 中国工商银行 = Industrial and Commercial Bank of China; 股份有限公司 = Co., Ltd.; 支行 = Sub-branch; 盖章 = seal."}
# - see the input table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "visa-copy:translate:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `visa-copy:translate:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "visa-copy:translate:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "visa-copy:translate:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "visa-copy:translate:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: visa-copy:translate:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"visa-copy:translate:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);