https://api.certo.bio/v1openapi.json

Issue records

POST/v1/issuance/batchesscope issuance:write

Issues records the organisation confirms about people (a job, a degree, a certificate) and mails each person a link to claim them on a passport. A university spends a credit per graduate, once per person; a learning provider issues certificates only, free. Each row is idempotent on its key; a changed row for the same person corrects the unclaimed one. Refused while certo has not approved the organisation. Requires an Idempotency-Key; try it first with ?dry_run=true.

As an MCP tool: issue_records

Query parameters

NameTypeDescription
dry_runbooleantrue: the same checks and the same answer per row, and the credits it would take; nothing is written, sent or spent.

Request body

NameTypeRequiredDescription
kindone of employment, education, certificationThe record every row carries unless the row says otherwise. A learning provider: certification only.
legal_basisstringWhy you may confirm it (an employment reference law, a degree register).
document_kindstringThe document this stands for (a Dienstzeugnis, a reference, a diploma); the attester reads "<document_kind>, <organisation>".
confirmed_fieldsarray of stringWhat you confirm, when not the kind's default (employment: period, position; education: period, award, subject; certification: credential_name, issuer).
peoplearray of objectYesThe rows, 1 to 500.
people[].keystringYour own key for the row, 8 to 128 characters: the same key again never issues twice. Derived from the row when absent.
people[].full_namestringYesThe person's name as your records hold it.
people[].date_of_birthstringYYYY-MM-DD (or DD.MM.YYYY). Required of an employer and a university: the claim is matched to the person's identity check. Optional for a learning provider.
people[].emailstringYesWhere the claim link goes: one plain address per account (no aliases).
people[].email_kindone of personal, workpersonal (the default) or work: a work address may stop working the day they leave.
people[].kindone of employment, education, certificationThis row's record, when it differs from the batch's kind.
people[].positionstringemployment: the job title.
people[].periodobjectemployment and education: when.
people[].period.startstringYYYY-MM or YYYY-MM-DD.
people[].period.endstring or nullYYYY-MM or YYYY-MM-DD; null or absent: still there.
people[].awardstringeducation: the award (BSc, MBA, ...).
people[].subjectstringeducation: the field of study.
people[].credential_namestringcertification: the course or credential.
people[].referencestringcertification: its number.
people[].expires_onstringcertification: when it expires, YYYY-MM-DD.
people[].notestringA note on the record, at most 600 characters.
people[].scopestringThe scope sentence the confirmation covers, when it differs from the default for the kind.
people[].custom_dataobject or nullYour own data on this record: any JSON object (an id from your ATS or HR system, a requisition number), at most 4096 bytes. certo stores it and answers it back on the reads and in your webhook events; it never reads it, and the candidate never sees it. Its keys go over the batch's custom_data. Never part of the row's content: a row sent again keeps its own, or takes this one if it had none.
custom_dataobject or nullYour own data on every record in the batch: any JSON object (an id from your ATS or HR system, a requisition number), at most 4096 bytes. certo stores it and answers it back on the reads and in your webhook events; it never reads it, and the candidate never sees it. A row's own custom_data goes over it.

Response fields

dry_run
boolean. Whether this was a dry run.
issuer_kind
one of employer, university, learning_provider. employer, university or learning_provider: what the organisation is to certo, which decides the cost.
can_certify
boolean. Whether certo holds an issuer agreement with the organisation, so its records are also signed as certificates.
results
array of object. One per row, in order.
results[].row
integer. The row's number in people, from 1.
results[].email
string. The address the claim link went to.
results[].outcome
one of issued, would_issue, already, resent, corrected, already_claimed, refused. issued (a new record and link), would_issue (a dry run), already (the same row again: nothing sent), resent (its link ran out: a new one, free), corrected (an unclaimed row replaced: same passport, a new link, no credit), already_claimed (a changed row for a claimed record: nothing changed, write to certo), refused.
results[].record_id
uuid or null. The issued record: the id for /issuance/records/{id}. Null for already_claimed and refused, and in a dry run.
results[].spent
boolean. Whether this row spent a credit (a university's graduate, once per person).
results[].code
string or null. Why the row was refused, a code with its sentence under errors (account.email_not_allowed, invite.email_invalid, invite.duplicate_row, ...).
outcomes
object. How many rows ended each way.
credits
object. The credits.
credits.needed
integer. Credits the rows to send take: one per new invitation (or graduate), none for already.
credits.spent
integer. Credits spent now (0 in a dry run).
credits.balance
integer. The balance after the call (before it, in a dry run).

Errors

  • 422 issuer.batch_invalid
  • 413 issuer.batch_too_large
  • 422 issuer.certificates_only
  • 409 issuer.registry_number_required
  • 403 onboarding.awaiting_certo
  • 409 organisation.profile_incomplete
  • 402 credit.none_left
  • 400 request.field_required
  • 400 request.unknown_field
  • 400 api.idempotency_key_required
  • 409 api.idempotency_mismatch
  • 409 api.idempotency_in_progress
  • 400 api.credential_in_url
  • 401 api.key_invalid
  • 401 api.key_expired
  • 401 api.key_revoked
  • 401 api.key_suspended
  • 403 api.scope_missing
  • 429 api.rate_limited