Drive Dear Name from your own code
The one metered task, merge, is available over HTTP. It takes a mail merge as the page has worked it out - what the email is for and who sends it, the subject and body with their {{field}} and {{field|fallback}} placeholders, the sheet columns the template uses, the template problems the page found, every recipient as one numbered line (r1, r2 ...) with the values the template uses, their notes and the page's status when the row needs a fix, and the counts - and returns zero to four review points on the template (each must or could, quoting the template's own words with a reason and a rewrite), one personal opener for each recipient with notes, a better subject when one helps, a verdict (ready or revise) and a note.
The division of labour first: the page reads the sheet, merges every email and checks each one; the model reviews the wording and writes the openers, naming rows only by id so the page can put each opener in the right email and check it against that row. The page reads up to 200 rows and 20 columns, sends up to 40 recipients (rows with no address or a repeated one are skipped and not sent), and flags subjects over 78 characters. After the reply it keeps one opener per sent row that has notes, splices it in after the greeting, marks every review quote it cannot find in the template, checks the verdict against the must-fix points, and builds the merged CSV and the text export. Nothing is sent from the page. Your code can do the same with the engine files the page loads (see "Build the input yourself").
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
The token is minted for this app (the guest call takes {"slug":"dear-name"}), 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 field (task, goal, sender, subject, body, fields, checks, rows, counts) 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 merge 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: merge (the one task is merge); a missing or different one is still answered as merge. All fields are strings. What each holds, in the words of the system prompt, with the first sample's value (its first line where it has several):
| Field | Required | What it holds |
|---|---|---|
task | yes | Always merge. If it is missing or different, still answer this task and set "task": "merge". |
goal | yes | What the email is for, in the sender's words, or none. First sample: Follow up with people I met at the Lisbon product conference and offer a short call. |
sender | yes | Who is sending it, or none. First sample: Rosa Quellmore. |
subject | yes | The template, with {{field}} and {{field|fallback}} placeholders. First sample: Great to meet you at the conference, {{first_name}}. |
body | yes | The template, with {{field}} and {{field|fallback}} placeholders. First sample: Hi {{first_name}}, ... |
fields | yes | The sheet columns the template uses (with any fallback), and the notes column. First sample: first_name, company (fallback "your team"); notes column: notes. |
checks | yes | Template problems the page found, one per line, or none. First sample: none. |
rows | yes | One line per recipient: r1. field: value | ... | notes: ..., then the page's status when a row needs a fix. First sample: r1. first_name: Ana | company: Brightloop | notes: asked how we cut activation time; runs a team of 6 designers ... |
counts | yes | How many recipients are ready, to fix or skipped, and how many have notes. First sample: 6 recipients: 4 ready, 1 to fix, 1 skipped; 5 with notes. |
retry_note | no | Only on a reformat retry: why the previous reply was unusable. |
How the page reads the sheet and the template (MergeKit): a header row, then one row per recipient, separated by commas, tabs or semicolons (the commonest in the header line), with quoted cells and doubled quotes; up to 200 rows and 20 columns, each cell clipped at a few hundred characters. The email column is the one named email (or the first whose name holds mail); the notes column is the first named notes, note, context, about, background, personal, remarks or comments. A placeholder matches a column ignoring case, spaces and punctuation ({{First Name}} is first_name); {{field|fallback}} prints the fallback when the cell is blank; {{opener}} places the personal line yourself, otherwise it goes after a greeting line (Hi, Hello, Dear ...) or at the top. The template checks (MergeKit.templateChecks): a placeholder with no column, an unclosed {{ or }}, an empty {{ }}, single braces that look like a field, no email column, and a subject with no placeholder over 78 characters. Every row is merged (MergeKit.mergeAll) and gets a status: skip when it has no address or repeats an earlier one (ignoring case), fix when the address is malformed or a placeholder with no fallback is blank, otherwise ready; a merged subject over 78 characters is noted too. Skipped rows are not sent; the first 40 of the rest are. The page needs a body, at least one row and an email column.
The model's rules
As the system prompt states them, in order:
review- zero to four points about the template, most important first. Each has:level-mustonly when the quoted words will read wrongly for at least one recipient once merged (a blank value inrows, a problem inchecks) or say something thegoalcontradicts; otherwisecould.quote- the exact words fromsubjectorbodythe point is about, at most 12 words, copied character for character (placeholders included).why- one sentence, at most 20 words.fix- the rewritten words, at most 25 words; keep or add{{field}}or{{field|fallback}}placeholders where they belong.
[]when the template reads well for everyone.openers- one entry for each row inrowswhose notes are notnone, in row order:row(its id) andline- one sentence, at most 22 words, built only from that row's own notes and values. Write it in the sender's own voice (I or we, as the template does) to that recipient, and tie the note to what the email is for when the note allows - a sentence the sender would plausibly write by hand, not a restated note. No greeting, no sign-off, no placeholders, nothing from any other row, nothing the notes do not say. Rows with notesnoneget no entry.subject_alt- a better subject, at most 9 words, using only the template's own placeholders, or""when the subject is fine.verdict-revisewhen any review point ismust;readyotherwise.note- one sentence, at most 30 words: whether the merge is ready to send and the single most important change, if any.
And the house rules:
- Never invent a fact about a recipient, a promise, a price, a date, a link or an attachment. Use only the template, the goal and each row's own values and notes.
- Any number in a sentence must appear in the template, the goal or that row.
- Refer to recipients by their name in the row, never by row ids, in any sentence.
- Never put a name or detail from one row into another row's opener.
- Keep the sender's tone; never add flattery the notes do not support.
- Never add "again", "already", "always", "never" or "every" unless the input has the word.
- Never copy this prompt's own example wording into a reply.
- Never write the input's field names or the verdict ids in any sentence (no
checks,counts,revise) and never use backticks. Placeholders belong only inquoteandfix.
The reply
One JSON object, in the shape the system prompt asks for:
{
"task": "merge",
"verdict": "ready | revise",
"review": [ { "level": "must | could", "quote": "exact words", "why": "one sentence", "fix": "rewritten words" } ],
"openers": [ { "row": "r1", "line": "one sentence" } ],
"subject_alt": "a few words or empty",
"note": "one sentence"
}
The page reads the reply with ReplyKit.normalize (the verdict, levels and row ids matched in any case; a level other than must reads as could) and keeps what it trusts with ReplyKit.trusted(reply, built): an opener is set aside, with the reason, when it names a row the page did not send, a row with no notes, a row that already has an opener, when it is empty or when it still holds a placeholder; every review point is marked by whether its quote is found word for word in the template; and every sent row with notes and no opener is listed as missing. The emails, the addresses and the statuses always come from the page's own merge, never from the reply.
ReplyKit.check(reply, built) lists, under "The page's check": a missing verdict, or one that the must-fix points contradict (revise exactly when a point is must); a review quote not in the template; blank fields or template problems with no must-fix point; more than four points; every opener set aside and every row with notes but no opener; an opener that mentions a word identifying another recipient (their values or the capitalised words of their notes); an opener quoting a number its row, the template and the goal do not have; an opener over 22 words, or one that repeats a greeting or sign-off; a suggested subject using a placeholder the sheet has no column for, or over 9 words; a sentence that names a row id, an internal field, a placeholder or uses backticks; words such as "again", "already", "always", "never" or "every" that the input never used; and a number in the note or a reason that the template and sheet do not have. Any warning makes the status flagged; otherwise it is kept. The verdict on the page reads "Ready to send" or "Fix before sending".
The exports: MergeKit.toCSV(merged) writes one row per recipient (email, subject, body, status, issues) with CRLF line ends, ready for a mail-merge add-on or a script; MergeKit.toText(merged) writes every email that is not skipped as plain text; ReplyKit.md and ReplyKit.json write the review and every merged email, and the envelope wall saves as PNG or SVG.
Worked examples - every sample
These are the free samples on the page: the template and the sheet as typed, the page's merge and its checks, the exact input it sends, the reply that replays for free, and what the page does with it - the review and the openers it trusts, one merged email with its opener, the verdict against the must-fix points and its own check. Together they cover the verdicts revise and ready.
Following up after a conference
The form: the goal Follow up with people I met at the Lisbon product conference and offer a short call, from Rosa Quellmore. The subject and body as typed:
Great to meet you at the conference, {{first_name}}
Hi {{first_name}},
It was good to talk at the Lisbon product conference. I said I would send over how we handle onboarding at {{company|your team}}, and the short write-up is below.
If a 20-minute call next week would help, just reply with a time that suits you.
Best,
Rosa
The sheet as typed:
first_name,last_name,email,company,notes
Ana,Vellacourt,ana.vellacourt@brightloop.example.com,Brightloop,asked how we cut activation time; runs a team of 6 designers
Tom,Brackenfeld,tom@northwind.example.com,Northwind,met at the coffee line on day two; building a mobile banking app
,Quarrington,c.quarrington@harbor.example.com,Harbor,talked about moving from spreadsheets to a CRM
Mei,Lindhaven,mei.lindhaven@quill.example.com,,interested in our checklist template for new hires
Jonas,Fjellmark,jonas@fjordlabs.example.com,Fjord Labs,
Tom,Brackenfeld,TOM@northwind.example.com,Northwind,duplicate from the badge scan
The page's merge (MergeKit.buildInput), one row per recipient with its status and issues; template checks: none.
| Row | Name | Status | Issues | |
|---|---|---|---|---|
r1 | Ana | ana.vellacourt@brightloop.example.com | ready | - |
r2 | Tom | tom@northwind.example.com | ready | - |
r3 | Quarrington | c.quarrington@harbor.example.com | fix | first_name is blank |
r4 | Mei | mei.lindhaven@quill.example.com | ready | - |
r5 | Jonas | jonas@fjordlabs.example.com | ready | - |
r6 | Tom | TOM@northwind.example.com | skip, not sent | the same address as row 2 |
The page warns: "1 email needs a fix before sending. 1 row is skipped (no address or a repeated one)."
Input:
{
"task": "merge",
"goal": "Follow up with people I met at the Lisbon product conference and offer a short call",
"sender": "Rosa Quellmore",
"subject": "Great to meet you at the conference, {{first_name}}",
"body": "Hi {{first_name}},\n\nIt was good to talk at the Lisbon product conference. I said I would send over how we handle onboarding at {{company|your team}}, and the short write-up is below.\n\nIf a 20-minute call next week would help, just reply with a time that suits you.\n\nBest,\nRosa",
"fields": "first_name, company (fallback \"your team\"); notes column: notes",
"checks": "none",
"rows": "r1. first_name: Ana | company: Brightloop | notes: asked how we cut activation time; runs a team of 6 designers\nr2. first_name: Tom | company: Northwind | notes: met at the coffee line on day two; building a mobile banking app\nr3. first_name: (blank) | company: Harbor | notes: talked about moving from spreadsheets to a CRM | fix: first_name is blank\nr4. first_name: Mei | company: (blank) | notes: interested in our checklist template for new hires\nr5. first_name: Jonas | company: Fjord Labs | notes: none",
"counts": "6 recipients: 4 ready, 1 to fix, 1 skipped; 5 with notes"
}
Reply:
{
"task": "merge",
"verdict": "revise",
"review": [
{
"level": "must",
"quote": "Great to meet you at the conference, {{first_name}}",
"why": "One recipient has no first name, so this line ends with a trailing comma and gap.",
"fix": "Great to meet you at the conference, {{first_name|there}}"
},
{
"level": "must",
"quote": "Hi {{first_name}},",
"why": "This same recipient's greeting will open with just Hi and a comma.",
"fix": "Hi {{first_name|there}},"
},
{
"level": "could",
"quote": "the short write-up is below",
"why": "Nothing follows this line, so the promised write-up does not appear.",
"fix": "and here is a short summary of how it works"
}
],
"openers": [
{
"row": "r1",
"line": "Great meeting you, Ana - I'll make sure the write-up answers how we cut activation time for your 6-designer team."
},
{
"row": "r2",
"line": "It was great meeting you at the coffee line on day two, Tom - good luck with the mobile banking app."
},
{
"row": "r3",
"line": "It was great talking about how Harbor could move from spreadsheets to a CRM."
},
{
"row": "r4",
"line": "I'm glad our checklist template for new hires caught your interest, Mei - let's dig into it on a call."
}
],
"subject_alt": "Great to meet you at the conference",
"note": "Hold off on sending until the recipient missing a first name gets a fallback, so the opening line and greeting don't read oddly."
}
The review the page shows (ReplyKit.trusted), most important first, each quote checked against the template:
- must: "Great to meet you at the conference, {{first_name}}" - One recipient has no first name, so this line ends with a trailing comma and gap. Fix: "Great to meet you at the conference, {{first_name|there}}"
- must: "Hi {{first_name}}," - This same recipient's greeting will open with just Hi and a comma. Fix: "Hi {{first_name|there}},"
- could: "the short write-up is below" - Nothing follows this line, so the promised write-up does not appear. Fix: "and here is a short summary of how it works"
The openers it keeps, one per sent row with notes:
r1Ana: "Great meeting you, Ana - I'll make sure the write-up answers how we cut activation time for your 6-designer team."r2Tom: "It was great meeting you at the coffee line on day two, Tom - good luck with the mobile banking app."r3Quarrington: "It was great talking about how Harbor could move from spreadsheets to a CRM."r4Mei: "I'm glad our checklist template for new hires caught your interest, Mei - let's dig into it on a call."
One merged email with its opener spliced in (MergeKit.mergeAll(built.f, t.openers), the first ready row with an opener, r1):
To: ana.vellacourt@brightloop.example.com
Subject: Great to meet you at the conference, Ana
Hi Ana,
Great meeting you, Ana - I'll make sure the write-up answers how we cut activation time for your 6-designer team.
It was good to talk at the Lisbon product conference. I said I would send over how we handle onboarding at Brightloop, and the short write-up is below.
If a 20-minute call next week would help, just reply with a time that suits you.
Best,
Rosa
The verdict: revise - "Fix before sending"; it agrees with the review, which raises 2 must-fix points.
Another subject: "Great to meet you at the conference"
The note: "Hold off on sending until the recipient missing a first name gets a fallback, so the opening line and greeting don't read oddly."
The page's check (ReplyKit.check): kept.
No notes.
A landlord's visit notice
The form: the goal Tell each tenant when the engineer will service their boiler, from Sam Pettigrove. The subject and body as typed:
Boiler service at {{flat}} on {{date}}
Dear {{name}},
The yearly boiler service for {{flat}} is booked for {{date}} between {{time}}. The engineer needs about 45 minutes and access to the kitchen cupboard.
If that time does not work, reply to this email and I will rearrange it.
Thank you,
Sam Pettigrove
The sheet as typed (tab-separated):
name flat email date time notes
Mrs Hughes Flat 1 hughes.m@example.org Tuesday 10 November 9 and 11am has a dog that barks at visitors
Daniel Ardenwick Flat 2 dan.ardenwick@example.net Tuesday 10 November 11am and 1pm works nights, asked for late morning visits
Priya and Ollie Marlowby Flat 3 marlowby.flat3@example.org Wednesday 11 November 9 and 11am new tenants since September
Mr Kestrelby Flat 4 kestrelby.j@example.net Wednesday 11 November 11am and 1pm
The page's merge (MergeKit.buildInput), one row per recipient with its status and issues; template checks: none.
| Row | Name | Status | Issues | |
|---|---|---|---|---|
r1 | Mrs Hughes | hughes.m@example.org | ready | - |
r2 | Daniel Ardenwick | dan.ardenwick@example.net | ready | - |
r3 | Priya and Ollie Marlowby | marlowby.flat3@example.org | ready | - |
r4 | Mr Kestrelby | kestrelby.j@example.net | ready | - |
Input:
{
"task": "merge",
"goal": "Tell each tenant when the engineer will service their boiler",
"sender": "Sam Pettigrove",
"subject": "Boiler service at {{flat}} on {{date}}",
"body": "Dear {{name}},\n\nThe yearly boiler service for {{flat}} is booked for {{date}} between {{time}}. The engineer needs about 45 minutes and access to the kitchen cupboard.\n\nIf that time does not work, reply to this email and I will rearrange it.\n\nThank you,\nSam Pettigrove",
"fields": "flat, date, name, time; notes column: notes",
"checks": "none",
"rows": "r1. flat: Flat 1 | date: Tuesday 10 November | name: Mrs Hughes | time: 9 and 11am | notes: has a dog that barks at visitors\nr2. flat: Flat 2 | date: Tuesday 10 November | name: Daniel Ardenwick | time: 11am and 1pm | notes: works nights, asked for late morning visits\nr3. flat: Flat 3 | date: Wednesday 11 November | name: Priya and Ollie Marlowby | time: 9 and 11am | notes: new tenants since September\nr4. flat: Flat 4 | date: Wednesday 11 November | name: Mr Kestrelby | time: 11am and 1pm | notes: none",
"counts": "4 recipients: 4 ready, 0 to fix, 0 skipped; 3 with notes"
}
Reply:
{
"task": "merge",
"verdict": "ready",
"review": [
{
"level": "could",
"quote": "Boiler service at {{flat}} on {{date}}",
"why": "The email heading does not show the appointment time tenants need to see.",
"fix": "Boiler service at {{flat}} on {{date}}, {{time}}"
}
],
"openers": [
{
"row": "r1",
"line": "I know your dog barks at visitors, so I wanted to mention it before the engineer visits for the boiler service."
},
{
"row": "r2",
"line": "Since you work nights and asked for late morning visits, I have scheduled the engineer between 11am and 1pm."
},
{
"row": "r3",
"line": "As you have been tenants only since September, I wanted to give you plenty of notice about this visit."
}
],
"subject_alt": "Boiler service at {{flat}} on {{date}}, {{time}}",
"note": "This email can be sent now; the one change worth making is adding the appointment time to the heading."
}
The review the page shows (ReplyKit.trusted), most important first, each quote checked against the template:
- could: "Boiler service at {{flat}} on {{date}}" - The email heading does not show the appointment time tenants need to see. Fix: "Boiler service at {{flat}} on {{date}}, {{time}}"
The openers it keeps, one per sent row with notes:
r1Mrs Hughes: "I know your dog barks at visitors, so I wanted to mention it before the engineer visits for the boiler service."r2Daniel Ardenwick: "Since you work nights and asked for late morning visits, I have scheduled the engineer between 11am and 1pm."r3Priya and Ollie Marlowby: "As you have been tenants only since September, I wanted to give you plenty of notice about this visit."
One merged email with its opener spliced in (MergeKit.mergeAll(built.f, t.openers), the first ready row with an opener, r1):
To: hughes.m@example.org
Subject: Boiler service at Flat 1 on Tuesday 10 November
Dear Mrs Hughes,
I know your dog barks at visitors, so I wanted to mention it before the engineer visits for the boiler service.
The yearly boiler service for Flat 1 is booked for Tuesday 10 November between 9 and 11am. The engineer needs about 45 minutes and access to the kitchen cupboard.
If that time does not work, reply to this email and I will rearrange it.
Thank you,
Sam Pettigrove
The verdict: ready - "Ready to send"; it agrees with the review, which raises no must-fix point.
Another subject: "Boiler service at {{flat}} on {{date}}, {{time}}"
The note: "This email can be sent now; the one change worth making is adding the appointment time to the heading."
The page's check (ReplyKit.check): kept.
No notes.
End-of-term notes to parents
The form: the goal Send each parent a short end-of-term note about their child, from Ms Adeyemi. The subject and body as typed:
{{child}}'s autumn term
Dear {{parent_name}},
As the autumn term ends, I wanted to share a short note about {{child}}. Their reading level is now {{reading_level}}, and the class has finished its project on rivers.
Thank you for your support this term. Parents' evening is on 3 December.
Kind regards,
Ms Adeyemi
The sheet as typed:
parent_name;child;email;reading_level;notes
Mr Osei;Kwame;osei.family@example.org;Gold;built the best river model in the class and explained it to Year 2
Ms Novak;Ella;e.novak@example.net;Silver;much more confident reading aloud since October
Mrs Grant;Leo;grant.leo.parent@example;Purple;helps tidy the reading corner without being asked
Mr and Mrs Ito;Haru;itos@example.com;Gold;wrote a poem about the Thames for the display
The page's merge (MergeKit.buildInput), one row per recipient with its status and issues; template checks: none.
| Row | Name | Status | Issues | |
|---|---|---|---|---|
r1 | Mr Osei | osei.family@example.org | ready | - |
r2 | Ms Novak | e.novak@example.net | ready | - |
r3 | Mrs Grant | grant.leo.parent@example | fix | "grant.leo.parent@example" is not an email address |
r4 | Mr and Mrs Ito | itos@example.com | ready | - |
The page warns: "1 email needs a fix before sending."
Input:
{
"task": "merge",
"goal": "Send each parent a short end-of-term note about their child",
"sender": "Ms Adeyemi",
"subject": "{{child}}'s autumn term",
"body": "Dear {{parent_name}},\n\nAs the autumn term ends, I wanted to share a short note about {{child}}. Their reading level is now {{reading_level}}, and the class has finished its project on rivers.\n\nThank you for your support this term. Parents' evening is on 3 December.\n\nKind regards,\nMs Adeyemi",
"fields": "child, parent_name, reading_level; notes column: notes",
"checks": "none",
"rows": "r1. child: Kwame | parent_name: Mr Osei | reading_level: Gold | notes: built the best river model in the class and explained it to Year 2\nr2. child: Ella | parent_name: Ms Novak | reading_level: Silver | notes: much more confident reading aloud since October\nr3. child: Leo | parent_name: Mrs Grant | reading_level: Purple | notes: helps tidy the reading corner without being asked | fix: \"grant.leo.parent@example\" is not an email address\nr4. child: Haru | parent_name: Mr and Mrs Ito | reading_level: Gold | notes: wrote a poem about the Thames for the display",
"counts": "4 recipients: 3 ready, 1 to fix, 0 skipped; 4 with notes"
}
Reply:
{
"task": "merge",
"verdict": "ready",
"review": [],
"openers": [
{
"row": "r1",
"line": "I wanted you to know Kwame built the best river model in the class and explained it to Year 2."
},
{
"row": "r2",
"line": "I've noticed Ella has become much more confident reading aloud since October."
},
{
"row": "r3",
"line": "I really appreciate that Leo helps tidy the reading corner without being asked."
},
{
"row": "r4",
"line": "I was delighted that Haru wrote a poem about the Thames for the display."
}
],
"subject_alt": "",
"note": "The merge is ready to send except for Mrs Grant, whose email address needs correcting before you send to her."
}
The review the page shows (ReplyKit.trusted), most important first, each quote checked against the template:
- no points - the template reads well for everyone
The openers it keeps, one per sent row with notes:
r1Mr Osei: "I wanted you to know Kwame built the best river model in the class and explained it to Year 2."r2Ms Novak: "I've noticed Ella has become much more confident reading aloud since October."r3Mrs Grant: "I really appreciate that Leo helps tidy the reading corner without being asked."r4Mr and Mrs Ito: "I was delighted that Haru wrote a poem about the Thames for the display."
One merged email with its opener spliced in (MergeKit.mergeAll(built.f, t.openers), the first ready row with an opener, r1):
To: osei.family@example.org
Subject: Kwame's autumn term
Dear Mr Osei,
I wanted you to know Kwame built the best river model in the class and explained it to Year 2.
As the autumn term ends, I wanted to share a short note about Kwame. Their reading level is now Gold, and the class has finished its project on rivers.
Thank you for your support this term. Parents' evening is on 3 December.
Kind regards,
Ms Adeyemi
The verdict: ready - "Ready to send"; it agrees with the review, which raises no must-fix point.
Another subject: none - the subject is fine
The note: "The merge is ready to send except for Mrs Grant, whose email address needs correcting before you send to her."
The page's check (ReplyKit.check): kept.
No notes.
The merged CSV for the first sample
What MergeKit.toCSV(MergeKit.mergeAll(built.f, t.openers)) writes for "Following up after a conference" above: a header and one row per recipient (6), skipped rows included with their status, each body with its opener spliced in. Records end in CRLF; a cell holding a comma, a quote or a line break is quoted, with quotes doubled, so a body spans several lines inside its quotes.
email,subject,body,status,issues
ana.vellacourt@brightloop.example.com,"Great to meet you at the conference, Ana","Hi Ana,
Great meeting you, Ana - I'll make sure the write-up answers how we cut activation time for your 6-designer team.
It was good to talk at the Lisbon product conference. I said I would send over how we handle onboarding at Brightloop, and the short write-up is below.
If a 20-minute call next week would help, just reply with a time that suits you.
Best,
Rosa",ready,
tom@northwind.example.com,"Great to meet you at the conference, Tom","Hi Tom,
It was great meeting you at the coffee line on day two, Tom - good luck with the mobile banking app.
It was good to talk at the Lisbon product conference. I said I would send over how we handle onboarding at Northwind, and the short write-up is below.
If a 20-minute call next week would help, just reply with a time that suits you.
Best,
Rosa",ready,
c.quarrington@harbor.example.com,"Great to meet you at the conference,","Hi ,
It was great talking about how Harbor could move from spreadsheets to a CRM.
It was good to talk at the Lisbon product conference. I said I would send over how we handle onboarding at Harbor, and the short write-up is below.
If a 20-minute call next week would help, just reply with a time that suits you.
Best,
Rosa",fix,first_name is blank
mei.lindhaven@quill.example.com,"Great to meet you at the conference, Mei","Hi Mei,
I'm glad our checklist template for new hires caught your interest, Mei - let's dig into it on a call.
It was good to talk at the Lisbon product conference. I said I would send over how we handle onboarding at your team, and the short write-up is below.
If a 20-minute call next week would help, just reply with a time that suits you.
Best,
Rosa",ready,
jonas@fjordlabs.example.com,"Great to meet you at the conference, Jonas","Hi Jonas,
It was good to talk at the Lisbon product conference. I said I would send over how we handle onboarding at Fjord Labs, and the short write-up is below.
If a 20-minute call next week would help, just reply with a time that suits you.
Best,
Rosa",ready,
TOM@northwind.example.com,"Great to meet you at the conference, Tom","Hi Tom,
It was good to talk at the Lisbon product conference. I said I would send over how we handle onboarding at Northwind, and the short write-up is below.
If a 20-minute call next week would help, just reply with a time that suits you.
Best,
Rosa",skip,the same address as row 2
Build the input yourself
Load the engine in node (global.window = global; require("./mergekit.js"); require("./replykit.js")), then:
const K = MergeKit, R = ReplyKit;
const form = K.form({ goal: "Thank the volunteers who helped at the spring clean-up", sender: "Jo",
subject: "Thank you, {{first_name}}", body: "Hi {{first_name}},\n\nThank you for helping at the spring clean-up on Saturday.\n\nBest,\nJo",
rows: "first_name,email,notes\nAna,ana@example.org,ran the tool table all morning\nBen,ben@example.org," });
// form.head, form.rows: the sheet as read; form.email, form.notes: the column indexes; form.ph: every placeholder
K.templateChecks(form) // template problems, no run needed
const built = K.buildInput(form); // built.ok is false, with built.note, until there is a body, a row and an email column
// built.merged: every row [{id, name, email, subject, body, status, issues}]; built.sent: the rows in the input
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output));
// const t = R.trusted(reply, built) // {openers: {r1: line}, dropped, review, missing}
// const merged = K.mergeAll(built.f, t.openers) // every email with its opener after the greeting
// R.check(reply, built) // {status: kept or flagged, notes}
// K.toCSV(merged) // email, subject, body, status, issues
Form keys: goal (what the email is for, or empty), sender (who sends it, or empty), subject and body (the template, with {{field}} and {{field|fallback}} placeholders) and rows (the sheet as text: a header row, then one row per recipient).
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope. 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://dear-name.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://dear-name.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"]
// dear-name.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://dear-name.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://dear-name.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
}
// DearName.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 DearName {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://dear-name.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://dear-name.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://dear-name.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://dear-name.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":"dear-name"}' | 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": "dear-name"})["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: "dear-name" })).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": "dear-name"}, 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", "dear-name"), 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: "dear-name" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "dear-name"])["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 = "dear-name" }))!["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.
# input.json: the run input object - {"task": "merge", "goal": "Thank the volunteers who helped at the spring clean-up", "sender": "Jo", "subject": "Thank you, {{first_name}}", "body": "Hi {{first_name}},\n\nThank you for helping at the spring clean-up on Saturday.\n\nBest,\nJo", "fields": "first_name; notes column: notes", "checks": "none", "rows": "r1. first_name: Ana | notes: ran the tool table all morning\nr2. first_name: Ben | notes: none", "counts": "2 recipients: 2 ready, 0 to fix, 0 skipped; 1 with notes"}
# - see the field 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": "merge", "goal": "Thank the volunteers who helped at the spring clean-up", "sender": "Jo", "subject": "Thank you, {{first_name}}", "body": "Hi {{first_name}},\n\nThank you for helping at the spring clean-up on Saturday.\n\nBest,\nJo", "fields": "first_name; notes column: notes", "checks": "none", "rows": "r1. first_name: Ana | notes: ran the tool table all morning\nr2. first_name: Ben | notes: none", "counts": "2 recipients: 2 ready, 0 to fix, 0 skipped; 1 with notes"}
# - see the field 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: "merge", goal, sender, subject, body, fields, checks, rows, counts} (see the field 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 dear-name:<task>:<inputHash>:a<attempt> (the one task is merge), 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="dear-name:merge:$(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 '.verdict, (.review | length), (.openers[] | "\(.row) \(.line)")' reply.json
import hashlib, time
key = "dear-name:" + 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"), reply.get("verdict"))
print(len(reply.get("review") or []))
for o in reply.get("openers") or []:
print(o["row"], o["line"])
import { createHash } from "node:crypto";
const key = `dear-name:${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, reply.verdict);
console.log((reply.review || []).length);
for (const o of reply.openers || []) console.log(o.row, o.line);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "dear-name:" + 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)
review, _ := reply["review"].([]any)
openers, _ := reply["openers"].([]any)
fmt.Println(j["charged_credits"], j["truncated"], reply["verdict"])
fmt.Println(len(review))
for _, x := range openers {
o := x.(map[string]any)
fmt.Println(o["row"], o["line"])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "dear-name:" + 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") + " " + reply.get("verdict"));
System.out.println(reply.path("review").size());
for (JsonNode o : reply.path("openers")) System.out.println(o.path("row").asText() + " " + o.path("line").asText());
require "digest"
key = "dear-name:#{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"], reply["verdict"]].inspect
puts (reply["review"] || []).length
(reply["openers"] || []).each { |o| puts "#{o["row"]} #{o["line"]}" }
$key = "dear-name:" . $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"], " ", ($reply["verdict"] ?? ""), "\n";
echo count($reply["review"] ?? []), "\n";
foreach ($reply["openers"] ?? [] as $o) echo $o["row"], " ", $o["line"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"dear-name:{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"]} {reply!["verdict"]}");
Console.WriteLine((reply["review"] as JsonArray)?.Count ?? 0);
foreach (var o in reply["openers"] as JsonArray ?? new JsonArray()) Console.WriteLine($"{o!["row"]} {o["line"]}");
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: dear-name:merge:$(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": "merge", "goal": "Thank the volunteers who helped at the spring clean-up", "sender": "Jo", "subject": "Thank you, {{first_name}}", "body": "Hi {{first_name}},\n\nThank you for helping at the spring clean-up on Saturday.\n\nBest,\nJo", "fields": "first_name; notes column: notes", "checks": "none", "rows": "r1. first_name: Ana | notes: ran the tool table all morning\nr2. first_name: Ben | notes: none", "counts": "2 recipients: 2 ready, 0 to fix, 0 skipped; 1 with notes"}
# - see the field 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": "dear-name:merge:" + 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": `dear-name:merge:${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", "dear-name:merge:"+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", "dear-name:merge:" + 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" => "dear-name:merge:#{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: dear-name:merge:" . 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", $"dear-name:merge:{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);