CRM and customer data — Passtastic API
☰
Passtastic
Explore
Product
Pricing
Use Cases
Customer Stories
Help
No account yet? Sign up free

CRM and customer data

Keep Passtastic customers in step with the contact list you already have.

Identifying customers

Most of a merchant's customers signed up on their own, long before your integration existed — so they have no externalCustomerId. Enroll finds them for you. It resolves in this order:

1. externalCustomerId, exactly, across the merchant's whole account. 2. email, if you sent one — only customers on the cardId you are enrolling on. 3. phone, if you sent one — only customers on that cardId too, and it must be E.164 (a leading + and country code, e.g. +34600111222); a national-format number like 600111222 normalises to nothing and simply never matches, with no error.

On a match it attaches your externalCustomerId to that customer and returns them — same passUserId, same wallet pass, same balance. Nothing is reset and no second pass is issued. From then on your own id resolves them directly.

One person on several cards. A merchant can run more than one card (say, store credit and prize points), and each card holds its own customer record. Enroll the same person on each card with the same email/phone — the contact match only looks at the card you name, so you get one record per card. externalCustomerId is unique across the whole account, though, so use a different one per card (e.g. crm_10293_credits and crm_10293_prize) and address each card's record by its own id. Reusing an id that is already on another card returns 409 EXTERNAL_ID_ON_OTHER_CARD with that record's cardId and passUserId, and writes nothing.

Both outcomes — "created" and "matched" — return HTTP 201. A match is not a 200: branch on result, never on the status code.

GET /v1/customers/lookup and an email:/phone: {ref} resolve by email or phone too, but across the whole account — they take no cardId. If the email or phone matches more than one customer (for example, the same person on two cards), you get 409 AMBIGUOUS_CUSTOMER_MATCH listing the candidates, and nothing is written. Passtastic will not guess which person you meant. Pick one and address it by pu_<id>, or set the identifier explicitly with PATCH /v1/customers/{ref}.

json
{
  "statusCode": 409,
  "error": "Conflict",
  "message": "AMBIGUOUS_CUSTOMER_MATCH",
  "matchedBy": "email",
  "candidates": [
    { "passUserId": "66a1e2f3c9d4b5a6f7081920", "contact": "a***z@example.com" },
    { "passUserId": "66a1e2f3c9d4b5a6f7081921", "contact": "a***z@example.com" }
  ]
}

Occasionally the match succeeds but the link cannot be made — you still get the right customer back, with "linked": false and a linkSkippedReason naming why: "external_id_taken" when the externalCustomerId you sent is already attached to a different customer, or "already_has_external_id" when the matched customer already carries a different externalCustomerId of their own. Either way the stamp or point still lands on the right person; the identifier link is yours to fix.

json
{
  "passUserId": "66a1e2f3c9d4b5a6f7081920",
  "externalCustomerId": "pos_88",
  "cardId": "64f1c2a9b8e4a2d1c0a1b2c3",
  "result": "matched",
  "matchedBy": "email",
  "linked": false,
  "linkSkippedReason": "already_has_external_id",
  "installUrl": "https://passtastic.io/get-pass/64f1c2a9b8e4a2d1c0a1b2c3?code=8K3F2Q",
  "status": "installed"
}

On "already_has_external_id" the externalCustomerId returned (pos_88 above) is the one that record already carried, not the one you sent. Trust it only when linked is true; otherwise address the customer by pu_<id> and repair the link with PATCH /v1/customers/{ref}.

The {ref} path segment on GET /v1/customers/{ref}, PATCH /v1/customers/{ref}, and the earn/redeem/adjust routes accepts your externalCustomerId directly, Passtastic's internal id prefixed with pu_ (e.g. pu_66a1e2f3c9d4b5a6f7081920), or email:alex@example.com / phone:+34600111222 — and an email:/phone: form that matches more than one customer raises the same 409 AMBIGUOUS_CUSTOMER_MATCH as enroll. GET /v1/customers/lookup is separate — it takes email/phone as query parameters, not a {ref} segment at all — and is the form to prefer when you have a choice, since it keeps the address out of URLs and access logs.

Sync a customer from your CRM or POS

Call enroll on every sync or sign-in. It is safe to repeat: a customer already carrying your externalCustomerId comes back unchanged, and one who signed up through the merchant's own page is matched on email or phone and linked to your id. Either way you get one customer and one wallet pass.

You do not need to check first — but if you want to, GET /v1/customers/lookup answers the same question without writing anything.

On a match, email, phone and name are used to find the customer but are not written to their record; customFields are. There is currently no API path that updates a customer's stored contact details — PATCH /v1/customers/{ref} accepts only externalCustomerId and customFields, and name/email/phone are reserved keys that customFields itself rejects.

bash
# Safe to repeat. Send email/phone so an existing customer is matched
# rather than duplicated.
curl -X POST https://api.passtastic.io/api/v1/customers \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "externalCustomerId": "crm_10293",
    "cardId": "64f1c2a9b8e4a2d1c0a1b2c3",
    "email": "alex@example.com",
    "phone": "+34600111222"
  }'
json
{
  "passUserId": "66a1e2f3c9d4b5a6f7081920",
  "externalCustomerId": "crm_10293",
  "cardId": "64f1c2a9b8e4a2d1c0a1b2c3",
  "result": "matched",
  "matchedBy": "email",
  "linked": true,
  "installUrl": "https://passtastic.io/get-pass/64f1c2a9b8e4a2d1c0a1b2c3?code=8K3F2Q",
  "status": "installed"
}

Store the merchant's own sign-up questions

If the card asks its customers for extra details — a member number, a national id, a birthday — send them as customFields, keyed by the field key from the card's sign-up form (case-insensitive). They are stored exactly as the sign-up page stores them, so they show up in the merchant's customer list, search and CSV export alongside everyone else's. A key the card doesn't ask for, one of the reserved keys (name, email, phone, status, custom), or a non-scalar value is a 400 naming the offending key — not a silent drop.

bash
curl -X POST https://api.passtastic.io/api/v1/customers \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "externalCustomerId": "crm_10293",
    "cardId": "64f1c2a9b8e4a2d1c0a1b2c3",
    "email": "alex@example.com",
    "customFields": { "member_number": "A-4471" }
  }'

Adopt a merchant's existing customers

Rolling an integration onto an account that already has customers? You do not have to wait for each of them to transact. Look them up by email and set your identifier directly.

bash
curl -X PATCH https://api.passtastic.io/api/v1/customers/email:alex@example.com \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "externalCustomerId": "crm_10293" }'
json
{
  "passUserId": "66a1e2f3c9d4b5a6f7081920",
  "externalCustomerId": "crm_10293",
  "cardId": "64f1c2a9b8e4a2d1c0a1b2c3",
  "updated": ["externalCustomerId"]
}

updated lists which fields were actually written — just ["externalCustomerId"] here, or ["customFields", "externalCustomerId"] when you set both in one call. An externalCustomerId already in use returns 409 EXTERNAL_ID_TAKEN and writes nothing.

See Limits and errors for every status code these calls can return.