Changelog
This page tracks every shipping change to the JobTactics Public API. Entries are dated and grouped by category. Breaking changes are called out explicitly.
v2.7.0 — 2026-09-15
Added — operations accept a fileId directly
Six operations now take cvFileId and jobFileId: pass the id returned by
files_upload instead of copying its extractedText into a text field.
| Operation | cvFileId | jobFileId |
|---|---|---|
ats_audit | yes | yes |
cv_generate | yes | yes |
coverLetter_generate | yes | yes |
cvOptimization_run | yes | yes |
jobOffer_analyze | yes | yes |
email_generate | — | yes |
Passing the id rather than the text is not just shorter. The server verifies the file’s
SHA-256 integrity before use, reconstructs a document that was uploaded as several
pages, and refuses a job offer passed as a CV — all things you would otherwise have
to reimplement on top of extractedText.
Failure is soft, except on ats_audit. A file that cannot be found, read, or that
turns out to be the wrong kind is ignored, and the operation continues from your
profileId or your text. You have already paid for the generation at that point, so a
stale id never costs you the document. ats_audit is the exception: there the CV is
the subject of the operation, so an unusable file is refused — before any credit is
spent, with a stable code (not_a_cv, file_no_text, file_not_found…).
For cv_generate, a CV file takes precedence over the profile and is announced to the
model as the source of truth, with the profile still available as fallback.
resignationLetter_generate is deliberately left out: it uses neither a CV nor an
offer, only identity, employer and notice period. A fileId there would be a setting
with no effect.
Added — POST /public/profiles/from-cv
Turns a CV file into structured profile data — identity, experiences, education, skills, languages, certifications and the additional sections. 1 credit. Accepts PDF, DOCX, plain text or a photo (PNG, JPEG, WebP), 10 MB max.
files_upload gives you raw text; this gives you fields. It is the missing link between
a file and a usable profile.
It does not create a profile. The data is returned for you to review — creating the
profile stays a deliberate act, so nothing is written to the account from a single
automated call. The returned fileId refers to the stored CV, which stays reusable by
the operations above.
A file that turns out not to be a CV is still processed, flagged with
warning: "not_a_cv" — the extraction may well be useful, and refusing it outright
would cost you the call.
Limits: 10 extractions per hour per API key.
This brings the surface to 33 operations, with the new one joining the existing
profiles resource rather than adding another. The fourteen resources are: cv,
cvOptimization, coverLetter, resignationLetter, ats, jobOffer, email,
content, documents, profiles, credits, templates, settings, files.
v2.6.0 — 2026-09-15
Added — chain an optimization report into CV generation
cv_generate now accepts optimizationReportId: pass the id returned by
cvOptimization_run and that report’s recommendations are applied to the CV.
POST /public/cv/optimize → { id: "8f2c1b7e-…", currentScore: 62, potentialScore: 84 }
POST /public/cv/generate → { profileId, optimizationReportId: "8f2c1b7e-…" }
Until now the two operations could not talk to each other. You could buy a report full
of rewrite suggestions and a prioritised action plan, and nothing could apply it —
cv_generate started from scratch every time. The recommendations were yours to retype
by hand, or to lose.
The report is read server-side. You pass an id, never the recommendations themselves: they are loaded from your own report and injected into the generation. You cannot mistype them, and you cannot make the model apply something the report never said. The model is explicitly told to apply them without inventing facts, projects or figures to satisfy them.
A bad id never costs you the document. An unknown id, a mistyped one, a report belonging to another account, or one with nothing actionable in it is simply ignored and the CV is generated without it. You have already paid for the generation at that point — failing the whole call over a stale identifier would be the wrong trade.
The field is optional and additive: requests that do not use it behave exactly as before.
v2.5.0 — 2026-09-15
Added — files_upload accepts images, and the size limit doubles
POST /public/files now accepts PNG, JPEG and WebP alongside PDF, DOCX and plain
text, and the ceiling moves from 5 MB to 10 MB. A photo of a CV taken on a phone
now works.
The image restriction was never justified. It was borrowed from the anonymous audit on the marketing site, which is free and unauthenticated — while this endpoint requires a valid API key, costs a credit and is capped at 30 uploads an hour. The closer precedent was JobTactics Lite, which has accepted photos from day one, for a reason that applies just as well here: people photograph their CV. So does an integrator’s mobile app.
The 5 MB limit went with it: a phone photo routinely weighs 3 to 6 MB, so keeping it would have accepted images while rejecting the very case that motivates them.
Still 1 credit, whatever the format. Images are read by vision rather than parsed, which costs us more, but a single tariff keeps the contract predictable — you do not have to know how a file will be processed to know what it costs.
Legibility is what matters. A blurred, cropped or glare-heavy photo yields no text:
it is rejected with file_no_text and refunded, like any unreadable upload. When
you have the choice, a PDF or DOCX remains the more reliable input.
HEIC — the iPhone default — is not accepted yet: it needs verifying that it survives the whole pipeline before we bill for it.
v2.4.0 — 2026-09-15
Fixed — insufficient credits now returns 402, as documented
Every endpoint that consumes credits returned 400 Bad Request when your balance was too low, while this documentation had always announced 402 Payment Required. The code now matches the documentation.
A depleted balance is not a malformed request: there is nothing in your payload to
fix, so 400 sent integrators looking for a validation error that did not exist.
The response body is unchanged — only the status code moves:
{
"statusCode": 402,
"message": "insufficient_credits",
"timestamp": "2026-09-15T10:00:00.000Z"
}
If you branch on message === "insufficient_credits" (what the SDK and our own
clients do), nothing changes for you. If you branch on status === 400 to detect a
depleted balance, switch to 402 — jt.credits.balance() remains free to call.
The 402 example in the reference was also fictional: it promised an error field
and a details object carrying creditsRequired and creditsAvailable, which no
version of the API has ever returned. It now shows the real body.
Affected: all credit-consuming operations, including files_upload and the six
content endpoints, whose 402 was documented but never actually returned.
v2.3.0 — 2026-09-15
Added — three operations that only existed inside the product
The Studio could optimize a CV, analyse a job offer and write an application email. The API could not. Three endpoints close that gap.
| Endpoint | Operation | Cost |
|---|---|---|
POST /public/cv/optimize | cvOptimization_run | 4 credits |
POST /public/jobs/analyze | jobOffer_analyze | 2 credits |
POST /public/email/generate | email_generate | 2 credits |
This brings the surface to 32 operations. The fourteen resources are: cv,
cvOptimization, coverLetter, resignationLetter, ats, jobOffer, email,
content, documents, profiles, credits, templates, settings, files.
Same prices and same prompts as the in-product versions: given the same input, the reports have the same structure whichever surface produced them.
cv/optimize is not ats/audit. The audit tells you what a résumé parser will
choke on — parsing risks, missing keywords. The optimization tells you what to rewrite
and how: action verbs, quantification of results, relevance, length, industry fit, and
a prioritised action plan with before/after previews. They answer different questions
and are billed separately.
email/generate is not coverLetter/generate. It writes the short message that
carries the application — the one with the attachments — not the letter itself.
attachmentsMentioned is taken at face value, so only list what you will really
attach.
Each operation accepts either a saved profileId or raw text, exactly like
ats/audit. Combined with POST /public/files, a PDF now goes all the way through:
POST /public/files → { id, extractedText }
POST /public/cv/optimize → { resumeText: extractedText, jobDescription }
Reports are persisted like every other generation: the returned id works with
GET /public/documents/{id}, and cv/optimize and jobs/analyze results can be
exported with documents_exportReportPdf. An application email has no PDF export —
it is meant to be copied into a mail client.
Note — still no fileId on business endpoints
These three operations take text, not file ids, like all the others. Pass the
extractedText returned by the upload. Accepting file ids directly remains planned.
v2.2.0 — 2026-09-15
Added — file upload (POST /public/files)
The API accepted no files at all: auditing a CV meant extracting its text yourself first. Four new endpoints close that gap.
| Endpoint | Cost |
|---|---|
POST /public/files | 1 credit |
GET /public/files | free |
GET /public/files/{id} | free |
DELETE /public/files/{id} | free |
This brings the surface to 29 operations. The eleven resources are: cv,
coverLetter, resignationLetter, ats, content, documents, profiles,
credits, templates, settings, files.
Upload a PDF, DOCX or plain-text file (5 MB max). The response carries the extracted text, which plugs straight into the JSON endpoints:
{
"id": "630748c9-…",
"contentType": "cv",
"extractedText": "Jean DUPONT
Developpeur Python Senior…",
"charCount": 648,
"creditsUsed": 1,
"expiresAt": "2026-12-14T12:42:46.559Z"
}
POST /public/files → { id, extractedText }
POST /public/ats/audit → { resumeText: extractedText, jobDescription }
Billed once. Re-uploading the same bytes is deduplicated by checksum: the second
call returns the existing record with creditsUsed: 0. A file with no readable text
(a scanned PDF with no usable OCR layer) is rejected with file_no_text and
refunded — an unusable upload is never charged.
Files expire automatically; check expiresAt. Rate limit: 30 uploads per hour per API
key — the first quota actually enforced on the public API.
Note — the API still has no file-native operations
ats/audit, cv/generate and the others do not yet accept a fileId: pass the
extractedText you got from the upload. Accepting file ids directly on those endpoints
is planned.
v2.1.0 — 2026-09-15
Fixed — generated documents are now persisted (the id is real)
POST /public/cv/generate, /ats/audit, /cover-letter/generate and
/resignation-letter/generate used to return an id that was never written to the
database. It could not be passed to GET /public/documents/{id} or to any export
endpoint — both returned 404.
v1.1.0 removed POST /public/documents on the stated premise that generation already
persisted. That premise was wrong, so the documented generate → export workflow had
been broken end to end ever since: exports only worked on documents created inside the
app.
Generation now persists the document and returns its real id, which works with
GET /public/documents/{id} and with the four export endpoints. The document also
appears in your library at app.jobtactics.io.
Added — persisted on every generation response
A boolean telling you whether the returned id is usable:
{ "id": "e71500c9-…", "persisted": true, "content": … }
persisted: false means generation succeeded and you were charged normally, but the
document could not be stored — so the id is throwaway and exports will 404. The
practical cause is an account with no profile: documents are attached to a profile, and
an API key whose owner never created one has nothing to attach to. Create a profile at
app.jobtactics.io and subsequent calls will persist.
Storage failures never fail the request: the content is yours once it is generated and billed.
Note — ats/audit accepts raw text
Not a change, but it was undocumented and easy to miss: ats/audit takes either a saved
profileId or resumeText (up to 50 000 characters), with an optional
jobDescription to audit against a specific offer. The API does not accept file uploads
— extracting text from a PDF or DOCX is currently the caller’s job.
v2.0.0 — 2026-09-12
Breaking — every operationId renamed
Each of the 25 operations now declares an explicit, stable operationId. The previous
identifiers were derived automatically from internal class names:
| Operation | Before | After |
|---|---|---|
POST /public/cv/generate | PublicApiController_generateCv | cv_generate |
GET /public/credits/balance | PublicCreditsController_balance | credits_balance |
POST /public/documents/{id}/export-pdf | PublicPdfController_exportCvPdf | documents_exportCvPdf |
Are you affected? Only if you generate code from this spec.
- Calling the REST endpoints directly? Nothing changes. No URL, no request body, no response shape, no status code, no authentication behaviour is altered by this release. You can ignore it entirely.
- Generating a client or types from the spec? Regenerate. Method and type names derived
from
operationIdwill change — for examplepublicApiControllerGenerateCv()becomescv.generate(). Nothing else in your integration needs to move.
Why we did this. Identifiers derived from class names leak our internal structure: the
word “Controller” means nothing to you, and PublicCreditsController_balance is not a name
anyone would choose for a method. Worse, they were unstable by construction — renaming a class
on our side would silently rename a method on yours.
We are doing this now, before publishing any official SDK. Once client libraries ship against these names, changing them would break real integrations. Renaming while the only consumers are direct HTTP callers costs nobody anything.
The new naming convention
Identifiers follow resource_method, so generated clients group operations by resource
instead of exposing 25 flat functions:
cv_generate → jt.cv.generate()
documents_list → jt.documents.list()
documents_exportCvPdf → jt.documents.exportCvPdf()
content_generateSummary → jt.content.generateSummary()
The ten resources are: cv, coverLetter, resignationLetter, ats, content,
documents, profiles, credits, templates, settings.
Two groupings differ from the previous documentation tags. Cover letters, résumés and
resignation letters each get their own resource, while the smaller writing helpers
(bullet points, summaries, rewriting, skills, LinkedIn) sit together under content — the
old split between “Generation” and “AI Generation” drew a line where there wasn’t one, since
all ten are AI generations. Document exports moved under documents: an export acts on a
document, so exportCvPdf now sits next to get rather than in a separate family.
v1.2.0 — 2026-08-21
Added — sortBy and sortOrder on every listing endpoint
GET /public/documents, GET /public/profiles and GET /public/templates now accept an explicit
sort, matching what GET /public/credits/history already supported:
| Endpoint | sortBy values | Default |
|---|---|---|
/public/documents | createdAt, title, type | createdAt desc |
/public/profiles | createdAt, label, updatedAt | createdAt desc |
/public/templates | name, slug, downloads | name desc |
sortOrder accepts asc or desc and defaults to desc. An unknown sortBy falls back to the
default rather than erroring, so a typo degrades gracefully instead of breaking a running scenario.
Why this matters. Asking for “10 documents” without saying which ten is an incomplete
contract: the caller cannot tell whether they got the newest, the oldest, or an arbitrary slice.
/public/documents did order by creation date, but that was implicit and undocumented — it could
have changed in a refactor and silently broken every integration built on it. /public/profiles
and /public/templates declared no ordering at all, leaving it to PostgreSQL: a paginated list
without a stable order can repeat or skip rows between two pages.
Defaults reproduce the previous behaviour exactly, so no existing caller changes.
Added — limit and offset on profile and template listings
GET /public/profiles and GET /public/templates now accept pagination, aligning them with
GET /public/documents which already supported it:
GET /api/v1/public/templates?limit=10&offset=0
GET /api/v1/public/profiles?limit=10&offset=0
| Parameter | Type | Default | Bounds |
|---|---|---|---|
limit | integer | 50 | clamped to 1–100 |
offset | integer | 0 | negatives clamped to 0 |
Both endpoints previously returned the entire collection and silently ignored any query
parameter. This is not a breaking change: omitting both parameters returns up to 50 items, and
neither collection is expected to exceed that for a single account. Callers who relied on
receiving everything in one response should pass an explicit limit.
Why it matters for automation platforms. In Make, a search module emits one bundle per
result, and every downstream module re-runs once per bundle. An unbounded listing therefore
multiplies the cost of a whole scenario — a 31-template catalogue re-ran the rest of the flow 31
times. Bounding the listing is what makes those modules usable in a chained scenario.
Fixed — Letters no longer carry a markdown code fence
POST /public/cover-letter/generate and POST /public/resignation-letter/generate returned a
content string wrapped in a markdown fence — literally starting with ```html or ```json
— which every caller had to strip by hand.
The fence is now removed server-side. When format is html (the default) and the model returns
a structured letter, the response is serialised to simple HTML (<p>, <br>) with all values
escaped, instead of being passed through raw.
Fixed — ATS audit no longer fails on long reports
POST /public/ats/audit returned a 500 when the generated report was long enough to be
truncated mid-JSON. The token budget for that operation has been raised, and a truncated response
now surfaces as a 503 with an actionable message instead of an opaque 500.
Changed — Validation errors name the offending field
A rejected payload previously answered Validation failed with no indication of which field was
wrong, because the generation schemas are unions and the underlying error was nested. The response
now carries the path of each faulty field:
{
"statusCode": 400,
"message": "Validation failed",
"errors": [
{ "path": "targetJob.title", "message": "String must contain at least 1 character(s)" }
],
"timestamp": "2026-08-21T10:00:00.000Z"
}
Only the field path and the reason are returned — never the value received.
Fixed — null is now treated as an omitted field
Automation platforms send null (not an absent key) for any field the user leaves blank. Optional
fields declared with .optional() rejected null, so a single empty Company box failed the
whole request. null values are now stripped before validation and treated as omitted.
Docs — GET /public/profiles/{id} documents all 20 sections
The endpoint description listed 8 sections while the response has carried 20 since the profile extension. Unfilled sections come back as empty arrays.
v1.1.1 — 2026-08-13
Fixed — Array-wrapped request bodies are now accepted
Automation platforms (Make, Zapier, n8n) send the request body as a bundle — a single-element
array [{ … }] rather than a plain object { … }.
Four endpoints rejected that shape with a 500:
POST /public/cv/generatePOST /public/ats/auditPOST /public/cover-letter/generatePOST /public/resignation-letter/generate
The remaining endpoints already accepted it, so the API behaved inconsistently with itself depending on which route you called. A single-element array is now unwrapped before validation on every endpoint. Sending a plain object keeps working exactly as before — no change is required in existing integrations.
Fixed — Invalid payloads return 400, not 500 (behaviour change)
A payload that failed schema validation used to surface as 500 Internal server error, which reads
as an outage and gives no clue about the offending field. Validation failures now return a proper
400 with the faulty paths:
{
"statusCode": 400,
"message": "Validation failed",
"errors": [
{ "path": "targetJob.title", "message": "String must contain at least 1 character(s)" }
],
"timestamp": "2026-08-13T10:00:00.000Z"
}
Only the field path and the validation message are echoed — never the value you sent, since request bodies can carry résumé content or credentials.
If your client treats 5xx as retryable, note that these cases now return 4xx and should not
be retried without fixing the payload.
v1.1.0 — 2026-06-30
Removed — Profile, folder and document CRUD (breaking)
The Public API is now a workflow integration surface, not a remote substitute for the JobTactics dashboard. Resources whose creation/editing only makes sense inside the dashboard’s purpose-built UI have been removed from the public surface. They remain fully available inside app.jobtactics.io.
Profiles (write surface removed; two reads remain):
POST /public/profiles— removedPATCH /public/profiles/{id}— removedDELETE /public/profiles/{id}— removedPATCH /public/profiles/{id}/default— removedGET /public/profiles— kept (returns id + label so workflows can map a profileId into generation endpoints).GET /public/profiles/{id}— kept (read-only; returns the full profile content so a workflow can pre-fill data or archive a snapshot outside the SaaS).
Folders (entire surface removed):
GET /public/documents/folders— removedPOST /public/documents/folders— removedPATCH /public/documents/folders/{id}— removedDELETE /public/documents/folders/{id}— removed
Documents (write surface removed; reads remain):
POST /public/documents— removed (documents are now created exclusively by the generation endpointsPOST /public/cv/generate,POST /public/cover-letter/generate, etc., which already return persisted documents with anid).PATCH /public/documents/{id}— removedDELETE /public/documents/{id}— removedGET /public/documents— kept (paginated listing, filterable by type/folder/search).GET /public/documents/{id}— kept (retrieve a single document with content).POST /public/documents/{id}/export-*— kept (4 endpoints: PDF, letter PDF, letter DOCX, report PDF).
Removed — Application score (breaking)
Application scoring is split between an integration signal and two SaaS-internal features. Only the former stays public:
GET /public/application-score/profile/{profileId}— kept. A 0-100 readiness signal for a single profile. An API provider can use it to decide whether a profile is ready before triggering a generation. Free of charge.GET /public/application-score/application/{applicationId}— removed. Fine-grained scoring of one tracked application is part of the application-tracking experience inside the dashboard, not a workflow building block.POST /public/application-score/applications/batch— removed. Batch-scoring a whole kanban is a dashboard refresh feature; it has no meaningful use through an external workflow.
The use case left behind: an external provider can still score a single profile, but auditing a specific application or batch-rating a board are SaaS-internal features.
Changed — Document exports : locale query parameter removed (breaking)
The locale query parameter has been removed from every export endpoint:
POST /public/documents/{id}/export-pdf— no more?locale=POST /public/documents/{id}/export-letter-pdf— no more?locale=POST /public/documents/{id}/export-report-pdf— no more?locale=
Why. Exports must reflect the document as it was generated, not be re-translated at
download time. The previous design let callers pass a locale that would override the
section labels (e.g. “Experience” → “Expérience”), producing PDFs where the body content
was in one language and the section labels in another. That was a footgun.
New behavior. The export endpoints now pick the language from the document itself, in this order:
content.languageif the document was generated with an explicit language.- The user’s preferred language (
users.language). - English as the ultimate fallback.
If you want a CV in French, generate it in French (POST /public/cv/generate with
language: "fr"). The export then matches.
Added — CV section labels follow the document language, for any locale
CV section labels (“Experience”, “Education”, “Skills”…) now follow the language the CV was generated in — including languages outside the six UI locales (fr, en, es, it, de, pt). Generate a CV in Japanese, Arabic or Chinese and the exported PDF’s section headers come out in that language instead of falling back to English.
How it works. When a CV is generated, the target language is stored on the document
(content.language), and the AI produces the translated section labels alongside the
content (content.sectionLabels). At export time, labels are resolved in this order, per
label: the document’s own sectionLabels, then the built-in registry for one of the six UI
locales, then English. Explicit per-document labels always win, so non-EU languages render
correctly without any new endpoint or parameter.
Changed — every export follows the document’s own language, end to end
The “language is intrinsic to the document” principle now covers all generation paths and all exports:
- Letters (cover letter, resignation letter) store their generation language on the document across every path — the Studio, the “cover letter for a job offer” background workflow, and the public API. The same is true for CVs generated by the background “CV for a job offer” workflow.
- Reports (
cv_audit,cv_optimization,job_analysis) always carry their language, set at generation time. - The Studio’s ad-hoc report PDF export now derives the PDF language from the report itself
(
data.language) instead of a separately supplied UI locale. Generate an audit in Spanish, export it, and the PDF headers and labels come out in Spanish — no matter what UI language you happen to be viewing the dashboard in.
Migration notes
If you previously created profiles, folders or documents via the API in scripts or integrations:
- Profiles — create them once in the dashboard (richer UI for the 10 sub-schemas:
experiences, education, skills, languages, certifications, social links, projects), then
reference them by
profileIdin your generation calls. You can still read a profile back withGET /public/profiles/{id}(full content) or list them withGET /public/profiles. - Application score — keep scoring profiles with
GET /public/application-score/profile/{profileId}. Per-application and batch scoring are no longer exposed; track and rate applications inside the dashboard. - Folders — organize the document library inside the dashboard. The
folderIdfilter onGET /public/documentsstill works server-side. - Documents — call
POST /public/cv/generate(or any other generation endpoint) which now systematically persists a document and returns itsid. Theidis immediately usable with the export endpoints. - Exports — drop any
&locale=from the export URL. The generated PDF/DOCX language follows the document’s own language, set at generation time. If you generate infr, the export comes out infr.
Total surface: 31 → 17 public endpoints. The Make and Zapier integrations have been updated in lockstep.
v1.0.1 — 2026-05-18
Removed — Matching (breaking)
POST /public/matching/quickandPOST /public/matching/quick/batchhave been removed from the public API. Quick match is an internal SaaS feature (provisional profile-vs-offer scoring that powers the dashboard and the jobs page) and is not relevant to third-party API consumers. Thekeywords/scanendpoint — never actually implemented and documented by mistake — is also gone (keyword coverage is provided byPOST /public/ats/audit).
Fixed
POST /public/cv/generate,POST /public/ats/audit,POST /public/cover-letter/generateandPOST /public/resignation-letter/generatereturned500because theoperation_costs.api_endpointmapping was never populated in production. The mapping is now backfilled and seeded — these endpoints work as documented.
v1.0.0 — 2026-05-04
The first public release of the API expands the surface from four endpoints to a complete integration toolkit (31 endpoints), all gated by personal API keys generated in Settings → API keys.
Added — AI generation
POST /public/bullet-points/generate— generate ATS-aware bullet points for a job experience.POST /public/experience-description/generate— produce a polished narrative description.POST /public/professional-summary/generate— craft a 3-5 sentence positioning summary.POST /public/reformulator/reformulate— rewrite text with the requested tone.POST /public/skills-generation/generate— generate 20 mixed (hard + soft) skills from a title.POST /public/linkedin-optimizer/generate— optimized LinkedIn profile content.
Added — Application scoring
GET /public/application-score/profile/{profileId}— readiness signal for a profile.GET /public/application-score/application/{applicationId}— fit score for one application.POST /public/application-score/applications/batch— batch-score a list of applications.
Added — Profile, document and folder CRUD
- Full CRUD on
/public/profiles(list, get, create, update, delete, set default). - Full CRUD on
/public/documentsfor every document type (cv, cover_letter, resignation_letter, linkedin_profile, cv_audit, cv_optimization, job_analysis), filterable by?type=. - Full CRUD on
/public/documents/folders.
Added — Document exports
POST /public/documents/{id}/export-pdf— CV to PDF.POST /public/documents/{id}/export-letter-pdf— letter (cover or resignation) to PDF.POST /public/documents/{id}/export-letter-docx— letter to DOCX.POST /public/documents/{id}/export-report-pdf— audit / optimization / analysis report PDF.
All export endpoints follow the same async pattern: the file is generated, uploaded to private storage and returned as a presigned URL valid for one hour, in this exact JSON shape:
{
"downloadUrl": "https://...",
"expiresAt": "2026-05-04T18:30:00.000Z",
"filename": "cv-jane-doe-software-engineer-acme-04-05-2026.pdf",
"contentType": "application/pdf",
"sizeBytes": 245678
}
Added — Read-only resources
GET /public/credits/balance— current credit balance.GET /public/credits/history— paginated transaction history.GET /public/templates— list templates the caller owns (free + purchased).GET /public/templates/{id}— retrieve a single owned template.
Added — Settings
PATCH /public/settings/language— change the preferred generation language.
Notes
- The earlier
/public/cv/generate,/public/ats/audit,/public/cover-letter/generateand/public/resignation-letter/generateendpoints are unchanged — existing integrations keep working. - The interactive reference is now a separately versioned site (this docs.jobtactics.io domain), served independently from the API.