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.

FieldTypeDescription
idstring (UUID)Assigned by Citizen Pay.
namestring1 to 120 characters. Required.
emailstring or nullUp to 254 characters, stored in lower case. Unique within your business.
externalIdstring or nullYour own id for this person, 1 to 120 characters. Unique within your business.
badgeCountintegerHow many badges the member holds.
createdAtstringRFC 3339 timestamp.
updatedAtstringRFC 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.

FieldTypeDescription
serialstringThe chip serial, normalised (see below). Unique within your business; it is the badge's id.
statusstringactive, blocked or lost.
memberIdstring or nullThe member holding the badge, or null when it is unassigned.
labelstring or nullYour own name for the badge, up to 120 characters.
assignedAtstring or nullWhen it was last assigned to a member.
createdAtstringRFC 3339 timestamp.
updatedAtstringRFC 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:

  1. spaces, colons and dashes are removed;
  2. letters are turned to upper case;
  3. 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

StatusMeaning
activeIn use. A new badge starts here.
blockedSwitched off for now. The card is refused.
lostReported 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 parameterDescription
limit, offsetPagination: limit 1–100, default 50.
qOnly members whose name, email or external id contains this text.
externalIdOnly 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 fieldDescription
nameRequired, 1 to 120 characters.
emailOptional. One @, something on both sides of it, no spaces.
externalIdOptional, 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 parameterDescription
limit, offsetPagination: limit 1–100, default 50.
statusOnly badges with this status: active, blocked or lost.
memberIdOnly the badges of this member.
assignedtrue 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 fieldDescription
serialRequired. The chip serial, in any notation described above.
labelOptional, up to 120 characters.
memberIdOptional. Assign the badge to this member straight away.
memberExternalIdOptional. 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}/assign

The 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}/revoke

No 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/bulk

Registers 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, with index, their position in the badges array (counting from 0), the serial and the reason: an invalid serial, an unknown member, or both memberId and memberExternalId given. 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.

Issued by Citizen Pay
docs.citizenpay.eu
Title
Members & Badges API
Lang
EN
Sheet
035 / 035

On this page