---
title: Member attestation
description: "For Zev partners: attest that a signed-in member is a verified member — student, professional or builder — through Zev Access."
---

Member attestation lets a partner organization tell Zev who one of their
members is, so every Zev product can offer that member the benefits of
[Zev Access](https://zevop.com/access). A partner attests its verified members —
today **student**, **professional** and **builder**.

> **Warning:**
> This is a partnership feature, not a self-serve one. Your organization must
> have an existing agreement with Zev and your workspace explicitly enabled for
> a program before these endpoints do anything. There is no self-serve
> enrollment. To become a partner, contact Zev.

## Before it works

Three conditions, all enforced on Zev's side. Attestation is refused, with a
message saying which, until every one holds:

1. **Your workspace is enrolled in a program.** A Zev admin enables it for an
   approved partner. Nothing you can do in the console turns it on.
2. **Your app is a verified Sign-in app.** The same domain and entity
   verification behind [going live](/guide/environments/). Attestation pauses
   if that verification lapses, and resumes when it is restored.
3. **The member has connected with ZevID.** They complete
   [Sign in with ZevID](/sdk/react/#sign-in-with-zevid) and approve the consent
   screen. Only then can you attest them. An account that exists only in your
   own database cannot be attested.

## Where to find the accountId

`accountId` is the member's **ZevID account UUID** — the identifier ZevID
issued for the person, not one you invent. After a member signs in with ZevID,
it appears on their user in this API, under `externalAccounts`, on the entry
whose `provider` is `zevid`:

```bash
curl https://api.zevauth.net/v1/users/user_123 \
  -H "Authorization: Bearer sk_live_..."
```

```json
{
  "id": "user_123",
  "email": "member@example.com",
  "externalAccounts": [
    {
      "provider": "zevid",
      "providerAccountId": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
      "email": "member@example.com",
      "verified": true,
      "revoked": false
    }
  ]
}
```

Pass that `providerAccountId` as `accountId`. ZevID rejects any other format
(for example an old `zid_...` string) with a `400`.

## Test and live

Attestation is **live only**. It writes a real fact onto a real ZevID account
that every Zev product reads, so there is no test form: a test key calling
these endpoints is refused before the request leaves ZevAuth, with a message
saying so. Build and rehearse the member's *sign-in* in your test environment as
usual; attest them only from your live app once it is verified.

## Attest

```
POST /v1/partner/affiliations/attest
```

Server-side only, with your **secret** key. You send who the member is; you
never send which workspace they belong to. That is taken from your key.

```bash
curl -X POST https://api.zevauth.net/v1/partner/affiliations/attest \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
    "memberRef": "MEMBER-2023-0451",
    "attributes": { "matriculationYear": 2023, "graduationYear": 2027, "institution": "Example University" }
  }'
```

| Field | Required | Meaning |
| --- | --- | --- |
| `accountId` | yes | The member's ZevID account UUID, from their `zevid` entry in `externalAccounts` (see above). |
| `memberRef` | no | Your own member identifier. Stored for you; never shown to products or other partners. |
| `type` | no | Which program to attest, if your workspace holds more than one. Omit it when you have one. |
| `attributes` | no | Program facts. Conventions are per program type, below. |
| `expiresAt` | sometimes | ISO 8601 end date. Optional when a `graduationYear` or your program default supplies one; **required** otherwise. See [End dates](#end-dates). |

Re-attesting the same member refreshes their attributes and expiry, and
reactivates a lapsed one. When your workspace runs a single program, its
**type** is inferred and you can leave `type` out; send it only when the
workspace holds more than one.

> **Note:**
> There is no `brokerClientId` or `tenantId` field, by design. Those identify
> which workspace is attesting, and they are taken from the key you authenticate
> with, so one workspace can never attest through another's. Sending them is a
> `400`.

### End dates

Every affiliation has an end date, so benefits expire on their own rather than
lingering. The date comes from the first of these that applies: the `expiresAt`
you send, a `graduationYear` in `attributes` (for students), or your program's
default. If none of them does, the attest is refused with a `400` — send an
`expiresAt`.

Your program also has a **maximum membership length**, agreed with Zev and
measured from the member's first attestation (for example four years). Send
shorter end dates freely; a date beyond the maximum is refused with a `400` that
names the member's last allowed date. Re-attesting does not extend it, even
after a revoke and a re-attest.

### Attribute conventions

| Type | Attributes |
| --- | --- |
| `student` | `matriculationYear`, `graduationYear`, `institution` |
| `professional` | Whatever your agreement defines. |
| `builder` | `tier` (`member` or `lead`), `chapter`, `role` |

## Revoke

```
POST /v1/partner/affiliations/revoke
```

The member left, was removed, or their membership lapsed. Revoking is allowed
even while a program is suspended, because reducing access is never blocked.

```bash
curl -X POST https://api.zevauth.net/v1/partner/affiliations/revoke \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "accountId": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d" }'
```

As with attest, add `type` only if your workspace holds more than one program.

## Reporting

```
GET /v1/partner/affiliations/report
```

Totals for your program, computed by ZevID. Server-side only, with your
**secret** key; the workspace is taken from the key, so you only ever see your
own numbers.

```bash
curl https://api.zevauth.net/v1/partner/affiliations/report \
  -H "Authorization: Bearer sk_live_..."
```

```json
{
  "attested": 1240,
  "active": 1097,
  "claimedAtLeastOne": 823,
  "claimsByOffer": [
    { "offerKey": "spotify", "title": "Spotify Premium", "claims": 611 }
  ],
  "activeOverTime": [
    { "month": "2026-08", "active": 1042 },
    { "month": "2026-09", "active": 1097 }
  ]
}
```

Add `?type=<program>` to narrow to one program when your workspace holds more
than one; omit it when you have a single program.

> **Note:**
> Reporting is **totals only** — counts, claims per offer, and active over time.
> There is no per-member breakdown here, by design: who claimed what is the
> member's business, on their own ZevID account. Reporting is live only, like
> attestation.

## What attestation is not

An affiliation is a fact Zev stores about a person, read by Zev products. It is
**not** a role, an organization, or user metadata in your own ZevAuth project,
and it does not appear in your users' tokens. If you need your own
authorization data, use [organizations](/sdk/organizations/) or
[publicMetadata](/concepts/modelling-users/); those are yours. Attestation is
for the Zev-wide member benefits your partnership is about.

Offers, packs and the claiming of them all live in ZevID and on the member's
ZevID account — ZevAuth does not model them. See
[Zev Access](https://zevop.com/access).

The member sees their affiliations on their ZevID account and can remove any of
them; benefits expire on their own at the attested expiry.
