Run Integration Pulse from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page classifies the ticket pile against today's surface, splits every integration's errors by family against its own baseline, catches silent integrations, reads blame against the deploy timeline, ranks the breaking ones by installs, ranks the rungs and reads the gates, then hands it all to the run as facts. The run writes the visibility design or the outreach; the page then holds it to its own facts (ipreply.js).
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 token is minted for this app (the guest call takes {"slug":"integration-pulse"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. 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, platform, signals, facts) is missing. The page always sends all five of task, platform, signals, carry, facts; carry is an empty string for surface. |
| 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: both lanes are metered and need 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 comes first
Two lanes, chosen by task, in the order the work runs - what integrators see, then who breaks now:
surface- the visibility design: the gap per failure class, the ten rungs as ranked, the log row, the request ID, the three error buckets, the notification posture and redaction.outreach- the outreach: every breaking integration in blast-radius order with whose fault it is, the channel, the message and the next step, and what we fix on our side.
A missing or unknown task is answered as surface. Both lanes send the same five fields. The facts are the page's checks, one row per line, and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the design's decisions into carry for the outreach lane.
The input
| Field | Required | What |
|---|---|---|
task | yes | surface or outreach. If it is missing or neither, do surface and set "task": "surface". |
platform | yes | the platform, what integrators build, the segments, tagging, webhooks, the request ID today, provider failures, bodies, retention, time to notice, how fast partners lose money, alerting today, an outbound channel, deadline, mandate and effort; "not given" means not given. |
signals | yes | the ticket pile by class against today's surface, our deploys, and every integration's calls and error rates split by family against its own baseline, the breaking ones ranked by installs with the page's cause. |
carry | send it (empty for surface) | outreach task only: the surface design's decisions, when the surface task ran first; may be empty. |
facts | yes | the page's checks, one line each as area | id | status | evidence, the verdict and the ranked rungs. A line starting "must:" applies to your reply. |
retry_note | no | optional; if present, your previous reply was unusable - follow it. |
The rules the model applies
From the source skill, @samber/integration-error-observability - the only standard the run applies:
- The platform knows first and knows more; every decision closes that gap or leaves the partner reverse-engineering the platform. Start from the support pile: for each failure class (synchronous API errors, webhooks that never arrived, auth or credential failures, rate-limit confusion, silent data mismatch, an integration that stopped calling) ask whether a surface you could ship would have answered it. Absence produces no errors - a volume-drop signal is its own design item. Retention shorter than the time partners take to notice generates the ticket anyway. A surface only the account owner can open fails the partner engineer.
- Segments: single-account integrators are finished by a good self-serve log; fleet partners cannot watch a thousand dashboards and need cross-install aggregation and push; no-code and agent builders need one plain-language failure list with a next step.
- Three surfaces, never conflated: the request log (did my call work), the event log (what did it change), the webhook delivery log (did you tell me). Rungs: expose the delivery-attempt log you already write by default when webhooks exist; the request log (timestamp, endpoint, status, error code, request ID, payload as far as the legal answer allows) as soon as synchronous failures are in the pile, and by default without webhooks; failure counts alone are a floor that converts silence into a ticket, not a fix; replay is the starved rung - promote it when integrators lose events during their own downtime, independent of the platform's own retry schedule. Bodies that can never leave the tenancy make metadata the ceiling; exposing bodies needs a redaction review first and is close to irreversible.
- Correlation: a request ID per call and a correlation ID per transaction, opaque and random. Echo the request ID on every response, success included, by default; make it searchable in the developer log the moment the log ships - a header nobody outside can look up closes nothing; propagate to sub-processors when support answers "that came from our provider". The error body belongs to the error-design skill.
- Aggregation: tag every request with the integration identity at ingestion; split 4xx (theirs), 5xx (ours) and 429 (neither - the rate-limit policy's) and never blend them; compare each integration with its own baseline, not a global threshold; correlate with our deploy timeline before concluding - a spike that lines up with our release is our regression; read distribution, not just volume.
- Notification: a periodic digest per integration by default; threshold alerts with a cooldown when partners lose money within hours; immediate alerts on discrete state changes (credentials revoked, integration disabled, subscription expired). An alert per failure is the fatigue failure mode, not a rung. Logging without push is not done.
- Fleet view: rank by blast radius (installs), not error count; separate broken since our deploy (ours outright) from broken since their release (the outreach case); include silent integrations; a per-partner health score's weights are yours to defend - never publish a reliability number to partners you cannot defend, and never present consumer-side SLO framing as a provider promise.
- Gates: every failure class in the pile has a surface integrators can diagnose with; every request ID resolves in a surface the integrator can reach. Trends, never gates: self-serve deflection against your own baseline, time from failure to partner awareness, alert action rate.
House rules
- Use only the input. Never invent an integration, a partner's name, a figure, an incident or a vendor fact.
- The page's checks, rates and causes are settled: never move a cause, blend a rate or call a failed gate fine.
- No URL at all.
- Write in the language of the input; keep the JSON keys, rung ids, class ids, causes and integration names as given.
- Never use backticks, never write the input's field names as labels in your text, and never copy this prompt's wording into a reply.
- Reply with ONLY one JSON object - no prose before or after, no code fences.
The replies
surface
Write the visibility design - what integrators will be able to see and when the platform reaches out. Choosing a retention, a cooldown or an alert threshold is a design decision; say what changes.
summary- one sentence, at most 40 words: what integrators cannot see today, and the first build.audit- one object per failure class the facts list:class(the class id),gap(true or false, exactly as the page reads it),fix(one sentence: the surface that closes it - an event log or a volume-drop signal counts even though it is not one of the rungs - or why none is needed).rungs- the ten rungs in the facts' order:rung(delivery-log,request-log,replay,header-echo,searchable-id,propagation,digest,threshold-cooldown,state-changeorfleet-view),use(exactly as the facts rank it),why(one sentence).log_fields- short strings: what each log row carries.correlation-header(the response header name),format(one sentence: the ID's form),searchable(true).buckets- exactly ["4xx", "5xx", "429"].notification-posture(digestorthreshold, as the facts rank it),cooldown_minutes(a number when the posture is threshold, else 0),state_changes(short strings: the discrete events alerted at once).redaction- short strings: what is redacted before a body leaves, or that only metadata is shown.caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "surface",
"summary": "one sentence",
"audit": [ { "class": "webhook", "gap": true, "fix": "one sentence" } ],
"rungs": [ { "rung": "delivery-log", "use": "now", "why": "one sentence" } ],
"log_fields": [ "timestamp" ],
"correlation": { "header": "X-Request-Id", "format": "one sentence", "searchable": true },
"buckets": [ "4xx", "5xx", "429" ],
"notification": { "posture": "digest", "cooldown_minutes": 0, "state_changes": [ "credentials revoked" ] },
"redaction": [ "a short string" ],
"caveats": [ ]
}
The page holds it to its own facts (check() in ipreply.js; any warning makes the reply flagged): the reply answers the task it was sent; no URL; no backticks; a summary of at most 40 words; no reliability number promised; one audit row per class in the pile with the page's gap and a fix; the ten rungs as ranked, each with a reason; a log row with a timestamp, the endpoint, the status, the error code and the request ID (metadata only when bodies never leave); a named header and a searchable ID; buckets exactly 4xx, 5xx and 429; the ranked posture, a cooldown when it is threshold, state changes named; redaction rules.
outreach
Write the outreach for the integrations breaking now - who hears what, in blast-radius order.
summary- one sentence, at most 40 words.contacts- one object per breaking integration, in the facts' order:integration(as written),cause(ours,theirs,ratelimitorsilent, exactly as the page reads it),channel(named- a person reaching the owner, first for fleet partners and for our own breaks with many installs;alertwhen partners lose money within hours;state-changefor a discrete event;digestotherwise),message(one or two sentences to the integrator; quote rates only by family; a theirs cause gets the evidence - the family, the date, their release - and where to look in the log, without blame),next(one sentence: what we or they do next).ours- short strings: what we fix on our side for every ours cause; [] when none.caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "outreach",
"summary": "one sentence",
"contacts": [ { "integration": "Shopvarne app", "cause": "ours", "channel": "named", "message": "one or two sentences", "next": "one sentence" } ],
"ours": [ "a short string" ],
"caveats": [ ]
}
The page requires: the reply answers the task it was sent; the same text rules; exactly the breaking integrations in the page's order with the page's causes; a known channel, a message and a next step each; an ours cause owns the fault, a rate-limit cause points at the quota, a silent cause asks about the stop; a quoted rate always names its family; what we fix on our side when a cause is ours.
Worked examples
Three runs from the page's free samples (invented platforms): the visibility design for a shipping API whose partners hear last, the outreach for the same platform carrying that design, and the design for a no-code platform with silent mismatches. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the facts and long strings are shortened in this display only, and every cut is listed under the example. The last line is the page's own check of the full reply.
surface - A shipping API whose partners hear last
The page read it as: integrators are debugging in the dark; 20 tickets; classes with no surface: sync, webhook, silent; breaking: Shopvarne app (ours), Labelquint plugin (theirs), Shipmorrow agency (ratelimit), Northbrae Wholesale (silent); 1 of 5 gates; failing checks: coverage, volume, redaction, resolvable, retention, fatigue, ours.
Input (signals is 1712 characters; the first 1397 are shown; facts is 3205 characters; the first 1398 are shown):
{
"task": "surface",
"platform": "Platform: Parcelvane Shipping API\nWhat integrators build and what they see today: Label and tracking API used by merchants' own engineers and by partner apps installed across thousands of merchant accounts; webhooks carry tracking events.\nSegments: single-account integrators yes, fleet partners yes, no-code and agent builders no\nIdentity tagged at ingestion: yes; webhooks: yes (per-attempt outcomes recorded: yes); request ID: Request ID on error responses only, searchable by integrators: no; failures often come from our own providers: yes\nBodies: Bodies hold PII or secrets, not yet reviewed; retention 7 days; partners take 10 days to notice; Partners lose money within hours; alerting today: An alert per failure; an outbound partner channel exists: yes\nDeadline: no; a compounding surface; effort ceiling: medium; as-of: 2026-11-01",
"signals": "Ticket pile by class (class | tickets | self-servable | surface today | covered):\n- sync | 5 | 4 | nothing | no\n- webhook | 8 | 7 | failure counts only | no\n- auth | 3 | 2 | a state-change alert | yes\n- ratelimit | 2 | 2 | rate-limit headers and a quota view | yes\n- silent | 2 | 1 | nothing | no\nOur deploys: 2026-10-28 label-rendering service v4.2; 2026-10-14 tracking webhooks batching\nIntegrations (split by family against their own baseline), breaking ones ranked by installs:\n- Shopvarne app | fleet | 1800 installs | calls 52000 (baseline 50000) | 4xx 0.6%, 5xx 8.1%, 429 0%; overall baseline 1% | failing since 2026-10-29 | their release - | BREAKING: ours - failing since 2026-10-29, within two days of our deploy 2026-10-28 (label-rendering service v4.2)\n- Labelquint plugin | fleet | 650 installs | calls 9000 (baseline 9500) | 4xx 16.7%, 5xx 0.2%, 429 0%; overall baseline 2% | failing since 2026-10-30 | their release 2026-10-29 | BREAKING: theirs - 4xx dominate, since their release 2026-10-29 - after our deploy 2026-10-28 too, so rule ours out first\n- Shipmorrow agency | fleet | 120 installs | calls 20000 (baseline 18000) | 4xx 0.5%, 5xx 0.3%, 429 15%; overall baseline 1% | failing since 2026-10-25 | their release - | BREAKING: ratelimit - 429 dominate - hand the quota question to the rate-limit policy\n- Northbrae Wholesale | single | 1 install | calls 40 (baseline 3000) | 4xx\n\n[... 315 more characters cut from this display ...]",
"carry": "",
"facts": "The page's checks (7 fail, 2 warn, 2 pass, 1 n/a); verdict: integrators are debugging in the dark:\n- What integrators can see | baseline | na | 16 of 20 tickets could have been answered by a surface you could ship\n- What integrators can see | coverage | fail | Synchronous API errors (5 tickets) - today nothing; Webhooks that never arrived (8 tickets) - today failure counts only; An integration that stopped calling (2 tickets) - today nothing\n- What integrators can see | volume | fail | an integration went silent and nothing alerts on absence\n- What integrators can see | counts | warn | Webhooks that never arrived - a count says something is wrong and gives no way to act\n- What integrators can see | redaction | fail | bodies carry PII or secrets with no review, and a log is going out\n- Correlation, aggregation and alerts | tagged | pass | API key, app or client ID on arrival\n- Correlation, aggregation and alerts | resolvable | fail | Request ID on error responses only; the ID is not searchable outside the platform\n- Correlation, aggregation and alerts | retention | fail | 7-day retention against 10 days to notice\n- Correlation, aggregation and alerts | fatigue | fail | an alert per failure trains partners to mute the channel\n- Correlation, aggregation and alerts | fleetpush | pass | An alert per failure\n- Who is breaking now | breaking | warn | Shopvarne app (ours), Labelquint\n\n[... 1807 more characters cut from this display ...]"
}
Reply:
{
"task": "surface",
"summary": "Integrators see nothing for failed calls, only counts for lost webhooks, and no warning when an integration goes quiet; ship the delivery-attempt log already recorded, then the request log.",
"audit": [
{
"class": "sync",
"gap": true,
"fix": "A request log per call with status, error code and a searchable request ID."
},
{
"class": "webhook",
"gap": true,
"fix": "Expose the per-attempt delivery log with replay, so a count becomes something to act on."
},
{
"class": "auth",
"gap": false,
"fix": "The existing state-change alert already answers these tickets."
},
{
"class": "ratelimit",
"gap": false,
"fix": "Rate-limit headers and the quota view already cover this class."
},
{
"class": "silent",
"gap": true,
"fix": "A volume-drop signal against each integration's own baseline, since absence raises no error."
}
],
"rungs": [
{
"rung": "delivery-log",
"use": "now",
"why": "Every attempt is already recorded, so showing it is the cheapest win."
},
{
"rung": "request-log",
"use": "now",
"why": "Failed synchronous calls sit in the ticket pile with nothing to look at."
},
{
"rung": "replay",
"use": "now",
"why": "Integrators lose tracking events while their own endpoints are down."
},
{
"rung": "header-echo",
"use": "now",
"why": "The ID reaches only error responses today; successes need it too."
},
{
"rung": "searchable-id",
"use": "now",
"why": "An ID no integrator can look up resolves nothing, so it ships with the log."
},
{
"rung": "propagation",
"use": "now",
"why": "Support keeps pointing at our providers, so the ID must travel to them."
},
{
"rung": "digest",
"use": "later",
"why": "Money is lost within hours, so spike alerts come first and the digest after."
},
{
"rung": "threshold-cooldown",
"use": "now",
"why": "Replace the per-failure alert with a spike alert held by a cooldown."
},
{
"rung": "state-change",
"use": "now",
"why": "Revoked keys or a disabled integration need word at once, not in a batch."
},
{
"rung": "fleet-view",
"use": "now",
"why": "Fleet partners span thousands of merchants, so we rank breakage by installs ourselves."
}
],
"log_fields": [
"timestamp",
"endpoint or event",
"status",
"error code",
"request ID",
"attempt number",
"redacted payload"
],
"correlation": {
"header": "X-Request-Id",
"format": "An opaque random ID per call plus a correlation ID per transaction, returned on every response.",
"searchable": true
},
"buckets": [
"4xx",
"5xx",
"429"
],
"notification": {
"posture": "threshold",
"cooldown_minutes": 60,
"state_changes": [
"credentials revoked",
"integration disabled",
"subscription expired"
]
},
"redaction": [
"Show metadata only until a redaction review clears bodies",
"Strip PII and secrets before any body leaves"
],
"caveats": [
"Retention of 7 days misses partners who notice after 10; raise it to 30 days.",
"A 60-minute cooldown trades some repeat alerts for a channel partners keep open."
]
}
What step 5 prints for it: delivery-log | request-log | replay | header-echo | searchable-id | propagation | threshold-cooldown | state-change | fleet-view
The page's check: kept - nothing to flag
outreach - A shipping API whose partners hear last
The input carries the saved design's decisions (R.laneForm fills carry from the design reply), as the page's handoff does. Breaking, by installs: Shopvarne app (ours), Labelquint plugin (theirs), Shipmorrow agency (ratelimit), Northbrae Wholesale (silent).
Input (signals is 1712 characters; the first 1397 are shown; facts is 2995 characters; the first 1398 are shown):
{
"task": "outreach",
"platform": "Platform: Parcelvane Shipping API\nWhat integrators build and what they see today: Label and tracking API used by merchants' own engineers and by partner apps installed across thousands of merchant accounts; webhooks carry tracking events.\nSegments: single-account integrators yes, fleet partners yes, no-code and agent builders no\nIdentity tagged at ingestion: yes; webhooks: yes (per-attempt outcomes recorded: yes); request ID: Request ID on error responses only, searchable by integrators: no; failures often come from our own providers: yes\nBodies: Bodies hold PII or secrets, not yet reviewed; retention 7 days; partners take 10 days to notice; Partners lose money within hours; alerting today: An alert per failure; an outbound partner channel exists: yes\nDeadline: no; a compounding surface; effort ceiling: medium; as-of: 2026-11-01",
"signals": "Ticket pile by class (class | tickets | self-servable | surface today | covered):\n- sync | 5 | 4 | nothing | no\n- webhook | 8 | 7 | failure counts only | no\n- auth | 3 | 2 | a state-change alert | yes\n- ratelimit | 2 | 2 | rate-limit headers and a quota view | yes\n- silent | 2 | 1 | nothing | no\nOur deploys: 2026-10-28 label-rendering service v4.2; 2026-10-14 tracking webhooks batching\nIntegrations (split by family against their own baseline), breaking ones ranked by installs:\n- Shopvarne app | fleet | 1800 installs | calls 52000 (baseline 50000) | 4xx 0.6%, 5xx 8.1%, 429 0%; overall baseline 1% | failing since 2026-10-29 | their release - | BREAKING: ours - failing since 2026-10-29, within two days of our deploy 2026-10-28 (label-rendering service v4.2)\n- Labelquint plugin | fleet | 650 installs | calls 9000 (baseline 9500) | 4xx 16.7%, 5xx 0.2%, 429 0%; overall baseline 2% | failing since 2026-10-30 | their release 2026-10-29 | BREAKING: theirs - 4xx dominate, since their release 2026-10-29 - after our deploy 2026-10-28 too, so rule ours out first\n- Shipmorrow agency | fleet | 120 installs | calls 20000 (baseline 18000) | 4xx 0.5%, 5xx 0.3%, 429 15%; overall baseline 1% | failing since 2026-10-25 | their release - | BREAKING: ratelimit - 429 dominate - hand the quota question to the rate-limit policy\n- Northbrae Wholesale | single | 1 install | calls 40 (baseline 3000) | 4xx\n\n[... 315 more characters cut from this display ...]",
"carry": "Building now: delivery-log, request-log, replay, header-echo, searchable-id, propagation, threshold-cooldown, state-change, fleet-view\nRequest ID: X-Request-Id, searchable true\nNotification: threshold with a 60-minute cooldown; state changes: credentials revoked, integration disabled, subscription expired\nGaps closed: sync - A request log per call with status, error code and a searchable request ID.; webhook - Expose the per-attempt delivery log with replay, so a count becomes something to act on.; silent - A volume-drop signal against each integration's own baseline, since absence raises no error.",
"facts": "The page's checks (7 fail, 2 warn, 2 pass, 1 n/a); verdict: integrators are debugging in the dark:\n- What integrators can see | baseline | na | 16 of 20 tickets could have been answered by a surface you could ship\n- What integrators can see | coverage | fail | Synchronous API errors (5 tickets) - today nothing; Webhooks that never arrived (8 tickets) - today failure counts only; An integration that stopped calling (2 tickets) - today nothing\n- What integrators can see | volume | fail | an integration went silent and nothing alerts on absence\n- What integrators can see | counts | warn | Webhooks that never arrived - a count says something is wrong and gives no way to act\n- What integrators can see | redaction | fail | bodies carry PII or secrets with no review, and a log is going out\n- Correlation, aggregation and alerts | tagged | pass | API key, app or client ID on arrival\n- Correlation, aggregation and alerts | resolvable | fail | Request ID on error responses only; the ID is not searchable outside the platform\n- Correlation, aggregation and alerts | retention | fail | 7-day retention against 10 days to notice\n- Correlation, aggregation and alerts | fatigue | fail | an alert per failure trains partners to mute the channel\n- Correlation, aggregation and alerts | fleetpush | pass | An alert per failure\n- Who is breaking now | breaking | warn | Shopvarne app (ours), Labelquint\n\n[... 1597 more characters cut from this display ...]"
}
Reply:
{
"task": "outreach",
"summary": "Four integrations are breaking: our label-rendering deploy broke Shopvarne app, Labelquint plugin errors follow its own release, Shipmorrow agency is hitting its quota, and Northbrae Wholesale went quiet.",
"contacts": [
{
"integration": "Shopvarne app",
"cause": "ours",
"channel": "named",
"message": "Our deploy on 2026-10-28 of label-rendering service v4.2 broke your calls: 5xx at 8.1% since 2026-10-29, and the fault is ours, not yours.",
"next": "We roll back or fix v4.2 and confirm once 5xx returns to baseline."
},
{
"integration": "Labelquint plugin",
"cause": "theirs",
"channel": "named",
"message": "Since your release on 2026-10-29, 4xx have risen to 16.7%, so requests your plugin now sends are being rejected.",
"next": "We first rule out our 2026-10-28 deploy, then share the failing error codes so you can patch the release."
},
{
"integration": "Shipmorrow agency",
"cause": "ratelimit",
"channel": "named",
"message": "Since 2026-10-25, 429 have reached 15% as your traffic presses against its quota; this is the rate limit, not a bug.",
"next": "Review your quota in the quota view and spread calls or request a higher limit."
},
{
"integration": "Northbrae Wholesale",
"cause": "silent",
"channel": "alert",
"message": "Your calls fell from about 3000 to 40 since 2026-10-27 with no errors on our side; did you stop on purpose?",
"next": "If the stop was not intended, check your scheduler and credentials and reply so we can trace it together."
}
],
"ours": [
"Fix or roll back label-rendering service v4.2, deployed 2026-10-28, which broke Shopvarne app"
],
"caveats": [
"Today's per-failure alerting should not carry these messages; send each once."
]
}
What step 5 prints for it: Shopvarne app ours | Labelquint plugin theirs | Shipmorrow agency ratelimit | Northbrae Wholesale silent
The page's check: kept - nothing to flag
surface - A no-code platform with silent mismatches
The page read it as: integrators are debugging in the dark; 20 tickets; classes with no surface: sync, mismatch; breaking: Sheetmarrow connector (theirs); 3 of 5 gates; failing checks: coverage, resolvable.
Input (facts is 2611 characters; the first 1396 are shown):
{
"task": "surface",
"platform": "Platform: Formulark\nWhat integrators build and what they see today: A no-code forms platform; flow builders send submissions into CRMs and spreadsheets through connectors, and a few customers call the API directly.\nSegments: single-account integrators yes, fleet partners no, no-code and agent builders yes\nIdentity tagged at ingestion: yes; webhooks: no (per-attempt outcomes recorded: no); request ID: Request ID on every response, searchable by integrators: no; failures often come from our own providers: no\nBodies: Bodies can never leave our tenancy; retention 30 days; partners take 3 days to notice; Noticed at the next reconciliation; alerting today: A periodic digest; an outbound partner channel exists: no\nDeadline: yes; a one-off fix; effort ceiling: low; as-of: 2026-11-01",
"signals": "Ticket pile by class (class | tickets | self-servable | surface today | covered):\n- sync | 6 | 5 | failure counts only | no\n- auth | 3 | 3 | a state-change alert | yes\n- ratelimit | 2 | 1 | rate-limit headers and a quota view | yes\n- mismatch | 9 | 6 | nothing | no\nOur deploys: 2026-10-20 form renderer\nIntegrations (split by family against their own baseline), breaking ones ranked by installs:\n- Sheetmarrow connector | fleet | 400 installs | calls 30000 (baseline 31000) | 4xx 7%, 5xx 0.1%, 429 0%; overall baseline 2% | failing since 2026-10-30 | their release 2026-10-29 | BREAKING: theirs - 4xx dominate, since their release 2026-10-29\n- Tallowby CRM flow | nocode | 1 install | calls 900 (baseline 950) | 4xx 1%, 5xx 0.1%, 429 0%; overall baseline 1% | failing since - | their release -",
"carry": "",
"facts": "The page's checks (2 fail, 4 warn, 4 pass, 1 n/a); verdict: integrators are debugging in the dark:\n- What integrators can see | baseline | na | 15 of 20 tickets could have been answered by a surface you could ship\n- What integrators can see | coverage | fail | Synchronous API errors (6 tickets) - today failure counts only; Silent data mismatch (9 tickets) - today nothing\n- What integrators can see | volume | warn | absence produces no errors - design the signal separately\n- What integrators can see | counts | warn | Synchronous API errors - a count says something is wrong and gives no way to act\n- What integrators can see | redaction | pass | metadata only - payload detail is off the menu\n- Correlation, aggregation and alerts | tagged | pass | API key, app or client ID on arrival\n- Correlation, aggregation and alerts | resolvable | fail | the ID is not searchable outside the platform\n- Correlation, aggregation and alerts | retention | pass | 30-day retention against 3 days to notice\n- Correlation, aggregation and alerts | fatigue | pass | A periodic digest\n- Correlation, aggregation and alerts | plain | warn | detail that helps engineers reads as noise to flow builders\n- Who is breaking now | breaking | warn | Sheetmarrow connector (theirs)\nRungs by the skill's ranking, in order: delivery-log skip (no webhooks - nothing to expose); request-log now (no webhooks, so the request\n\n[... 1215 more characters cut from this display ...]"
}
Reply:
{
"task": "surface",
"summary": "Integrators see only failure counts for API errors and nothing for mismatched data, and request IDs cannot be looked up; first build is a metadata request log searchable by ID.",
"audit": [
{
"class": "sync",
"gap": true,
"fix": "A request log per call with status, error code and a searchable request ID replaces bare counts."
},
{
"class": "auth",
"gap": false,
"fix": "The existing state-change alert already answers these tickets."
},
{
"class": "ratelimit",
"gap": false,
"fix": "Rate-limit headers and the quota view already cover this class."
},
{
"class": "mismatch",
"gap": true,
"fix": "An event log showing what each submission changed, in plain language with a next step for flow builders."
}
],
"rungs": [
{
"rung": "delivery-log",
"use": "skip",
"why": "There are no webhooks, so no delivery attempts exist to show."
},
{
"rung": "request-log",
"use": "now",
"why": "Without webhooks the request log is the default surface, and API errors fill the pile."
},
{
"rung": "replay",
"use": "skip",
"why": "No webhooks means no deliveries to resend."
},
{
"rung": "header-echo",
"use": "done",
"why": "Every response already carries the request ID."
},
{
"rung": "searchable-id",
"use": "now",
"why": "The ID exists but nobody outside can look it up, so it lands with the log."
},
{
"rung": "propagation",
"use": "later",
"why": "Failures rarely come from our providers yet."
},
{
"rung": "digest",
"use": "now",
"why": "Problems surface at reconciliation, so a periodic summary per integration fits."
},
{
"rung": "threshold-cooldown",
"use": "later",
"why": "No partner loses money within the hour today."
},
{
"rung": "state-change",
"use": "now",
"why": "Revoked credentials or a disabled connector must be told at once."
},
{
"rung": "fleet-view",
"use": "later",
"why": "There are no fleet partners to rank yet."
}
],
"log_fields": [
"timestamp",
"endpoint",
"status",
"error code",
"request ID",
"plain-language next step"
],
"correlation": {
"header": "X-Request-Id",
"format": "An opaque random ID per call and a correlation ID per submission flow.",
"searchable": true
},
"buckets": [
"4xx",
"5xx",
"429"
],
"notification": {
"posture": "digest",
"cooldown_minutes": 0,
"state_changes": [
"credentials revoked",
"integration disabled",
"subscription expired"
]
},
"redaction": [
"Metadata only; bodies never leave our tenancy"
],
"caveats": [
"No outbound partner channel exists, so the digest needs a delivery route first."
]
}
What step 5 prints for it: request-log | searchable-id | digest | state-change
The page's check: kept - nothing to flag
Build the input yourself
Load the engine in node (global.window = global; require("./ipkit.js"); require("./ipreply.js")), then:
const K = PulseKit, R = PulseReply;
const f = K.form(Object.assign(K.blank(), { name: "Acme", tickets: "webhook | yes\nsync | no", surfaces: "webhook | counts",
stats: "Big app | fleet | 900 | 10000 | 10000 | 50 | 900 | 0 | 1 | 2026-10-29 |", deploys: "2026-10-28 | release 7" }));
// f.auditRows (each class with its surface and gap), f.statRows (rates split by family, cause, why), f.breaking, f.ladder, f.gate
// K.auditMd(f), K.fleetCsv(f) - the audit table and the fleet view
const b = K.buildInput(f, "surface"); // or "outreach" (R.laneForm(form, "outreach", design) fills carry)
const wire = K.toWire(b.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "surface");
// R.check(reply, b) // {status: kept or flagged, notes}
Get a token for the steps below from /tokens.html.
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="YOUR_TOKEN" # from https://integration-pulse.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 = "YOUR_TOKEN" # from https://integration-pulse.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"]
// integration-pulse.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://integration-pulse.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 = "YOUR_TOKEN" // from https://integration-pulse.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
}
// Integration Pulse.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 Integration Pulse {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://integration-pulse.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 = "YOUR_TOKEN" # from https://integration-pulse.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 = "YOUR_TOKEN"; // from https://integration-pulse.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 = "YOUR_TOKEN"; // from https://integration-pulse.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":"integration-pulse"}' | 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": "integration-pulse"})["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: "integration-pulse" })).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": "integration-pulse"}, 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", "integration-pulse"), 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: "integration-pulse" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "integration-pulse"])["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 = "integration-pulse" }))!["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": "surface", "platform": "Platform: Parcelvane Shipping API\nWhat integrators build and what they see today: ...", "signals": "Ticket pile by class (class | tickets | self-servable | surface today | covered):\n- sync | 5 | 4 | nothing | no ...", "carry": "", "facts": "The page's checks (8 fail, ...); verdict: integrators are debugging in the dark:\n- What integrators can see | coverage | fail | ..."}
# - 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": "surface", "platform": "Platform: Parcelvane Shipping API\nWhat integrators build and what they see today: ...", "signals": "Ticket pile by class (class | tickets | self-servable | surface today | covered):\n- sync | 5 | 4 | nothing | no ...", "carry": "", "facts": "The page's checks (8 fail, ...); verdict: integrators are debugging in the dark:\n- What integrators can see | coverage | fail | ..."}
# - 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: "surface" or "outreach", platform, signals, carry, 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 integration-pulse:<task>:<inputHash>:a<attempt> - the lane is part of the key, retry_note is left out of the hash - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A surface reply carries summary, audit, rungs, log_fields, correlation, buckets, notification, redaction and caveats; an outreach reply carries summary, contacts, ours and caveats. The code below prints the gist of either: each rung and its use, or each contact and its cause.
KEY="integration-pulse:$(jq -r .task input.json):$(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 'if .task == "outreach" then (.contacts[] | "\(.integration): \(.cause)")
else (.rungs[] | "\(.rung) \(.use)") end' reply.json
import hashlib, time
key = "integration-pulse:" + 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["task"])
if reply["task"] == "outreach":
for x in reply["contacts"]:
print(x["integration"], x["cause"])
else:
for x in reply["rungs"]:
print(x["rung"], x["use"])
import { createHash } from "node:crypto";
const key = `integration-pulse:${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.task);
if (reply.task === "outreach") { for (const x of reply.contacts) console.log(x.integration, x.cause); }
else { for (const x of reply.rungs) console.log(x.rung, x.use); }
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "integration-pulse:" + 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"], reply["task"])
key := "rungs"
if reply["task"] == "outreach" {
key = "contacts"
}
ls, _ := reply[key].([]any)
for _, x := range ls {
xx := x.(map[string]any)
fmt.Println(xx["rung"], xx["use"], xx["integration"], xx["cause"])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "integration-pulse:" + 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.path("task").asText());
if (reply.path("task").asText().equals("outreach")) {
for (JsonNode x : reply.path("contacts")) System.out.println(x.path("integration").asText() + " " + x.path("cause").asText());
} else {
for (JsonNode x : reply.path("rungs")) System.out.println(x.path("rung").asText() + " " + x.path("use").asText());
}
require "digest"
key = "integration-pulse:#{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["task"]].inspect
if reply["task"] == "outreach"
reply["contacts"].each { |x| puts "#{x["integration"]} #{x["cause"]}" }
else
reply["rungs"].each { |x| puts "#{x["rung"]} #{x["use"]}" }
end
$key = "integration-pulse:" . $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["task"], "\n";
if ($reply["task"] === "outreach") { foreach ($reply["contacts"] as $x) echo $x["integration"], " ", $x["cause"], "\n"; }
else { foreach ($reply["rungs"] as $x) echo $x["rung"], " ", $x["use"], "\n"; }
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"integration-pulse:{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>())!;
var task = (string?)reply["task"];
Console.WriteLine($"{j["charged_credits"]} {task}");
if (task == "outreach") { foreach (var x in reply["contacts"]!.AsArray()) Console.WriteLine($"{x!["integration"]} {x["cause"]}"); }
else { foreach (var x in reply["rungs"]!.AsArray()) Console.WriteLine($"{x!["rung"]} {x["use"]}"); }
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: integration-pulse:$(jq -r .task input.json):$(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": "surface", "platform": "Platform: Parcelvane Shipping API\nWhat integrators build and what they see today: ...", "signals": "Ticket pile by class (class | tickets | self-servable | surface today | covered):\n- sync | 5 | 4 | nothing | no ...", "carry": "", "facts": "The page's checks (8 fail, ...); verdict: integrators are debugging in the dark:\n- What integrators can see | coverage | fail | ..."}
# - 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": "integration-pulse:" + rev["task"] + ":" + 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": `integration-pulse:${rev.task}:${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", "integration-pulse:"+input["task"].(string)+":"+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", "integration-pulse:" + input.get("task") + ":" + 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" => "integration-pulse:#{JSON.parse(rev)["task"]}:#{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: integration-pulse:" . json_decode($rev, true)["task"] . ":" . 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", $"integration-pulse:{input["task"]}:{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);