QR Slip Verification API
The QR Slip Verification API confirms a bank transfer payment from the slip your shopper sends you, for payments made on Beam.
The following operation is available:
- Verify Charge with QR Slip (
POST /api/v1/charges/verify-qr-slip): match a slip against your charges and update the matching charge toSUCCEEDEDif it is stillPENDING.
For the concepts behind this endpoint, see QR Slip Verification. For the full schema, see the API reference.
Beam enables this API separately from the QR slip verification feature in Lighthouse, so contact Beam support
before you build this integration. Until then, every request to this endpoint returns 403 Forbidden. See
QR Slip Verification.
Verify a charge with a QR slip
The request uses multipart/form-data. The format field tells Beam which other field carries the QR code:
format | Description |
|---|---|
RAW | The content of the QR code from the bank transfer slip, sent in the raw field. Use this if your own app already scans the QR code from the slip. |
IMAGE | The slip image file, sent in the image field. Beam reads the QR code for you. |
Send the QR code on the slip, not the QR code that the shopper scanned to pay. See Slips you can verify.
Verify with the raw QR code content
Set format=RAW and put the raw content of the QR code in the raw field:
POST https://api.beamcheckout.com/api/v1/charges/verify-qr-slip
Authorization: Basic <base64(merchantId:apiKey)>
Content-Type: multipart/form-data; boundary=WebAppBoundary
--WebAppBoundary
Content-Disposition: form-data; name="format"
RAW
--WebAppBoundary
Content-Disposition: form-data; name="raw"
0041000600000101030040220015077082818AQR086795102TH9104FF93
--WebAppBoundary--Send the QR code content exactly as your scanner reads it, do not modify the content.
Verify with the slip image
Set format=IMAGE and put the slip image in the image field:
POST https://api.beamcheckout.com/api/v1/charges/verify-qr-slip
Authorization: Basic <base64(merchantId:apiKey)>
Content-Type: multipart/form-data; boundary=WebAppBoundary
--WebAppBoundary
Content-Disposition: form-data; name="format"
IMAGE
--WebAppBoundary
Content-Disposition: form-data; name="image"; filename="example-slip.png"
< example-slip.png
--WebAppBoundary--The image must be a JPEG or PNG file, and its size must not exceed 2 MB.
Handle the verification response
A 200 means the slip matched one of your charges and that charge is now SUCCEEDED. The body carries the
chargeId of the matched charge and the verificationResult of the check:
{
"chargeId": "ch_2xTsz7Qit55pahSvKfJG3UMkpFQ",
"verificationResult": "UPDATED_TO_SUCCEEDED"
}verificationResult | Charge status before your call | What Beam does | What you do |
|---|---|---|---|
UPDATED_TO_SUCCEEDED | PENDING | Updates the charge to SUCCEEDED and sends the related webhooks, such as charge.succeeded | Mark the order as paid |
ALREADY_SUCCEEDED | SUCCEEDED | Leaves the charge as it is, because the payment is already confirmed | Mark the order as paid, if you have not already |
Read verificationResult when you need to know which case happened. This is also what makes retries safe. If you
send the same slip again, the second call returns 200 with the same chargeId and ALREADY_SUCCEEDED.
When Beam updates a charge to SUCCEEDED, we also sends the related webhooks, such as charge.succeeded. These are
the same events you get when a charge succeeds in any other way. See
Event Types for the payloads.
Errors
When Beam cannot verify the slip, it returns a standard error response and no charge changes:
| Status Code | Error Code Enum | Meaning | Recommended action |
|---|---|---|---|
| 400 Bad Request | API_VALIDATION_ERROR | Beam cannot read a QR code from the image, or the content of the QR code is not valid | Ask the shopper for a clearer slip, or check the QR code content you sent |
| 403 Forbidden | NO_PERMISSION_ERROR | Your merchant account is not allowed to use this API | Contact Beam support to enable it |
| 404 Not Found | NOT_FOUND_ERROR | The slip does not match any of your charges | Ask the shopper for the correct slip |
For example, when no charge matches the slip:
{
"code": 404,
"message": "not found",
"error": {
"errorCode": "NOT_FOUND_ERROR",
"errorMessage": "not found"
}
}Check the HTTP status code to know if the payment is verified. verificationResult only separates the two success
cases, and it never reports a failure. A slip that Beam cannot read or match comes back as a 4xx error instead.
Testing on Playground
Beam matches a slip against a charge that the shopper paid by bank transfer, and that transfer has to be real. Playground has no real bank transfers, so you cannot test the success case there. What you can test is whether Beam accepts the slip or the QR code content you send, which covers the rest of your integration.
Send your test requests to https://playground.api.beamcheckout.com/api/v1/charges/verify-qr-slip with your
Playground API key:
format | What you send | What Beam returns |
|---|---|---|
IMAGE | An image that Beam cannot read a QR code from | 400 with API_VALIDATION_ERROR |
IMAGE | A slip image whose QR code is not a bank transfer QR code | 400 with API_VALIDATION_ERROR |
IMAGE | A real bank transfer slip with a valid QR code | 404 with NOT_FOUND_ERROR |
RAW | Text that is not QR code content, for example a link or a random string | 400 with API_VALIDATION_ERROR |
RAW | The content of a QR code that is not a bank transfer QR code | 400 with API_VALIDATION_ERROR |
RAW | The content of a valid QR code, copied from a real bank transfer slip | 404 with NOT_FOUND_ERROR |
On Playground, a 404 is the result you want. It means Beam read the QR code and looked for a matching charge, and
only that charge is missing, because no Playground charge comes from a real bank transfer.
Your authentication, your multipart/form-data body, and the QR code itself are all correct.
Test the 200 case in production, with a charge that the shopper paid by bank transfer. Production is also the only
place where you see verificationResult. The first call on a PENDING charge returns UPDATED_TO_SUCCEEDED, and
the same slip sent again returns ALREADY_SUCCEEDED.