One POST when the job closes.
Your software already has the work order. Send it when the job closes and the vehicle gets a permanent, sealed record — nobody re-types anything, ever.
How the integration works
Push, not pull
CarKnown never reaches into your system. When a repair order closes (and the customer has approved sharing), your software POSTs the record. That's the whole integration.
Idempotent by design
Send your system's stable record id as sourceRecordId. Retries, crashes, and replays are always safe — the same record is never created twice.
How records are counted. A shop's profile and the “shops with published records on this engine” list on a vehicle page are built from one rule, applied in the database and nowhere else: a sealed record, not removed, attributed to a shop by shopId; a correction chain counts once; one record per shop per vehicle per day; history a shop imported from its own prior records is counted apart from work recorded as it happened. Engines are grouped by the identity CarKnown derives from the VIN — make, displacement, cylinders, fuel — never by anything a partner sends. Lists are sorted by published records on this engine, most first; ties by distinct vehicles, then most recent record, then name, and nothing renders until at least 3 shops each have at least 5 records on that engine. Nothing else is a count: no ratings, no repeat customers, no comeback rate, no technician data, no claimed specialty, no money, no partner-sent totals, no paid placement.
Send your system's stable id for the shop as shopId (and its URL slug as shopSlug, if it has one). shopName is free text and two shops can share one; shopId is what a shop's CarKnown profile and its verified-experience counts hang off. It is optional — a record without it is still sealed and still shows on the vehicle page — but it belongs to no shop, and the response says so. Engine identity is never sent: CarKnown derives it from the VIN, the same way for every partner.
What the shop measured. Trouble codes read, tests run, and the photographs and scope captures the shop holds, added to the same record. Every one is a fact about the vehicle on the day it was read, so it stays true for as long as the record stands. All optional:
"dtcs": [
{ "code": "P0303", "status": "confirmed", "module": "PCM", "clearedAtVisit": true }
],
"tests": [
{ "type": "compression", "result": "pass",
"measurements": [
{ "name": "cylinder 3", "value": 165, "unit": "psi" },
{ "name": "cylinder 4", "value": 168, "unit": "psi" }
],
"spec": { "min": 140, "unit": "psi" } },
{ "type": "scope.ignition-secondary", "result": "fail",
"measurements": [{ "name": "burn time", "value": 0.6, "unit": "ms", "position": "cyl 3" }],
"capture": { "sha256": "<SHA-256 of the capture file>", "channel": "A", "circuit": "coil cyl 3" } }
],
"photos": [
{ "sha256": "<SHA-256 of the image file>", "kind": "issue" },
{ "sha256": "<SHA-256 of the image file>", "kind": "work" }
]dtcs — code matching /^[PBCU][0-3][0-9A-F]{3}(?:-[0-9A-F]{2})?$/ (SAE J2012, optional failure-type byte); status one of confirmed, pending, permanent, history, or omit; module, description (your words, shown as yours) and clearedAtVisit optional. A code was read at the visit the record describes — codes read on another day belong on that day's record.
tests — type a lowercase key like compression or battery.load; result one of pass, fail, advisory (shown as the shop's result, never CarKnown's); measurements as { name, value, unit, position? } where value is a number and unit is one of psi kpa bar % v a ah cca ms hz ohm mm in 32nds f c rpm deg kv; spec and capture optional.
Names go in values, never in keys. CarKnown refuses any field whose name contains a money word — rate, charge, total, balance and the rest — so { "stateOfCharge": 78 } is refused and { "name": "state of charge", "value": 78, "unit": "%" } is not.
photos and a test's capture carry the SHA-256 of the file, never the file. The image stays with the shop; the sealed fingerprint proves it existed when the record was sealed and has not been swapped since. kind is one of issue, work, before, after, part. When photos is sent, the photograph count is taken from it.
Never sent: a licence plate, registration or fleet number — plate, registration, license and fleet are refused as field names. A plate is reissued to strangers and links a car to whoever holds it; a fleet number names the owner and changes on sale. Keep unit, asset and stock numbers in your own system.
A malformed code, test, measurement or photograph is dropped, the rest of the record seals, and the response names what was dropped in warnings and counts what was kept in facts. Limits per record: 50 codes, 30 tests, 24 measurements per test, 40 photographs.
Odometer units. Send mileageUnit with mileage, one of mi, km, h — miles, kilometres, or engine hours for equipment that runs on an hour meter. Omitted means miles, as mileage always has. A reading is only ever compared with one it can be: miles and kilometres against each other, converted exactly (1 mi = 1.609344 km) and flagged across units only when lower by more than one mile, so two honest readings rounded into different units never flag; hours only against hours, never against distance. An unknown unit drops the reading, with a warning, rather than guessing one. A correction can restate the unit alone — { "mileageUnit": "km" } — when the number was right and the unit was not; the reading it replaces stops counting, and any flag it raised is resolved.
Shapes and limits. A test type matches /^[a-z][a-z0-9]*(?:[-.][a-z0-9]+)*$/. Text is limited per field; text over its limit is dropped with a warning, never truncated, because cutting a shop's words short would change what it said:
- dtcs[].module — 16
- dtcs[].description — 200
- tests[].type — 60
- tests[].measurements[].name — 40
- tests[].measurements[].position — 16
- tests[].capture.channel — 8
- tests[].capture.circuit — 60
- tests[].capture.timebase — 24
- tests[].capture.scale — 24
Field names CarKnown refuses. Every key in a submission, at any depth, is split into words — partPrice, part_price and part-price are all part + price — and the whole record is refused if any word is on this list, which is printed from the code that applies it:
- money — prices, costs and payment details are between the shop and its customer: price prices cost costs amount total subtotal tax discount fee charge rate msrp markup margin invoice balance payment paid deposit currency
- customer — a record describes a vehicle, not a person: customer client owner email phone mobile address street zip postcode firstname lastname fullname dob license plate registration
- ownership — who owns a vehicle is not part of its record: fleet
- individual — records identify the shop, not the individual who did the work: technician tech mechanic advisor adviser writer sealer employee staff operator
- supply — who a shop buys parts from is its own business: vendor supplier distributor
Always allowed whatever words they contain, matched without regard to case: sourcerecordid signature signaturealg keyid.
CarKnown seals it
On ingestion the record is validated (including mileage consistency against the vehicle's history), hashed, and Ed25519-signed with CarKnown's published key. Anyone can verify it forever at its verify URL.
Say how the record came to exist
Send provenance: "captured" when your system recorded the work as it happened, or "imported" when a shop is bringing across its own prior records (add importedAt for when). CarKnown shows the difference to consumers, because it is a real difference in evidence. Omit the field and CarKnown records it as unstated — it will never assume the stronger claim on your behalf.
Backdate freely — history is the point
There is no limit on how far back occurredAt may be, and any number of shops can hold records on the same VIN. Only impossible dates are refused: the future, and anything before 1981 when the 17-character VIN began.
Every record must identify, date and describe
Required: vin, sourceRecordId, shopName, occurredAt, and something real in work or complaint. An absent date used to default to today, which stamped imported history as having happened this morning — so it is now required outright.
Work lines that say nothing — "L", "Labor", "Service" — are dropped with a warning, and a record left describing nothing at all is refused. Mileage stays optional; most shops never capture it.
Tag the kind of job, and grade the parts
Both optional. serviceTypes takes your own category keys — brakes.front, oil, emissions.dpf, whatever your platform classifies — so "Front brakes" and "R&R front pads and rotors" can be recognised as the same fact. You are not confined to a CarKnown list: any lowercase dotted key is kept and displayed, and CarKnown only decides how nicely it is named. Where you send none, CarKnown reads a tag from your own wording and marks it as read rather than stated.
Each part takes an optional grade: oem, oe-equivalent, aftermarket, remanufactured or used. Absent means you did not say, which is not the same as aftermarket.
Say how well evidenced the repair is, if you score it
Optional. Send evidence as { confidence: 0-100, band: "Well documented" }, where the band is the word your own scale puts on the figure. CarKnown shows it as your score, attributed to you, and describes it as how well corroborated the repair is rather than how well it was done.
Nothing is inferred if you omit it, and evidence that arrives malformed is dropped with a warning rather than failing the record — a real repair should not be lost over a score.
The same rules for every partner
One API, one set of fields, one validation path and one set of trust labels. No platform gets privileged standing, extra fields, or a softer check — including any platform connected to CarKnown today. A registry that graded its partners differently would not be worth reading.
Send only what's public
Descriptions, part names, mileage, photo count. Never prices, customer names, or contact details — the vehicle's record is public by design.
Create a record
POST https://www.carknown.com/api/v1/service-records
Authorization: Bearer ck_live_your_partner_key
Content-Type: application/json
{
"vin": "1FTFW1ET1EKE12345",
"sourceRecordId": "your-system's-stable-record-id",
"shopName": "Maple Ave Auto",
"occurredAt": "2026-08-15T10:00:00Z",
"mileage": 82450,
"complaint": "Brake noise",
"diagnostics": "Worn front pads",
"work": ["Replace front pads and rotors"],
"serviceTypes": ["brakes.front"],
"parts": [{ "name": "Front brake pads", "grade": "oem" }],
"photoCount": 2,
"provenance": "captured",
"evidence": { "confidence": 78, "band": "Well documented" }
}Response — 201 Created (or 200 with duplicate: true on a resend):
{
"id": "…",
"vin": "1FTFW1ET1EKE12345",
"status": "SEALED",
"signed": true,
"hash": "sha256:…",
"mileage": { "status": "no-discrepancy-detected", "unit": "mi" },
"verifyUrl": "https://www.carknown.com/api/v1/service-records/…/verify",
"publicUrl": "https://www.carknown.com/vehicle/1FTFW1ET1EKE12345"
}A mileage reading lower than the vehicle's prior history returns "mileage": {"status": "discrepancy", …} — the record is still accepted and sealed; the conflict is preserved and flagged on the public page rather than rejected. Structural problems (missing VIN, malformed fields) return 422 with details.
What CarKnown will not accept
A submission carrying any of the following is refused with 422 and the offending field named. Nothing from a refused submission is stored. This is checked on field names only — the shop's own words are never inspected, so a complaint that mentions what a customer said or declined to pay is fine and always will be.
price, cost, total, laborRate, tax, invoice and the like. What a repair cost is between the shop and its customer, and a public history carrying it would leak both the shop's margin and the customer's business.complaint if you have it — CarKnown reads it to work out what kind of job the record describes — but it is never published, on any page or through the API. It is the one field on a work order where a person's name or number reliably ends up. For the same reason a record needs work or diagnostics: it has to say what the shop did or found, because that is what a reader is shown. A diagnosis with no labour lines is a real record and is accepted — its findings appear under “Found”.complaint, which is never published but is stored, and storage is permanent. Bare digits are not refused: a part number is not a phone number, and refusing those would break real records to catch a hypothetical one.customerName, email, phone, address, plate. A record describes a vehicle, not a person.technicianName, advisor, sealerName. Send shopName. The shop stands behind the work; putting one employee's name on a permanent public record makes it searchable against that car forever.vendor, supplier. Send parts[].grade instead — a buyer wants to know an OEM pad went on, not who sold it.The exact patterns, so your own scrub can match them. If your platform strips contact details before sending — and it should, because what reaches CarKnown is stored permanently — use these, or records will still be refused on text your scrub thought was clean:
# refused in any text field, exactly as CarKnown applies them
EMAIL /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i
PHONE /(?:\+\d{1,3}[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}\b/
# PHONE requires a separator or a country code, so these pass:
# "Fitted part 1234567890" part number, not a phone number
# "WO-5551234567" your own reference
# and these are refused:
# "AC out, call 713-555-1234"
# "ring (312) 555 0142 first"
# "+1 713.555.1234"
# not scanned, because they are not prose:
# vin sourceRecordId signature signatureAlg keyId sha256This is a net, not a guarantee, and both sides should treat it that way. A number written out in words gets through it. The real control is your platform not putting a customer into the payload in the first place; this catches the ordinary mistake, which is a phone number typed into a complaint by whoever answered the phone.
There is no unpublish. A sealed record is not withdrawn on your request or your customer's. The only exception is a legal demand, in which case the record's contents are removed and its seal is kept. Corrections are sealed beside the original with both left visible, and a record sent in error is marked as published in error rather than removed. Build your consent step before the publish call, because after it there is nothing to undo. Everything on this page exists so that a permanent record contains nothing anyone would need to withdraw.
If you send a signature, it has to match what you sent. A signature that contradicts the body is refused with 422, not published with a warning — the result would be permanent and would appear on the public page as a signature mismatch against the shop. An unsigned record is fine; it claims nothing. A signature CarKnown cannot check — no key URL configured, or the key id is not among your published keys — is also fine, and is reported as unverifiable rather than as a failure.
The usual cause of a mismatch is not tampering. It is a platform that alters the payload after signing it — stripping customer contact details at the transmit boundary while signing the unstripped record behind it. Sign the transmitted view.
If your platform seals a fuller internal work order, sign the published subset rather than the internal one — CarKnown verifies your signature against the body exactly as it arrives, so the two have to be the same bytes.
Check your key without publishing
GET https://www.carknown.com/api/v1/whoami
Authorization: Bearer <your partner key>
200 OK
{
"authenticated": true,
"partner": { "name": "Your Platform", "status": "active", "environment": "live" },
"submissions": 0,
"records": 0,
"lastSubmissionAt": null,
"signing": { "keyUrlConfigured": true, "keysFetched": 1, "keyIds": ["6ba8abcd4f18b852"] }
}This writes nothing. A registry is permanent, so proving a credential by posting a record costs you either a real job you may not have yet or a fake one you can never take back. whoami answers the question on its own: a 200 means the key is valid and a well-formed record from it would be accepted. It also reports how many records this key has actually published, because a valid key that has never published looks identical from the outside to a broken one.
A wrong key returns 401, and repeated failures from one address return 429 — the attempt limit applies here exactly as it does to the write endpoints.
Test keys
Build against a test key before you receive a live one. A test key starts with ck_test_ and works on every endpoint above, at the same URLs. Every request is validated exactly as it would be with a live key — refused fields, contact details, VIN check digits, codes, tests, units and your signature — and answered in the same shape, with "test": true and "stored": false. Nothing is stored, sealed or published.
POST https://www.carknown.com/api/v1/service-records
Authorization: Bearer ck_test_your_test_key
201 Created
{
"id": "…",
"vin": "1FTFW1ET1EKE12345",
"status": "TEST",
"test": true,
"stored": false,
"facts": { "dtcs": 1, "tests": 0, "photos": 0 },
"mileage": { "status": "no-discrepancy-detected", "unit": "mi" },
"verifyUrl": null,
"publicUrl": null
}A test record is checked against the vehicle's real mileage history, so a rollback warning appears exactly as it would live; nothing is flagged or recorded. A test correction, removal or published-in-error mark validates the body but does not look up the target record id. Test requests count toward the same rate limit as live ones. whoami reports which kind of key you are using.
Read & verify
reason, a sourceRecordId, and the fields to correct. The original is never rewritten — the correction is sealed separately and both stay on the record. A corrected odometer replaces the reading it fixes, so it does not flag itself as a rollback.reason: legal. The seal stays — id, hash, signature and date — so the removal itself can be seen; every word of content goes, including the raw submission and odometer readings; the original and its corrections go together. Only the partner that sealed a record may remove it. Idempotent.sourceRecordId and a reason: wrong_vehicle, duplicate, test_data or entered_in_error. The record and its corrections stay sealed and visible, labeled as published in error, and stop counting toward shop totals and mileage checks; any mileage flag they caused is resolved. A sealed marker record states the reason. Only the partner that sealed a record may mark it, it cannot be undone, and a marked record cannot be corrected. Idempotent.city and region (required), name, slug, country (optional, ISO-2, default US). A street address, phone or email is refused at the door by the same rule that keeps a customer's out of a record. Sign it like a record if you sign records. A profile page exists only once the shop has a published record.Getting access
- Tell us about your platform. Email partners@carknown.com with your company, the software you run, roughly how many shops use it, how you verify that an account is the shop it claims to be, and how shops obtain their customers' consent before a record is published.
- Review and agreement. CarKnown reviews the request and, if it is a fit, sends the partner agreement to sign.
- Test key. You build against a test key until your records come back clean.
- Go-live review. One real shop publishes its first records with a live key, and we review them together before you enable more shops.
- Live. Your platform is listed under connected platforms on the shops page, on the same terms as every other partner.
Changes to this contract are published on this page before they take effect.