Verigrant › Integration guide

Apply for your user. Receive without ingesting.

Apply on your user's behalf without holding their data. One credential, structured submission, no forms, no retyping, and no liability for what you never stored. That is the whole of what this page is about, and it is the same line llms.txt gives a machine that arrives without reading any of this.

You apply under a mandate and an institution receives under a grant. The page is written for both of those readers, and the thing both of you need to know next is that the store in the middle is blind. Verigrant holds ciphertext and sealed keys, and no route here will decrypt anything for you, because no route here can.

The addresses, before the argument

If you are a machine and want the short version, everything below is elaboration on these seven lines.

POST https://verigrant.com/api/vera/applications
The whole of what a Vera ID in review mode authorises. Present the id in X-Verigrant-Vera-Id. Writes a pending row in the owner's approval queue and discloses nothing by existing.
GET https://verigrant.com/api/shared/records
The delegated read, for a registered institution. Returns ciphertext plus the record keys sealed to your published public key. Takes ?kind=preview or ?kind=full. Two credentials, described below.
POST https://verigrant.com/api/shared/advance
Stage two of the gate. Asks the candidate for the full set. The keys to it do not exist anywhere until they answer, so this is a request and not a flag.
GET https://verigrant.com/api/rp/keys/log/sth
The signed tree head of the append only log every relying party key is published into. Inclusion and consistency proofs are at /api/rp/keys/log/proof, and one institution's registry entry is at /api/rp/{slug}/keys. All three are open, deliberately, because a client that has to authenticate before it can check us is not checking us.
POST https://verigrant.com/v1/grants/verify
Open to anybody. Answers whether a delegation is signed, unexpired and unrevoked without disclosing a field of any record. The answer carries relying_party_required and, when the delegation names one, the audience slug, so a valid answer is never mistaken for a redeemable one.
GET https://verigrant.com/api/postings
Open vacancies, no credential required. /api/postings/{id} carries a json_ld object, a schema.org JobPosting, built from the same row it is serving.
POST https://verigrant.com/mcp
The same service in the shape an assistant already speaks. Model Context Protocol over JSON-RPC 2.0, with a grant token as the whole session and four tools that take no arguments. It is a wrapper and holds no rules of its own, because each tool is a call to a route above. Use it where the point is to start without an integration project. The sealed read is served over REST only.

The OpenAPI document is at https://verigrant.com/api/openapi.json, generated from the handlers it describes rather than maintained beside them, so a renamed field or a changed status code moves the description with it. Point a client generator or an API viewer at that URL. llms.txt is the same surface written as prose.

If you apply for someone: hold a mandate, not a copy of a person

The usual shape of an application agent is browser automation that reads a job page, reconstructs its user's history from whatever it was pasted, and types that into an applicant tracking system. Every part of that is fragile, and two parts are worse than fragile: you become the party asserting facts about a person, and you become a custodian of their identity documents.

Verigrant inverts both. Your user issues you a Vera ID. The ordinary kind carries no key material at all and authorises exactly one route. You call it once per application. The disclosure that answers it is sealed in your user's own browser and travels to the institution, not through you.

What that changes for you

  • The facts are not yours to invent You never assemble a claim about your user, so you are never the party who has to stand behind one. The institution receives what your user sealed, signed by them.
  • You store nothing that matters Losing your Vera ID means somebody can add rows to a queue. That is the whole incident. It is a very short answer to give a security reviewer.
  • You cannot be redirected No route takes a subject parameter. An instruction hidden in a job description cannot make you act for a different person, because there is no argument in which to name one.
  • A refusal is a decision Past the window, past a cap, outside the scopes, or revoked, is a 403 with a reason and a row in your user's trail. Report which one and stop. Do not retry around it.
  • There is no Verigrant agent to compete with We ship no application agent of our own and are not going to. Running one would mean reading the person whose record we are carrying, which is the one thing this service is built not to do, and it would put us opposite the agents that bring people here. The drafting helper inside a person's own account writes a draft for them to send, with their own model key or a metered one, and applies for nobody.
one application, start to finish
# the id is the whole credential. it opens nothing.
POST https://verigrant.com/api/vera/applications
X-Verigrant-Vera-Id: vera_

{ "rp": "acme-university",
  "scopes": [ "profile:read", "history:read" ],
  "kind": "preview",
  "reference": "MSc Data Science, autumn intake",
  "note": "Deadline is the 14th." }

# the answer. nothing has been disclosed yet.
{ "request_id": "…", "status": "pending",
  "source": "review",
  "org_slug": "acme-university",
  "org_domain": "acme-university.edu",
  "auto_issue": false }

# auto_issue false means a human will answer this.
# do not poll. tell your user it is in their queue.

The two modes

Both are minted by the account holder, never by you, and both are stored as a hash and shown once. The difference is whether a human answers. In both, the account holder sets how long a share lasts.

The differences between review mode and auto mode
Property Review mode Auto mode
Key material held None Scope keys sealed to your own key
Routes authorised One Four, all signature bearing
Who issues The user, in their browser You, inside a signed preset
How long a share lasts Set on the approval Set in the preset the mandate signs
Minted by POST /me/vera-ids Pairing at POST /me/capabilities
If the string leaks Queue noise, no disclosure Nothing without your signing key

Before you ask a user for auto mode

Say this to them, in these terms. Auto mode means that person, you, and anybody your key leaks to before they next sign in, can read the preset's scopes for the length of its window. Verigrant still cannot. There is no arrangement of these primitives that avoids it, because an agent that issues a disclosure with nobody online must be able to read what it is disclosing.

Revocation stops you at every route immediately. Retiring the scope keys you already hold needs the user's own key and runs at their next sign in, and the API says rotation_pending until it does. If your product does not genuinely need to act while the user is away, take review mode.

A paired agent's routes are GET /api/agent/pending, GET /api/agent/records and POST /api/agent/grants, each requiring the id in X-Verigrant-Vera-Id and an Ed25519 signature over the request in X-Verigrant-Agent-Signature. Every limit in the preset is checked on our side, not asserted by you.

If you receive applications: pull fields, not files, and decrypt them yourself

Intake pipelines usually begin with an extraction step, and that step is where the accuracy budget goes: a two column layout, a date range written three ways, a job title that is really a department. Everything downstream inherits whatever it guessed, and everything downstream also inherits whatever a stranger wrote into the document on purpose.

There is no document here. GET /shared/records returns named fields from one schema for every applicant. What it actually hands you is ciphertext plus the record keys sealed to the public key you published, and our open library opens them locally with a private key you keep wherever you keep private keys.

Registering, so that you can read at all

  • Publish two keys An X25519 key for receiving sealed record keys and an Ed25519 key for signing your own announcements, posted to POST /orgs/{id}/relying-party/keys. Several generations may be active at once, and a grant records the exact key it was sealed to, so rotating never breaks an outstanding disclosure.
  • Attest on your own domain Serve the matching signed statement at /.well-known/verigrant-relying-party.json. An applicant's browser fetches that directly from you, never through us, and refuses to seal if it disagrees with what our registry says.
  • Every registration is logged, permanently Each key becomes a leaf in an append only Merkle log with signed tree heads. That does not stop us inserting a fraudulent key. It makes doing so public, permanent and attributable, which is the entire and deliberate claim of a transparency log.
  • Keep the private key away from us The library supports a key provider backed by a KMS, an HSM or, with a warning on every use, a file. There is no route on this service that would accept a private key, and there never will be.

Two doors, and which one to lead with

REST is the production door. It is what Workday, an applicant tracking system, a student information system and your own software integrate against, with an API key that belongs to your institution, and it is the only door the sealed record is served through. Where the people deciding this are the people who run your systems, this is the door to put in front of them: it is versioned, it can be monitored, and it can go through a review that ends in a signature.

MCP is the other door onto the same service, and it adds nothing. That is the point of it. Every tool is a call to a route listed above, so there is no second set of rules to drift out of step with the first. What it removes is the project: an institution with no integration budget and nobody to assign can hand an assistant a URL and a grant token and be reading the same day, which is the version of this that gets used where procurement is the obstacle rather than the interface.

Two limits come with that door and are better read here than discovered. The tools wrap the grant check and the profile read, so an institution that wants the encrypted bundle integrates over REST. And a deployment expecting agent traffic wants its rate limit raised, because a conversation makes several calls where a REST client makes one.

preview, then advance, then full
# two credentials. the grant, and proof you are you.
GET https://verigrant.com/api/shared/records?kind=preview
Authorization: Bearer v1.
X-Verigrant-Relying-Party: Key vgrp_

# ciphertext, plus record keys sealed to your key.
{ "statement": { "kind": "preview", "entries": [  ] },
  "signature": "…",
  "records": [ { "ciphertext": "…", "sealed_dek": "…" } ] }

# you shortlisted. now ask for the rest.
POST https://verigrant.com/api/shared/advance

# until they answer, the full keys do not exist.
GET https://verigrant.com/api/shared/records?kind=full
# -> 409 bundle_not_sealed, which is not an error
#    on your side. it is the gate working.

What every refusal means

The response for each state of a delegated read
State Answer
Signed, unexpired, unrevoked, in scope, and you are registered 200
No institution credential presented 403 relying_party_required
Valid, but issued to a different institution 403
Valid, but that scope was never granted 403 insufficient_scope
Withdrawn by its owner 403 revoked
Asked for a stage the candidate has not sealed 409 bundle_not_sealed
Expired, unknown, tampered with, or absent 401

What you keep, stated once

A record you pulled and decrypted is yours, as a dated snapshot of a specific version. Withdrawal stops the next read and every further seal on the very next request. It does not reach what you already opened, and we tell the applicant so at the moment they agree to advance rather than implying otherwise.

A fresh key is used for every saved version, so a delegation cannot quietly follow a record forward. What you were given is what you were given.

What is no longer true, if you integrated before the cutover

The server stopped being able to hold a readable profile. Anything that depended on it being able to changed, and pretending otherwise would waste a day of your life.

The plaintext profile routes are gone

The narrative core, the positions held, the qualifications and the skills are now encrypted records. The columns were dropped and the routes that read and wrote them went with the columns. There is no second way in, because a handler that wanted to write a headline has nowhere to put it.

The MCP tools and the versioned read still exist

/mcp and GET /v1/profile serve the part of the record that has not yet crossed to the encrypted path, plus the summary of checks. Treat them as the legacy edge. A section that is empty means either that the grant does not reach it or that it now lives as ciphertext, and the encrypted read is GET /shared/records.

Nobody ranks candidates any more

The server side candidate ranking is retired outright. We cannot rank people we cannot read. Institutions shortlist on the preview, in their own systems, which is what the two stage gate was always asking them to do.

A check travels as a note, never as evidence

A result carrying field values is refused. What travels is a short note naming the kind of check, who ran it and when. Today that reviewer is a member of Verigrant staff, with outside verification vendors to follow. The note is signed so you can check it offline, and it says a review happened rather than that anything in the record is true.

The server no longer validates content

It cannot see a phone number to tell you it is malformed. Shape, size, rate and scope checks stay; field validation moved to the client and to you. Garbage is now caught at the ends rather than in the middle.

Documents are opaque

Files are chunked ciphertext with no server side parsing or text extraction. That removed a parser attack surface as well as a readable store, and it is the mechanical reason there is no injection surface in the intake flow.

Why this beats automating the form

Not an appeal to principle. Six things that make your product work better, and keep it working.

A contract instead of a layout

Form automation breaks when somebody moves a field. An interface breaks when somebody publishes a new version, on purpose, having said so. Your maintenance load stops being proportional to the number of institutions your users apply to.

Custody you never wanted

Holding somebody's identity documents is a liability with no upside for an agent. Here you can say truthfully that you hold none of it, which is a sentence that shortens security reviews and lengthens funnels.

Consent you can point at

A user telling you to is not evidence. A signed statement naming the records, the versions and the recipient is. It lives in their account, and they can read and revoke it without asking you.

A blast radius chosen in advance

Screen scraping runs with whatever the session can reach, which is everything. A delegation reaches the records it names. The worst thing a bug in your agent can do is bounded before your agent runs.

Injection has nowhere to land

Job descriptions are hostile input and you already knew that. Here no route takes a subject, so the classic escalation, talking a model into naming a different person, has no parameter to write into.

One layer, five verticals

Jobs, admissions, insurance, lending and rentals are the same call with a different institution slug. You build the integration once and your product widens without another one.

What the edge will and will not do to you

  • Rate limiting is per address, with a Retry-After An agent conversation makes several calls where a REST client makes one, and /mcp is inside the same limiter as everything else. Read the header rather than guessing a backoff. The open postings feed is deliberately not throttled, because a job board that answers a crawler with 429 has published nothing.
  • Bodies are capped at 16 KiB A grant token is a few hundred bytes and an application request is not much more. The cap exists so nothing unbounded can be streamed at the edge, not to constrain real traffic.
  • Upstream calls have a 15 second deadline Five seconds to connect. A timeout is reported as a timeout. Nothing here degrades into a plausible looking empty answer.
  • Every read is recorded and the owner is told Reading through a delegation is not anonymous. Build as though your user will look at the trail, because they can, and because an unexpected read announcing itself is one of the few defenses against a compromised gateway.
  • Do not retry a refusal revoked, expired, insufficient_scope and relying_party_required are decisions, not transient faults. Report which one, and let the person issue a new delegation.
  • Verify a key before you trust it, and refuse if you cannot If you seal anything, fetch the institution's attestation from the institution's own domain, check the inclusion proof against a pinned tree head, and refuse rather than falling back. A key handed to you by the party you are defending against is not a key you checked.