Members & Badges API
Every endpoint for keeping your members and their NFC badges in sync with Citizen Pay, with request and response examples.
These endpoints manage the same members and badges as the
Badges page of the dashboard: whatever you change here shows up there,
and the other way round. Every request needs an API key — see
Authentication — and the examples below assume it is in the
CITIZENPAY_API_KEY environment variable.
When a badge is accepted
A badge is accepted at a terminal only when its status is active and it is assigned to a
member. A badge of your business that is blocked, lost or unassigned is refused. A card whose
serial is not registered as one of your badges is not affected.
Every badge is a Citizen Pay card on your organization's own token — the one it issues under
Your token. Enabling Badges uses that token, so without an
issued token Badges cannot be enabled. Registering a badge here enrols the card on that token automatically — nothing else
is needed — and the card follows the badge from then on: active is accepted (once the badge is
assigned), blocked and lost are refused, and deleting a badge blocks the card. The terminals in
your places must use the same token for NFC — see
NFC token. If no badge token is configured
for your business, registering, bulk-registering, changing the status of and deleting a badge answer
422 with {"error":"badges treasury not configured"} and change nothing.
Objects
Member
A person who holds badges.
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Assigned by Citizen Pay. |
name | string | 1 to 120 characters. Required. |
email | string or null | Up to 254 characters, stored in lower case. Unique within your business. |
externalId | string or null | Your own id for this person, 1 to 120 characters. Unique within your business. |
badgeCount | integer | How many badges the member holds. |
createdAt | string | RFC 3339 timestamp. |
updatedAt | string | RFC 3339 timestamp. |
{
"id": "5f0c2a4e-8d1b-4c3a-9e7f-2b6d1a0c9e11",
"name": "Anna Peeters",
"email": "anna.peeters@example.com",
"externalId": "EMP-0042",
"badgeCount": 1,
"createdAt": "2026-09-25T09:30:00Z",
"updatedAt": "2026-09-25T09:30:00Z"
}Badge
An NFC card, fob or sticker, identified by its chip serial.
| Field | Type | Description |
|---|---|---|
serial | string | The chip serial, normalised (see below). Unique within your business; it is the badge's id. |
status | string | active, blocked or lost. |
memberId | string or null | The member holding the badge, or null when it is unassigned. |
label | string or null | Your own name for the badge, up to 120 characters. |
assignedAt | string or null | When it was last assigned to a member. |
createdAt | string | RFC 3339 timestamp. |
updatedAt | string | RFC 3339 timestamp. |
{
"serial": "04A23B1C5D6E80",
"status": "active",
"memberId": "5f0c2a4e-8d1b-4c3a-9e7f-2b6d1a0c9e11",
"label": "Front desk",
"assignedAt": "2026-09-25T09:31:12Z",
"createdAt": "2026-09-25T09:31:12Z",
"updatedAt": "2026-09-25T09:31:12Z"
}Serial numbers
A serial is the UID of the NFC chip in hexadecimal — exactly what the terminal reads when the badge is tapped. You can send it in any common notation; it is normalised before it is stored or looked up:
- spaces, colons and dashes are removed;
- letters are turned to upper case;
- what is left must be 8 to 20 hexadecimal digits, and an even number of them. Anything else is
rejected with
400.
So 04:a2:3b:1c:5d:6e:80, 04 A2 3B 1C 5D 6E 80 and 04A23B1C5D6E80 are the same badge, and the
API always returns the last form. Serials in a URL path are normalised the same way, but the plain
form needs no URL encoding, so it is the one to use there.
Statuses
| Status | Meaning |
|---|---|
active | In use. A new badge starts here. |
blocked | Switched off for now. The card is refused. |
lost | Reported lost or stolen. The card is refused. |
Any status can be changed to any other one. Whether a badge is accepted at a terminal depends on
two things together: the status is active and memberId is not null.
Members
List members
GET /v2/business/members| Query parameter | Description |
|---|---|
limit, offset | Pagination: limit 1–100, default 50. |
q | Only members whose name, email or external id contains this text. |
externalId | Only the member with exactly this external id. |
curl "https://api2.citizenpay.xyz/v2/business/members?q=peeters" \
-H "Authorization: Bearer $CITIZENPAY_API_KEY"{
"items": [
{
"id": "5f0c2a4e-8d1b-4c3a-9e7f-2b6d1a0c9e11",
"name": "Anna Peeters",
"email": "anna.peeters@example.com",
"externalId": "EMP-0042",
"badgeCount": 1,
"createdAt": "2026-09-25T09:30:00Z",
"updatedAt": "2026-09-25T09:30:00Z"
}
],
"total": 1,
"limit": 50,
"offset": 0
}To look someone up by your own id, ?externalId=EMP-0042 returns a list of zero or one member.
Create a member
POST /v2/business/members| Body field | Description |
|---|---|
name | Required, 1 to 120 characters. |
email | Optional. One @, something on both sides of it, no spaces. |
externalId | Optional, 1 to 120 characters. |
curl -X POST "https://api2.citizenpay.xyz/v2/business/members" \
-H "Authorization: Bearer $CITIZENPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Anna Peeters","email":"anna.peeters@example.com","externalId":"EMP-0042"}'Answers 201 with the new Member, badgeCount 0. Another member with the same
email or external id is a 409.
Get a member
GET /v2/business/members/{id}Returns the Member with one extra field, badges: the list of
Badge objects the member holds.
curl "https://api2.citizenpay.xyz/v2/business/members/5f0c2a4e-8d1b-4c3a-9e7f-2b6d1a0c9e11" \
-H "Authorization: Bearer $CITIZENPAY_API_KEY"{
"id": "5f0c2a4e-8d1b-4c3a-9e7f-2b6d1a0c9e11",
"name": "Anna Peeters",
"email": "anna.peeters@example.com",
"externalId": "EMP-0042",
"badgeCount": 1,
"createdAt": "2026-09-25T09:30:00Z",
"updatedAt": "2026-09-25T09:30:00Z",
"badges": [
{
"serial": "04A23B1C5D6E80",
"status": "active",
"memberId": "5f0c2a4e-8d1b-4c3a-9e7f-2b6d1a0c9e11",
"label": "Front desk",
"assignedAt": "2026-09-25T09:31:12Z",
"createdAt": "2026-09-25T09:31:12Z",
"updatedAt": "2026-09-25T09:31:12Z"
}
]
}Update a member
PATCH /v2/business/members/{id}Send only the fields you want to change: name, email, externalId. A field you leave out keeps
its value; email or externalId set to null clears it.
curl -X PATCH "https://api2.citizenpay.xyz/v2/business/members/5f0c2a4e-8d1b-4c3a-9e7f-2b6d1a0c9e11" \
-H "Authorization: Bearer $CITIZENPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email":null,"name":"Anna Peeters-Maes"}'Answers with the updated Member. The same 400 and 409 rules as for creating
apply.
Delete a member
DELETE /v2/business/members/{id}curl -X DELETE "https://api2.citizenpay.xyz/v2/business/members/5f0c2a4e-8d1b-4c3a-9e7f-2b6d1a0c9e11" \
-H "Authorization: Bearer $CITIZENPAY_API_KEY"Answers 204 with no body. The member's badges are not deleted: they stay registered with
memberId: null, which means they are no longer accepted at your terminals until you assign them
to someone else.
Badges
List badges
GET /v2/business/badges| Query parameter | Description |
|---|---|
limit, offset | Pagination: limit 1–100, default 50. |
status | Only badges with this status: active, blocked or lost. |
memberId | Only the badges of this member. |
assigned | true for badges held by a member, false for unassigned ones. |
curl "https://api2.citizenpay.xyz/v2/business/badges?status=lost" \
-H "Authorization: Bearer $CITIZENPAY_API_KEY"Answers with the list envelope, its items being Badge objects.
Register a badge
POST /v2/business/badges| Body field | Description |
|---|---|
serial | Required. The chip serial, in any notation described above. |
label | Optional, up to 120 characters. |
memberId | Optional. Assign the badge to this member straight away. |
memberExternalId | Optional. The same, using your own id for the member. |
Send memberId or memberExternalId, not both — both together is a 400. Leave both out to
register the badge without a holder.
curl -X POST "https://api2.citizenpay.xyz/v2/business/badges" \
-H "Authorization: Bearer $CITIZENPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"serial":"04:A2:3B:1C:5D:6E:80","label":"Front desk","memberExternalId":"EMP-0042"}'Answers 201 with the new Badge, status active. A new badge with a member is
therefore accepted at your terminals right away. A serial that is already registered is a 409; a
member that does not exist is a 404, and the badge is not created. If no badge token is configured
for your business, the answer is 422 with {"error":"badges treasury not configured"}, and the badge is not created
either.
Get a badge
GET /v2/business/badges/{serial}curl "https://api2.citizenpay.xyz/v2/business/badges/04A23B1C5D6E80" \
-H "Authorization: Bearer $CITIZENPAY_API_KEY"Answers with the Badge.
Change status or label
PATCH /v2/business/badges/{serial}Send status, label or both. label: null clears the label.
curl -X PATCH "https://api2.citizenpay.xyz/v2/business/badges/04A23B1C5D6E80" \
-H "Authorization: Bearer $CITIZENPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status":"lost"}'Answers with the updated Badge. This is how you block a card (blocked), report it
lost (lost) and put it back in use (active); the card on your token follows straight away.
Changing status when no badge token is configured for your business answers 422 with
{"error":"badges treasury not configured"}, and nothing is changed.
Assign a badge
POST /v2/business/badges/{serial}/assignThe body names the member, with exactly one of memberId or memberExternalId.
curl -X POST "https://api2.citizenpay.xyz/v2/business/badges/04A23B1C5D6E80/assign" \
-H "Authorization: Bearer $CITIZENPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"memberExternalId":"EMP-0043"}'Answers with the updated Badge, assignedAt set to now. If the badge was held by
someone else it simply moves to the new member; there is no need to revoke it first. The status is
not changed. An unknown member is a 404.
Revoke a badge
POST /v2/business/badges/{serial}/revokeNo body. Takes the badge back from its member.
curl -X POST "https://api2.citizenpay.xyz/v2/business/badges/04A23B1C5D6E80/revoke" \
-H "Authorization: Bearer $CITIZENPAY_API_KEY"Answers with the Badge, memberId and assignedAt now null. The status is not
changed, but an unassigned badge is not accepted at your terminals whatever its status.
Delete a badge
DELETE /v2/business/badges/{serial}curl -X DELETE "https://api2.citizenpay.xyz/v2/business/badges/04A23B1C5D6E80" \
-H "Authorization: Bearer $CITIZENPAY_API_KEY"Answers 204 with no body. The badge record is removed and the card is blocked on your token, so it
is refused at your terminals from then on. If no badge token is configured for your business, the
answer is 422 with {"error":"badges treasury not configured"}, and the badge is kept.
Block rather than delete
Deleting blocks the card, but it also forgets the badge's member and label. To take a card out of
use for a while, set its status to blocked or lost, or revoke it, instead.
Bulk import
POST /v2/business/badges/bulkRegisters up to 1,000 badges in one request. The body holds a badges array whose items take the
same fields as registering one badge: serial, and optionally label and
memberId or memberExternalId. More than 1,000 items is a 400 and nothing is registered. If no
badge token is configured for your business, the whole request answers 422 with
{"error":"badges treasury not configured"}, and nothing is registered either.
curl -X POST "https://api2.citizenpay.xyz/v2/business/badges/bulk" \
-H "Authorization: Bearer $CITIZENPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"badges": [
{"serial":"04A23B1C5D6E80","label":"Front desk","memberExternalId":"EMP-0042"},
{"serial":"04A23B1C5D6E81","memberExternalId":"EMP-0043"},
{"serial":"04A23B1C5D6E82"},
{"serial":"04A23B1C5D6E83","memberExternalId":"EMP-9999"},
{"serial":"XYZ"}
]
}'Each item is handled on its own, so one bad item does not stop the others. The request answers
200 with where every item ended up:
{
"created": ["04A23B1C5D6E81", "04A23B1C5D6E82"],
"conflicts": ["04A23B1C5D6E80"],
"errors": [
{ "index": 3, "serial": "04A23B1C5D6E83", "error": "member not found" },
{ "index": 4, "serial": "XYZ", "error": "invalid serial" }
]
}created— serials that were registered.conflicts— serials that were already registered. They are left exactly as they were; use assign or PATCH to change them.errors— items that could not be registered, withindex, their position in thebadgesarray (counting from 0), the serial and the reason: an invalid serial, an unknown member, or bothmemberIdandmemberExternalIdgiven. As with every error, the text is meant for people, so don't match on it.
Because conflicts are reported rather than refused, sending the same list again is safe: whatever is
already registered simply comes back under conflicts.
Related pages
- Issued by Citizen Pay
- docs.citizenpay.eu
- Title
- Members & Badges API
- Lang
- EN
- Sheet
- 035 / 035