Migrating from V0
Overview
The Beam V0 API shuts down on 1 July 2027. Migrate every integration to the V1 API before that date.
The V0 API (partner-api.beamdata.co) is deprecated and is replaced by the V1 API (api.beamcheckout.com). Migration
is easier than it looks: every V0 endpoint already stores data in V1 format to Payment Links, Charges, and Refunds.
Reasons to Migrate to V1
- Clearer model. V0 packed the whole payment lifecycle into one Purchase object; V1 uses separate Payment Link, Charge, and Refund resources that each do one job, so requests and responses are easy to follow (see Concept).
- Your integration stays stable. Beam can add new payment methods and features without changing the fields you already use.
- Future-proof. New Beam features ship to V1 only — V0 receives none.
Concept
V0 modelled everything as one Purchase: the payment request and its result lived in the same record, which you also queried for customer data, status, and refunds.
V1 splits that lifecycle into three resources, each holding one part:
- Payment Link — the request, and the direct successor to the Purchase. It is a
Beam-hosted checkout page: create one for an order, redirect the shopper to its
url, and Beam collects payment. It does not hold the result. The result (who paid, how) lives in Charge, not the Payment Link. - Charge — one payment attempt. A payment link can have several; only a
SUCCEEDEDcharge means the shopper paid, and customer and payment-method data come from it. - Refund — tied to a
chargeId, not a payment link. Save thechargeIdfrom thecharge.succeededwebhook so you can refund later.
| V0 concept | V1 API concept |
|---|---|
| Purchase | Payment Link |
| Purchase ID | Payment Link ID |
| Payment ID | Charge ID for completed payments |
| Purchase state | Payment Link status and Charge status |
| Refund by purchase ID | Refund by charge ID, then lookup by Refund ID |
Before you start
V1 uses HTTP Basic Auth over HTTPS, and the Merchant ID is no longer in the URL path — Beam reads it from your
credentials. Authenticate with your Merchant API Key; partners use their Partner API Key plus the
X-Beam-Partner-ID header. See Authentication.
You don’t have to switch everything at once. V0 and V1 share the same underlying data, so you can run both in parallel, move traffic to V1 gradually, and retire V0 before the shutdown date.
Flow differences
In V0 you created one Purchase and read everything back from it. In V1 the same job is split across three resources (see Concept), so your API calls change. The table maps each step of the old flow to the new one.
V0 paths start from https://partner-api.beamdata.co; V1 paths start from https://api.beamcheckout.com.
| Step | V0 (partner-api.beamdata.co) | V1 (api.beamcheckout.com) |
|---|---|---|
| 1. Create | POST /purchases/{merchantId} → purchaseId + payment URL | POST /api/v1/payment-links → id + url |
| 2. Redirect | Send the shopper to the URL | Send the shopper to url |
| 3. Get notified | purchase.succeeded webhook (optional X-Hub-Signature) | payment_link.paid + charge.succeeded webhooks (HMAC-SHA256) — save the chargeId |
| 4. Read result | GET /purchases/{merchantId}/{purchaseId}/detail | GET /api/v1/charges?source_in=PAYMENT_LINK&sourceId={paymentLinkId} — use the SUCCEEDED charge |
| 5. Refund | POST /refunds/{merchantId}/{purchaseId} (by purchaseId) | POST /api/v1/refunds (by chargeId) |
Other operations:
| Operation | V0 | V1 | Reference |
|---|---|---|---|
| Get a payment link | GET /purchases/{merchantId}/{purchaseId} | GET /api/v1/payment-links/{paymentLinkId} | Payment Links API |
| Disable a payment link | POST /purchases/{merchantId}/{purchaseId}/disable | PATCH /api/v1/payment-links/{paymentLinkId}/disable | Payment Links API |
| Find a link’s charges | GET /purchases/{merchantId}/{purchaseId}/detail | GET /api/v1/charges?source_in=PAYMENT_LINK&sourceId={paymentLinkId} | Charges API |
| Create a refund | POST /refunds/{merchantId}/{purchaseId} | POST /api/v1/refunds (by chargeId) | Refunds API |
| Look up a refund | GET /refunds/{merchantId}/{purchaseId} | GET /api/v1/refunds/{refundId} | Refunds API |
Migration checklist
- Switch the base URL to
api.beamcheckout.com(update your DNS allowlists and firewalls). - Update server-side auth to V1 credentials — HTTP Basic Auth, with no
{merchantId}in the path. - Replace purchase creation with
POST /api/v1/payment-links, and save the returnedidas yourpaymentLinkId. - Redirect shoppers to the returned
url. - Set up a webhook for
payment_link.paidandcharge.succeeded, and save eachchargeId. - Replace
/detailreads withGET /api/v1/charges?source_in=PAYMENT_LINK&sourceId={paymentLinkId}. - Replace refunds with
POST /api/v1/refunds(bychargeId), and save the returnedrefundId. - Add
x-beam-idempotency-keyto allPOSTandPATCHretries. See Idempotency. - Test the full flow on Playground before switching production traffic, and confirm your records reconcile with V1 IDs and amount units.
What else to watch for
- Status codes changed. Create returns
201 Created; disable returns202 Accepted. V0 returned200for both. Update any code that checks for exactly200. - Amounts are in the smallest currency unit.
900becomes90000for THB. See Field mapping. - Refunds use a
chargeId, not a payment link ID. Partial refunds areCARD-only, and the V0optionandtypefields are gone. See Refunds API. - Webhooks are signed and split. V1 signs every webhook with HMAC-SHA256 over the raw body, and replaces the single
purchase.succeededevent withpayment_link.paidandcharge.succeeded. See Webhooks. /detailis gone. Read customer and payment-method data from theSUCCEEDEDcharge instead, and note that less customer data is returned than in V0. See Detail response replacement.- Errors are structured. Branch on
error.errorCode, not the message. See Error responses. - Playground and Production keys differ. A
401 INVALID_CREDENTIALS_ERRORusually means a Playground key was used against Production (or the wrong key type). See Authentication. - Some request fields changed.
order.currencyis required,order.totalAmountandorder.totalDiscountare removed (send the final amount asnetAmount), andcollectPhoneNumberdefaults totrue. See Field mapping. installmentOptionsis restructured. See Installment options.
Webhooks
Migrate legacy events to V1 webhooks
V0 sent one event, purchase.succeeded. In V1 this becomes two events: payment_link.paid (fulfill the order) and
charge.succeeded (save the chargeId for future refunds). Register your webhook URL
in Lighthouse under Developers and subscribe to both.
See Event Types for each event’s payload.
V1 signs every webhook with HMAC-SHA256 over the raw, unformatted request body — reformatting the JSON before hashing breaks the signature. See Webhook authentication.
Migrate V0 state polling
Replace V0 state polling with webhooks. If you must poll, use the charges query instead.
See List Charges API.
V0 state | What it means | V1 replacement |
|---|---|---|
initiated / beamed / paying | Not paid yet | PaymentLink status == ACTIVE and no SUCCEEDED charge |
complete | Payment succeeded | Listen to payment_link.paid or charge.succeeded, or query charges for a SUCCEEDED charge |
failed | Payment attempt failed | Listen to charge.failed, or query charges and look for a FAILED charge |
Create a payment link
The request body is reshaped: amounts use the smallest currency unit, order fields are renamed, orderItems flatten
out of the nested product object, and payment methods move into linkSettings. The example below omits orderItems
for brevity — see Field mapping for the full list, or
Payment Links API for the complete schema.
Before (V0):
{
"order": {
"currencyCode": "THB",
"description": "1 Cookies Box",
"merchantReference": "cookies order 1",
"merchantReferenceId": "merchantRef1",
"netAmount": 900,
"totalAmount": 1000,
"totalDiscount": 100,
"orderItems": [
{
"product": {
"name": "Cookies",
"price": 1000,
"sku": "string"
},
"quantity": 1
}
]
},
"redirectUrl": "string",
"requiredFieldsFormId": "formId123",
"supportedPaymentMethods": [
"creditCard"
]
}After (V1 API):
{
"order": {
"currency": "THB",
"description": "1 Cookies Box",
"internalNote": "cookies order 1",
"referenceId": "merchantRef1",
"netAmount": 90000
},
"expiresAt": "2030-01-01T00:00:00.000Z",
"redirectUrl": "https://example.com/payment-complete",
"collectPhoneNumber": false,
"linkSettings": {
"card": {
"isEnabled": true
},
"qrPromptPay": {
"isEnabled": true
}
}
}End-to-end example
How the pieces connect in a single payment:
- Create —
POST /api/v1/payment-linksreturnsid(use it as yourpaymentLinkId) andurl. See Payment Links API. - Redirect the shopper to
url; Beam hosts the checkout. - Receive webhooks —
payment_link.paidandcharge.succeeded. Thecharge.succeededbody is the Charge; save itschargeId. See Event Types. - Read details (optional) —
GET /api/v1/charges?source_in=PAYMENT_LINK&sourceId={paymentLinkId}; use theSUCCEEDEDcharge for customer and payment-method data. See Charges API. - Refund (later) —
POST /api/v1/refundswith the savedchargeId; store the returnedrefundId. See Refunds API.
The IDs chain through the flow: paymentLinkId → chargeId → refundId. Keep each one as you go — you cannot refund
without the chargeId.
Error responses
The V1 API returns a consistent error format on every 4xx/5xx response. Check error.errorCode (not message) to
decide
what to do. See Error Handling for the full enum.
Appendix
Field-level reference for the migration: converting V0 request payloads and replacing the V0 detail response. Use these tables once your main flow works.
Installment options
Before: "installmentOptions": [{ "period": 3 }, { "period": 6 }]
After:
{
"linkSettings": {
"cardInstallments": {
"isEnabled": true,
"installments3m": {
"isEnabled": true
},
"installments4m": {
"isEnabled": false
},
"installments6m": {
"isEnabled": true
},
"installments10m": {
"isEnabled": false
}
}
}
}Detail response replacement
- Payment link details →
GET /api/v1/payment-links/{paymentLinkId} - Customer and payment-method data →
GET /api/v1/charges?source_in=PAYMENT_LINK&sourceId={paymentLinkId}
| V0 detail field | V1 API source |
|---|---|
purchaseId | Payment Link paymentLinkId |
genericPurchaseId | No public equivalent. Track the link yourself if you used this. |
merchantId | Payment Link, Charge, and Refund merchantId |
paymentId | No equivalent. Use paymentLinkId (link) and chargeId (paid operations). |
paymentLink | Payment Link url |
order | Payment Link order |
redirectUrl | Payment Link redirectUrl |
expiry | Payment Link expiresAt |
isDisabled | Payment Link status == "DISABLED" |
state | Payment Link status and Charge status |
merchantBasicInfo.name, merchantBasicInfo.logoUrl | Not in the public API — keep merchant display info in your own system. |
merchantBasicInfo.availablePaymentMethods | Use Payment Link linkSettings plus the methods enabled for your account. |
requiredFieldsFormId | Payment Link collectDeliveryAddress and collectPhoneNumber |
customer | SUCCEEDED Charge customer (only primaryPhone, email, deliveryAddress). V0 firstName, lastName, title, dateOfBirth, billingAddress, social handles, and additionalField0..9 have no equivalent. |
paymentMethod | SUCCEEDED Charge paymentMethod.paymentMethodType (e.g. CARD, QR_PROMPT_PAY). |
installmentPeriod | SUCCEEDED Charge paymentMethod.cardInstallments.installmentPeriod (months). Only when paymentMethodType == CARD_INSTALLMENTS. |
created | SUCCEEDED Charge transactionTime (payment time), or Charge createdAt. Not on the public Payment Links API. |
lastUpdated | Charge updatedAt. Not on the public Payment Links API. |
originator.email | Not in the public API — track the creator yourself. |
refunds | Store refundId from POST /api/v1/refunds, then call GET /api/v1/refunds/{refundId}. |
Payment method mapping
| V0 payment method | V1 API payment link setting |
|---|---|
creditCard | linkSettings.card.isEnabled |
installmentsCc | linkSettings.cardInstallments.isEnabled |
qrThb | linkSettings.qrPromptPay.isEnabled |
eWallet | linkSettings.eWallets.isEnabled |
internetBanking | linkSettings.mobileBanking.isEnabled |
bnpl | linkSettings.buyNowPayLater.isEnabled |
Field mapping
| V0 field | V1 API field | Notes |
|---|---|---|
order.netAmount | order.netAmount | Convert to the smallest currency unit. 900 becomes 90000 for THB. |
order.currencyCode | order.currency | Renamed. V1 requires currency; currently only THB is supported. |
order.merchantReferenceId | order.referenceId | Use this for your order ID or matching reference. |
order.merchantReference | order.internalNote | Internal merchant note. |
order.totalAmount | No direct equivalent | The V1 API charges order.netAmount. Calculate discounts and the total amount in your own order system. |
order.totalDiscount | No direct equivalent | Send the final payable amount as order.netAmount. |
order.orderItems[].product.* | order.orderItems[].* | Fields move out of the nested product object to the top level (e.g. name → itemName); quantity is unchanged. Convert price to the smallest currency unit. |
| (no V0 equivalent) | order.orderItems[].productId | Optional internal product identifier. |
expiry | expiresAt | Renamed. |
requiredFieldsFormId | collectDeliveryAddress and collectPhoneNumber | Use collectDeliveryAddress: true when you need delivery address collection. Phone number collection is enabled by default. |
supportedPaymentMethods | linkSettings | Configure payment methods explicitly in the payment link request. |
| Failure reason on a failed payment | Charge failureCode | Read failureCode from the Charge object (also present in webhook payloads); it is one of the CH_* enum codes and is absent when the charge did not fail. |