Self Pour API
v1

Third-party integration

Self Pour API

Verify a patron's age, start a pour, and receive a summary when the tap closes. Phone verification stays with your system. You send us the phone number, license, and selfie, and we return the result.

Base URL:

https://sandbox.ipourit.com/api/third-party/self-pour/v1

All requests and responses are JSON, except the verification endpoint, which takes multipart/form-data so the images are sent as files.

Sandbox and live use separate credentials and separate base URLs. Switch with the Sandbox / Live control in the header.
curl https://sandbox.ipourit.com/api/third-party/self-pour/v1/pours/7f0c3a9e-2b1d-4e8a-9c5f-61d2e7a40b13 \
  -H "X-Client-Id: $IPI_CLIENT_ID" \
  -H "X-Auth-Key: $IPI_AUTH_KEY"

Authentication

Authenticate every request with two headers. We issue both when your client is set up.

X-Client-Idstringrequired

Identifies your integration.

X-Auth-Keystringrequired

Secret for your integration. Keep it server-side. Rotate it on request.

The tenant and location are resolved from your client record. You never send them.

curl https://sandbox.ipourit.com/api/third-party/self-pour/v1/patrons/ST-88121/verification \
  -H "X-Client-Id: $IPI_CLIENT_ID" \
  -H "X-Auth-Key: $IPI_AUTH_KEY"

Idempotency

Send an Idempotency-Key header on every POST. Use a UUID per logical action, and reuse it when you retry the same action.

If we have already processed a key, we return the original response and set Idempotent-Replayed: true. Nothing runs twice. A key is kept for 24 hours.

Reuse a key only for the exact same request. Sending the same key with a different body returns 422 idempotency_mismatch.
curl https://sandbox.ipourit.com/api/third-party/self-pour/v1/pours \
  -H "X-Client-Id: $IPI_CLIENT_ID" \
  -H "X-Auth-Key: $IPI_AUTH_KEY" \
  -H "Idempotency-Key: 4b1d6e52-0c8a-4f7e-9a2b-7d3e1f0c9a11" \
  -H "Content-Type: application/json" \
  -d '{"partner_patron_ref":"ST-88121","tap_id":14,"ounces":4}'

Errors

Errors return a non-2xx status and an error object. Match on code; message is for people and can change.

HTTPcodeMeaning
400bad_requestMalformed body or a missing field.
401unauthorizedMissing or wrong client credentials.
403patron_not_verifiedPatron has not passed verification.
404patron_not_foundNo patron for this reference.
409not_checked_inNo active session at this location.
409idempotency_in_progressSame key is still processing. Retry shortly.
422document_invalidLicense is expired or unreadable.
422ounces_invalidZero, negative, or above the tap limit.
422limit_reachedPatron reached the pour limit.
422idempotency_mismatchKey reused with a different body.
503tablet_unreachableTap did not respond. Retry with the same key.
{
  "error": {
    "code": "patron_not_verified",
    "message": "Patron must complete age verification before pouring.",
    "request_id": "req_01HZX3K9Q2"
  }
}

Verify a patron

POST/patrons/verify

Reads the license, checks the minimum pour age for your location, and matches the selfie to the license photo. Creates the patron on first use, keyed by your partner_patron_ref. Saves the selfie only when every check passes.

Failed checks return 200 with a non-VERIFIED status. Only malformed input or an unreadable document returns an error.

Body parameters

phone_e164stringrequired

Phone number in E.164 format, e.g. +12105550100. Verified on your side.

partner_patron_refstringrequired

Your identifier for this customer. Up to 64 characters.

documentfilerequired

Photo of the front of a driver's license. JPEG or PNG, up to 10 MB.

selfiefilerequired

Photo of the customer's face. JPEG or PNG, up to 10 MB.

Returns

A Verification object.

statusenum
VERIFIED AGE_VERIFICATION_FAILED EXTRACTION_FAILED NOT_MATCHED FAILED ERROR
curl https://sandbox.ipourit.com/api/third-party/self-pour/v1/patrons/verify \
  -H "X-Client-Id: $IPI_CLIENT_ID" \
  -H "X-Auth-Key: $IPI_AUTH_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F phone_e164=+12105550100 \
  -F partner_patron_ref=ST-88121 \
  -F document=@dl_front.jpg \
  -F selfie=@selfie.jpg
{
  "partner_patron_ref": "ST-88121",
  "status": "VERIFIED",
  "reason": null,
  "similarity": 97.4,
  "is_id_verified": true
}

Get verification status

GET/patrons/{partner_patron_ref}/verification

Check a returning patron before asking for images again. If is_id_verified is true, skip Verify a patron.

Path parameters

partner_patron_refstringrequired

The reference you used when verifying.

Returns

A Verification object. Returns 404 patron_not_found if the reference is unknown.

curl https://sandbox.ipourit.com/api/third-party/self-pour/v1/patrons/ST-88121/verification \
  -H "X-Client-Id: $IPI_CLIENT_ID" \
  -H "X-Auth-Key: $IPI_AUTH_KEY"
{
  "partner_patron_ref": "ST-88121",
  "status": "VERIFIED",
  "is_id_verified": true,
  "verified_at": "2026-10-22T18:04:11Z"
}

Create a pour

POST/pours

Starts a pour on a tap at your location for a verified patron. The request returns once the tap acknowledges. The final amount arrives in a pour.closed webhook when the tap closes.

Body parameters

partner_patron_refstringrequired

A verified patron.

tap_idintegerrequired

A tap at your location.

ouncesnumberrequired

Greater than 0, up to the tap's limit.

Returns

A Pour object with status: "started". Repeating the request with the same Idempotency-Key returns the same object and does not pour again.

curl https://sandbox.ipourit.com/api/third-party/self-pour/v1/pours \
  -H "X-Client-Id: $IPI_CLIENT_ID" \
  -H "X-Auth-Key: $IPI_AUTH_KEY" \
  -H "Idempotency-Key: 4b1d6e52-0c8a-4f7e-9a2b-7d3e1f0c9a11" \
  -H "Content-Type: application/json" \
  -d '{"partner_patron_ref":"ST-88121","tap_id":14,"ounces":4}'
{
  "pour_id": "7f0c3a9e-2b1d-4e8a-9c5f-61d2e7a40b13",
  "partner_patron_ref": "ST-88121",
  "tap_id": 14,
  "ounces": 4,
  "status": "started"
}

Retrieve a pour

GET/pours/{pour_id}

Returns the current state of a pour. Use it if a pour.closed webhook has not arrived yet.

Path parameters

pour_idstringrequired

The pour_id from Create a pour.

Returns

A Pour object. Returns status: "notfound" if the ID is unknown for your client.

curl https://sandbox.ipourit.com/api/third-party/self-pour/v1/pours/7f0c3a9e-2b1d-4e8a-9c5f-61d2e7a40b13 \
  -H "X-Client-Id: $IPI_CLIENT_ID" \
  -H "X-Auth-Key: $IPI_AUTH_KEY"
{
  "pour_id": "7f0c3a9e-2b1d-4e8a-9c5f-61d2e7a40b13",
  "status": "completed",
  "ounces_requested": 4,
  "ounces_poured": 3.8
}

pour.closed

We send this to your webhook URL when a tap closes. We retry on any non-2xx response or on a timeout over 10 seconds. Use pour_id to dedupe, since a retry can arrive more than once.

Headers

X-Ipourit-Eventstring

Always pour.closed.

X-Ipourit-Deliverystring

Unique ID for this delivery attempt.

X-Ipourit-Signaturestring

sha256= followed by the HMAC-SHA256 of the raw body. See Verify signatures.

Body

eventstring

pour.closed

pour_idstring

The pour this event describes.

partner_patron_refstring

Your patron reference, as sent when the patron was verified.

p_unique_idstring

Our identifier for the patron.

location_idinteger

Location where the pour happened.

tap_idinteger
ounces_pourednumber

Ounces dispensed when the tap closed.

closed_atstring (ISO 8601, UTC)

Retry schedule

AttemptSent
1Immediately
21 minute later
35 minutes later
430 minutes later
52 hours later
612 hours later, then stop and alert
{
  "event": "pour.closed",
  "pour_id": "7f0c3a9e-2b1d-4e8a-9c5f-61d2e7a40b13",
  "partner_patron_ref": "ST-88121",
  "p_unique_id": "a1b2c3d4e5f6",
  "location_id": 3,
  "tap_id": 14,
  "ounces_poured": 3.8,
  "closed_at": "2026-10-22T18:09:55Z"
}
HTTP/1.1 200 OK
Content-Length: 0

Verify signatures

Compute the HMAC-SHA256 of the raw request body with your webhook secret, and compare it to the signature header. Use the raw bytes, not re-serialized JSON.

The webhook secret is separate from X-Auth-Key. We issue it with your client.

Compare with a constant-time function, for example crypto.timingSafeEqual or hmac.compare_digest.
import hmac, hashlib

def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_header)

The Verification object

partner_patron_refstring

Your reference for the patron.

statusenum

Outcome of the last verification.

VERIFIED License read, age met, face matched.
AGE_VERIFICATION_FAILED Under the location's minimum pour age.
EXTRACTION_FAILED No date of birth could be read.
NOT_MATCHED Selfie does not match the license photo.
FAILED No face found in one of the images.
ERROR Face-match service error. Safe to retry.
reasonstring | null

Plain-language reason for a non-VERIFIED status.

similaritynumber

Face-match score from 0 to 100.

is_id_verifiedboolean

True only when the patron can pour at your location.

verified_atstring | null

ISO 8601 UTC time of the passing verification. Returned by the status endpoint.

{
  "partner_patron_ref": "ST-88121",
  "status": "AGE_VERIFICATION_FAILED",
  "reason": "User is underage: age < minimum_pour_age=21",
  "similarity": 97.4,
  "is_id_verified": false
}

The Pour object

pour_idstring

Unique ID for this pour.

statusenum
started Tap acknowledged the request.
dispatched Tap is pouring.
completed Tap closed. Final amounts set.
notfound No pour with this ID for your client.
partner_patron_refstring
tap_idinteger
ounces_requestednumber
ounces_pourednumber | null

Set when the pour completes.

{
  "pour_id": "7f0c3a9e-2b1d-4e8a-9c5f-61d2e7a40b13",
  "status": "dispatched",
  "partner_patron_ref": "ST-88121",
  "tap_id": 14,
  "ounces_requested": 4,
  "ounces_poured": null
}

The Error object

codestring

Stable identifier. Match on this.

messagestring

Readable description. May change between releases.

request_idstring

Quote this when contacting us about a failed request.

{
  "error": {
    "code": "not_checked_in",
    "message": "Patron has no active session at this location.",
    "request_id": "req_01HZX3K9Q7"
  }
}