Help center
https://api.certo.bio/v1openapi.json
Issue records
POST
/v1/issuance/batchesscope issuance:writeIssues 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
| Name | Type | Description |
|---|---|---|
| dry_run | boolean | true: the same checks and the same answer per row, and the credits it would take; nothing is written, sent or spent. |
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| kind | one of employment, education, certification | The record every row carries unless the row says otherwise. A learning provider: certification only. | |
| legal_basis | string | Why you may confirm it (an employment reference law, a degree register). | |
| document_kind | string | The document this stands for (a Dienstzeugnis, a reference, a diploma); the attester reads "<document_kind>, <organisation>". | |
| confirmed_fields | array of string | What you confirm, when not the kind's default (employment: period, position; education: period, award, subject; certification: credential_name, issuer). | |
| people | array of object | Yes | The rows, 1 to 500. |
| people[].key | string | Your own key for the row, 8 to 128 characters: the same key again never issues twice. Derived from the row when absent. | |
| people[].full_name | string | Yes | The person's name as your records hold it. |
| people[].date_of_birth | string | YYYY-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[].email | string | Yes | Where the claim link goes: one plain address per account (no aliases). |
| people[].email_kind | one of personal, work | personal (the default) or work: a work address may stop working the day they leave. | |
| people[].kind | one of employment, education, certification | This row's record, when it differs from the batch's kind. | |
| people[].position | string | employment: the job title. | |
| people[].period | object | employment and education: when. | |
| people[].period.start | string | YYYY-MM or YYYY-MM-DD. | |
| people[].period.end | string or null | YYYY-MM or YYYY-MM-DD; null or absent: still there. | |
| people[].award | string | education: the award (BSc, MBA, ...). | |
| people[].subject | string | education: the field of study. | |
| people[].credential_name | string | certification: the course or credential. | |
| people[].reference | string | certification: its number. | |
| people[].expires_on | string | certification: when it expires, YYYY-MM-DD. | |
| people[].note | string | A note on the record, at most 600 characters. | |
| people[].scope | string | The scope sentence the confirmation covers, when it differs from the default for the kind. | |
| people[].custom_data | object or null | Your 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_data | object or null | Your 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