OSINTverseDocs

Certificates API

Verify any certificate without a key, issue and revoke with a team ov_ key over REST, and verify from an agent with the MCP tool verify_credential.

Markdown version

Three interfaces, all on https://osintverse.com (not the SearchIn API host):

InterfaceAuthWhat it does
Public verificationNoneRead a certificate's signed record, status, and how to check it.
REST issuingTeam ov_ key with Certificates accessIssue, list, read, and revoke certificates for the key's issuer.
MCP verify_credentialAny MCP connectionThe public record, for agents.

Public verification

GET /api/certificates/verify/{id}
GET /certificates/verify/{id}.json      (same response)
GET /certificates/verify/{id}.md        (Markdown twin)
GET /api/certificates/keys              (every signing key, past and present)
GET /api/certificates/{id}/openbadge.json  (Open Badges 3.0 export, eddsa-jcs-2022 proof)
GET /.well-known/did.json               (did:web document naming the export's signing keys)

The Open Badges export is a W3C Verifiable Credential built from the signed record on request; valid and expired certificates export, revoked and superseded ones answer 410. Details and how to verify the proof are on How verification works.

No account, no key. Rate-limited per IP; 429 carries Retry-After. The response never includes the recipient's email. An unknown id answers 404 with { "message", "code": "not_found" }. Ids are normalised, so ovc7k3qm8px2dla and OVC-7K3Q-M8PX-2DLA are the same certificate; issuer-prefixed ids such as OSINTCOL1-48213967 work the same way.

Verify by id
curl -s https://osintverse.com/api/certificates/verify/OVC-7K3Q-M8PX-2DLA | jq '{status, title, issuer, recipient, signature}'

The response shape and the verification steps are on How verification works. Pages that the recipient has not opted into indexing carry X-Robots-Tag: noindex.

REST issuing

Authentication

Create a team API key on the team's API keys page with access level Certificates (issue, read, and revoke certificates; quotes and providers readable; never charges) or Full access. Personal keys cannot issue: certificates belong to a team's issuer. The key's creator must still be a member whose role can issue (owner, admin, member, or analyst).

Send the key as a bearer token or in x-api-key:

Authorization: Bearer ov_your_key
x-api-key: ov_your_key

Every call counts once toward the key's daily request limit. Errors are { "message": "…", "code": "…" }:

StatusCodeMeaning
401invalid_keyMissing, unknown, expired, or revoked key.
403team_key_requiredA personal key. Create the key on the team's API keys page.
403capability_missingThe key's access level does not include certificates.
403forbiddenThe key creator's team role cannot issue.
403issuer_blocked, batch_limit, monthly_capTrust-level limits (see Issuing).
404no_issuerThe key's team has no issuer yet. Create one in the issuer console.
404not_foundUnknown type or certificate for this issuer.
409all_duplicates, type_archived, bad_statusNothing to do, or the state does not allow the action.
429rate_limitedThe key's daily request limit. Retry-After is set.
503certificates_disabled, signing_unavailableIssuing is paused, or the signing key is not configured. Verification keeps working.

POST /api/certificates/issue

Issues one certificate or a batch from a type you own. Same validation and caps as the console; the batch source is recorded as api.

FieldTypeRequiredNotes
typestringYesThe credential type's slug or id.
recipientsobject[]One of recipients or recipientBatch. The size limit depends on the issuer's trust level.
recipientobjectA single recipient (same shape as a recipients item).
personal_messagestringNoA line for every email in the batch; a recipient's message overrides it.
allow_duplicatesbooleanNoIssue even to recipients who already hold a valid certificate of this type. Default false: they are skipped and counted in skipped.

Each recipient:

FieldTypeRequiredNotes
namestringYes
emailstringYesLower-cased.
handlestringNoWithout the @. Needed for handle display modes.
issued_atstringNoYYYY-MM-DD; default today.
expires_atstringNoYYYY-MM-DD, after issued_at; default from the type.
fieldsobjectPer type{ "score": "92" }; values are strings and are validated by the type's field kinds.
messagestringNoPersonal line for this recipient's email.
Issue one certificate
curl -X POST https://osintverse.com/api/certificates/issue \
  -H "Authorization: Bearer ov_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "osint-fundamentals-completion",
    "recipient": { "name": "Jane Doe", "email": "jane@example.com", "handle": "janedoe", "fields": { "score": "92" } },
    "personal_message": "Great work on the final exercise."
  }'
201 Created
{
  "batch_id": "6f1c…",
  "source": "api",
  "issued": 1,
  "skipped": 0,
  "certificates": [
    {
      "id": "OVC-7K3Q-M8PX-2DLA",
      "recipient_name": "Jane Doe",
      "recipient_email": "jane@example.com",
      "verify_url": "https://osintverse.com/certificates/verify/OVC-7K3Q-M8PX-2DLA",
      "json_url": "https://osintverse.com/certificates/verify/OVC-7K3Q-M8PX-2DLA.json"
    }
  ]
}

Rows that fail validation fail the whole call with 400 and code: invalid_rows; the message names the first few problems. Emails go out after the response; read each certificate's email.status later.

No idempotency key yet

A retried call issues again unless the recipients already hold the certificate (the default duplicate check). Keep allow_duplicates off for retries.

GET /api/certificates/issued

Lists the issuer's certificates, newest first, with keyset paging.

QueryNotes
qMatches name, email, handle, or id.
typeType slug or id.
statusvalid, revoked, or superseded.
cursornext_cursor from the previous page.
limitRows per page, up to the list page limit.
200 OK
{ "items": [ { "id": "OVC-…", "…": "…" } ], "next_cursor": "…" }

GET /api/certificates/issued/:id

One certificate with its event log (:id is the certificate id, for example OVC-7K3Q-M8PX-2DLA).

200 OK
{
  "certificate": {
    "id": "OVC-7K3Q-M8PX-2DLA",
    "type": { "id": "…", "title": "OSINT Fundamentals — Completion" },
    "batch_id": "6f1c…",
    "recipient": { "name": "Jane Doe", "email": "jane@example.com", "handle": "janedoe" },
    "display_mode": "name",
    "listed": false,
    "indexable": false,
    "fields": { "score": "92" },
    "personal_message": "Great work on the final exercise.",
    "issued_at": "2026-10-31T00:00:00.000Z",
    "expires_at": null,
    "status": "valid",
    "public_status": "valid",
    "revocation": null,
    "superseded_by": null,
    "supersedes": null,
    "claimed_at": null,
    "email": { "status": "sent", "sent_at": "2026-10-31T09:12:04.000Z", "error": null },
    "view_count": 3,
    "created_at": "2026-10-31T09:12:01.000Z",
    "urls": { "html": "…", "json": "…", "pdf": "…", "png": "…" }
  },
  "events": [ { "kind": "emailed", "at": "…", "meta": { "status": "sent" } }, { "kind": "issued", "at": "…", "meta": { "source": "api" } } ]
}

The recipient's email and the private fields (display_mode, listed, indexable, the private revocation reason) are visible to the issuer here and never on the public endpoint.

POST /api/certificates/issued/:id/revoke

Request
{ "reason": "Issued to the wrong cohort", "public": true }

Returns { "certificate": … } with status: "revoked". Only a valid certificate can be revoked; the action cannot be undone. Reissue after a typo from the console instead.

MCP: verify_credential

The OSINTverse MCP server has a read-only tool for agents:

{ "name": "verify_credential", "arguments": { "credential_id": "OVC-7K3Q-M8PX-2DLA" } }

It returns the same JSON as the public verification endpoint: status, title, issuer and whether OSINTverse verified it, the recipient as disclosed, the signed record, the hash and signature, the keys URL, and the steps to check them. It needs the osint:read scope, never charges, and is available in sandbox and live mode. Agents are told that the issuer's and recipient's details are the issuer's statements, signed by OSINTverse at issue time, and that the signature does not vouch for the issuer unless the issuer is marked verified.

On this page