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.
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.
Identifies your integration.
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.
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.
| HTTP | code | Meaning |
|---|---|---|
| 400 | bad_request | Malformed body or a missing field. |
| 401 | unauthorized | Missing or wrong client credentials. |
| 403 | patron_not_verified | Patron has not passed verification. |
| 404 | patron_not_found | No patron for this reference. |
| 409 | not_checked_in | No active session at this location. |
| 409 | idempotency_in_progress | Same key is still processing. Retry shortly. |
| 422 | document_invalid | License is expired or unreadable. |
| 422 | ounces_invalid | Zero, negative, or above the tap limit. |
| 422 | limit_reached | Patron reached the pour limit. |
| 422 | idempotency_mismatch | Key reused with a different body. |
| 503 | tablet_unreachable | Tap 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
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 number in E.164 format, e.g. +12105550100. Verified on your side.
Your identifier for this customer. Up to 64 characters.
Photo of the front of a driver's license. JPEG or PNG, up to 10 MB.
Photo of the customer's face. JPEG or PNG, up to 10 MB.
Returns
A Verification object.
VERIFIED AGE_VERIFICATION_FAILED EXTRACTION_FAILED NOT_MATCHED FAILED ERRORcurl 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
import os, uuid, requests
r = requests.post(
f"{BASE}/patrons/verify",
headers={
"X-Client-Id": os.environ["IPI_CLIENT_ID"],
"X-Auth-Key": os.environ["IPI_AUTH_KEY"],
"Idempotency-Key": str(uuid.uuid4()),
},
data={
"phone_e164": "+12105550100",
"partner_patron_ref": "ST-88121",
},
files={
"document": open("dl_front.jpg", "rb"),
"selfie": open("selfie.jpg", "rb"),
},
)
verification = r.json()
{
"partner_patron_ref": "ST-88121",
"status": "VERIFIED",
"reason": null,
"similarity": 97.4,
"is_id_verified": true
}
Get verification status
Check a returning patron before asking for images again. If is_id_verified is true, skip Verify a patron.
Path parameters
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
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
A verified patron.
A tap at your location.
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}'
import os, requests
r = requests.post(
f"{BASE}/pours",
headers={
"X-Client-Id": os.environ["IPI_CLIENT_ID"],
"X-Auth-Key": os.environ["IPI_AUTH_KEY"],
"Idempotency-Key": "4b1d6e52-0c8a-4f7e-9a2b-7d3e1f0c9a11",
},
json={"partner_patron_ref": "ST-88121", "tap_id": 14, "ounces": 4},
)
pour = r.json()
{
"pour_id": "7f0c3a9e-2b1d-4e8a-9c5f-61d2e7a40b13",
"partner_patron_ref": "ST-88121",
"tap_id": 14,
"ounces": 4,
"status": "started"
}
Retrieve a pour
Returns the current state of a pour. Use it if a pour.closed webhook has not arrived yet.
Path parameters
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
Always pour.closed.
Unique ID for this delivery attempt.
sha256= followed by the HMAC-SHA256 of the raw body. See Verify signatures.
Body
pour.closed
The pour this event describes.
Your patron reference, as sent when the patron was verified.
Our identifier for the patron.
Location where the pour happened.
Ounces dispensed when the tap closed.
Retry schedule
| Attempt | Sent |
|---|---|
| 1 | Immediately |
| 2 | 1 minute later |
| 3 | 5 minutes later |
| 4 | 30 minutes later |
| 5 | 2 hours later |
| 6 | 12 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.
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
Your reference for the patron.
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.
Plain-language reason for a non-VERIFIED status.
Face-match score from 0 to 100.
True only when the patron can pour at your location.
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
Unique ID for this pour.
started Tap acknowledged the request.dispatched Tap is pouring.completed Tap closed. Final amounts set.notfound No pour with this ID for your client.
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
Stable identifier. Match on this.
Readable description. May change between releases.
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"
}
}