Melostore H2H · API v1
H2H API documentation
Start integrating, choose products, and follow every order result.
Base URL https://api.melostore.id/api/v1/h2h
Import OpenAPI or Postman and fill your environment credentials. Order examples use sandbox.
Start integration
Base URL: https://api.melostore.id/api/v1/h2h. All examples use fictional data; replace example SKUs with your account catalogue. There is no separate introduction/authentication endpoint: use GET /profile to check access.
In the dashboard API Credentials menu, click Rotate Keys to generate the API Key, Secret Key, and Webhook Secret. All three are shown once in the generation modal; save them before closing it. Subsequent rotations replace all three immediately, so update both your API credentials and the secret on your webhook receiver.
Import a collection below, select the environment, fill api_key and secret_key from the generated credentials, and choose active SKUs from the regular or Smart catalogue. Keep buyer_trx_id/buyer_bulk_id when retrying a request without a response. Save the returned ID for status checks.
Regular, Smart and bulk create examples use sandbox_mode=true. Sandbox simulates processing/history/callback without debits or financial invoices/email. Dashboard orders are always live. Account checks follow account mode and their own billing; sandbox_mode on order creation does not control account checks.
Authentication, IP, and rate limits
Send Accept: application/json for JSON validation errors; a successful invoice still returns Content-Type application/pdf. Send X-API-Key and X-Secret-Key together on authenticated operations. Obtain credentials from the dashboard; do not expose them in a public browser. GET /check-nickname/games and GET /check-nickname/plan are public. The account must be active and not suspended.
If you use an IP whitelist, register your integration server's public IP in the dashboard before placing live orders. HTTP 403 may indicate that the account or IP is not permitted; check your account settings and server IP.
The general limit follows the tier, shown in X-RateLimit-Limit and X-RateLimit-Remaining. Regular/Smart/category catalogue endpoints also share 20 requests/minute per profile. Account checks use X-Nickname-RateLimit-Limit and X-Nickname-RateLimit-Remaining. If HTTP 429 includes Retry-After, wait that number of seconds; daily allowance resets at 00:00 UTC.
Profile
/api/v1/h2h/profileAuthenticated profile and tier. MC/IDR is the wallet currency; USD is a display estimate.
Both headers: X-API-Key and X-Secret-Key
Parameters
No parameters.
curl -X GET 'https://api.melostore.id/api/v1/h2h/profile' \
-H 'Accept: application/json' \
-H "X-API-Key: $H2H_API_KEY" \
-H "X-Secret-Key: $H2H_SECRET_KEY"Request succeeded.
{
"success": true,
"data": {
"id": "11111111-1111-4111-8111-111111111111",
"name": "Example Partner",
"email": "[email protected]",
"tier": {
"name": "Basic",
"code": "basic",
"rate_limit": 30,
"max_ips": 3
},
"h2h_balance": 100000,
"is_sandbox_mode": true,
"total_volume_30_days": 5000,
"webhook_url": "https://partner.example.com/h2h-callback",
"created_at": "2026-10-02T03:00:00+00:00",
"usd_idr_rate": 17000,
"display_usd_idr_rate": 17000,
"display_rate_updated_at": "2026-10-02T03:00:00+00:00"
}
}successbooleanRequireddataobjectRequireddata.idstringRequireddata.namestringRequireddata.emailstringRequireddata.tierobjectRequireddata.tier.namestringRequireddata.tier.codestringRequireddata.tier.rate_limitintegerRequireddata.tier.max_ipsintegerRequireddata.h2h_balancenumberRequireddata.is_sandbox_modebooleanRequireddata.total_volume_30_daysnumberRequireddata.webhook_urlstring | nullRequireddata.created_atstringRequireddata.usd_idr_ratenumberRequired- Configured deposit conversion rate; separate from the reference display rate.
data.display_usd_idr_ratenumber | nullRequireddata.display_rate_updated_atstring | nullRequired- Last reference FX update, or null when a valid reference rate is unavailable.
Balance and FX
/api/v1/h2h/profile/balanceReference FX is separate from configured deposit conversion. Nullable USD estimate when reference FX is unavailable.
Both headers: X-API-Key and X-Secret-Key
Parameters
No parameters.
curl -X GET 'https://api.melostore.id/api/v1/h2h/profile/balance' \
-H 'Accept: application/json' \
-H "X-API-Key: $H2H_API_KEY" \
-H "X-Secret-Key: $H2H_SECRET_KEY"Request succeeded.
{
"success": true,
"data": {
"h2h_balance": 100000,
"is_sandbox_mode": true,
"usd_idr_rate": 17000,
"display_usd_idr_rate": 17000,
"display_rate_updated_at": "2026-10-02T03:00:00+00:00",
"h2h_balance_usd": 5.88
}
}successbooleanRequireddataobjectRequireddata.h2h_balancenumberRequireddata.is_sandbox_modebooleanRequireddata.usd_idr_ratenumberRequired- Configured deposit conversion rate; separate from the reference display rate.
data.display_usd_idr_ratenumber | nullRequireddata.display_rate_updated_atstring | nullRequired- Last reference FX update, or null when a valid reference rate is unavailable.
data.h2h_balance_usdnumber | nullRequired
Fetch prices for one brand
1. Fetch GET /pricelists/categories for regular brands, or GET /smart-pricelists/categories for Smart brands. Each record is a brand with its parent category object in category and child product types in type. Results are sorted by parent category ID ascending, then brand name A–Z.
2. Choose a data[] entry and use brand_id as the filter: GET /pricelists?brand_id=10&limit=500 or GET /smart-pricelists?brand_id=10&limit=500. Replace 10 with brand_id from your response. In the brand directory, brand_id replaces id; update your integration to read this field.
3. Alternatively, use the brand slug in /pricelists/{category_slug} or /smart-pricelists/{category_slug}. Copy the exact slug from the matching brand list rather than guessing it from a game name.
meta.brands describes the current price page only. Use the brands endpoint for the full list. A listed brand can include out_of_stock products; only order active products.
category is null when the brand has no parent category; these brands appear last. type contains all direct child types sorted by name A–Z, or [] when none exist. A type entry does not guarantee an available product in the selected catalogue.
SKUs, servers, and customer fields
sku_code identifies a regular product for /transaction and bulk. smart_sku_code identifies a Smart product; pass its value as sku_code to /smart-transaction. Both come from the catalogue, independently of account-check game codes.
brand_id and slug come from the brands list. server_code groups products within the catalogue; customer_target_zone identifies the buyer account zone/server according to the product form. Their values need not match.
For the regular catalogue, omit server or leave it empty to include all servers. Omit format or leave it empty for compact; choose legacy to embed inquiry_fields in each product without pagination.
Look up the product inquiry_form_key in meta.inquiry_forms on the same page. Map the first field to customer_target, the second to customer_target_zone when present, and other fields to additional_data using their form keys. For selections, submit value rather than label. Read notice.id or notice.en for extra instructions.
game_code is specific to account checks and comes from games[].game_code in GET /check-nickname/games. Account-check support does not imply that all similar product names share the same zones or conditions.
Pagination and customer details
Fetch the catalogue with limit=500. Pass meta.pagination.next_cursor as cursor on the next request and continue until meta.pagination.has_more=false. The available limit range is 1–1000; use at most 500 per page. This applies to both regular and Smart catalogues.
Follow the selected product's form to enter customer details. In compact format, use inquiry_form_key to find the form in meta.inquiry_forms. Set customer_target from the first field, customer_target_zone from the second when required, and additional_data for other fields using the form's keys. In regular legacy format, the form is available directly in inquiry_fields.
One regular or Smart request creates one order. To repeat a regular SKU for the same target, use bulk order_count (1–100). Bulk orders are processed one at a time in sequence.
Regular catalogue
/api/v1/h2h/pricelistsPrices for your account tier. Choose an active sku_code and use POST /transaction. Use bulk order_count for repeated orders. For one brand, pass brand_id from the matching brands list or use its slug in the path. Follow next_cursor until has_more=false.
Both headers: X-API-Key and X-Secret-Key
Parameters
| Name | Type / source | Required | Description |
|---|---|---|---|
| limit | integer · query | Optional | Items per page, 1–1000. Use 500 for synchronisation. Without limit, regular responses return all matching products; send limit to paginate. min=1 max=1000 |
| cursor | string · query | Optional | Copy meta.pagination.next_cursor from the previous page. Keep the same filters while paging. |
| server | string · query | Optional | Product catalogue server_code, not the customer account zone. Omit or leave empty to include all catalogue servers. |
| format | compact | legacy · query | Optional | Omit or leave empty for compact. legacy embeds inquiry_fields and returns all matching items without cursor pagination. |
| brand_id | integer · query | Optional | Filter by data[].brand_id from the matching catalogue brands endpoint. Omit to fetch all brands. min=1 |
curl -X GET 'https://api.melostore.id/api/v1/h2h/pricelists?limit=500' \
-H 'Accept: application/json' \
-H "X-API-Key: $H2H_API_KEY" \
-H "X-Secret-Key: $H2H_SECRET_KEY"Request succeeded.
{
"success": true,
"data": [
{
"name": "Example product",
"sku_code": "EXAMPLE-10",
"price": 1000,
"category_name": "Example category",
"brand_id": 10,
"type_name": "Example type",
"category_order": 1,
"type_order": 1,
"server_code": "global",
"server_name": "Global",
"status": "active",
"inquiry_form_key": "example-form",
"h2h_description": null,
"description": null,
"id": 1
}
],
"meta": {
"brands": [
{
"id": 10,
"name": "Example brand",
"thumbnail": null,
"order": 1
}
],
"inquiry_forms": {
"example-form": {
"fields": [
{
"key": "customer_target",
"name": "Player ID",
"type": "text"
},
{
"key": "customer_target_zone",
"name": "Zone",
"type": "text"
}
],
"notice": {
"id": "Masukkan ID dan zone sesuai akun pembeli.",
"en": "Enter the ID and zone from the buyer account."
}
}
},
"pagination": {
"cursor": null,
"next_cursor": null,
"limit": 100,
"total": 1,
"has_more": false
}
}
}successbooleanRequireddataarrayRequireddata[].namestringRequireddata[].sku_codestringRequireddata[].pricenumberRequireddata[].category_namestringRequireddata[].brand_idinteger | nullRequired- Brand ID; full brand discovery is available from the matching categories endpoint.
data[].type_namestringRequireddata[].category_orderintegerRequireddata[].type_orderintegerRequireddata[].server_codestring | nullRequired- Catalogue product server. This is separate from customer_target_zone.
data[].server_namestring | nullRequireddata[].statusactive | out_of_stockRequired- Order only active products. out_of_stock means unavailable.
data[].inquiry_form_keystring- Look up this key in meta.inquiry_forms on this same page.
data[].h2h_descriptionstring | nullRequireddata[].descriptionstring | nullRequireddata[].idintegermetaobjectmeta.brandsarray- Brand metadata for this page only. Use the categories endpoint for the full list.
meta.brands[].idintegermeta.brands[].namestring | nullmeta.brands[].thumbnailstring | nullmeta.brands[].orderinteger | nullmeta.inquiry_formsobject / array- Product input forms keyed by inquiry_form_key. An empty result may be {} or [].
meta.inquiry_forms.{key}.fieldsarraymeta.inquiry_forms.{key}.fields[].keystringmeta.inquiry_forms.{key}.fields[].namestringmeta.inquiry_forms.{key}.fields[].typestringmeta.inquiry_forms.{key}.fields[].requiredboolean- Whether the buyer must provide this field.
meta.inquiry_forms.{key}.fields[].optionsarray- For selection fields, display label and submit value.
meta.inquiry_forms.{key}.fields[].options[].labelstringmeta.inquiry_forms.{key}.fields[].options[].valuestring | numbermeta.inquiry_forms.{key}.fields[].placeholderstring | nullmeta.inquiry_forms.{key}.noticeobject | null- Instructions by language. Read notice.id or notice.en; null when absent.
meta.inquiry_forms.{key}.notice.idstringmeta.inquiry_forms.{key}.notice.enstringmeta.paginationobjectmeta.pagination.cursorstring | nullmeta.pagination.next_cursorstring | nullmeta.pagination.limitintegermeta.pagination.totalintegermeta.pagination.has_morebooleancategoryobjectcategory.namestringRequiredcategory.slugstringRequireddata[].inquiry_fieldsobject- Included directly only in the regular legacy format.
data[].inquiry_fields.fieldsarraydata[].inquiry_fields.fields[].keystringdata[].inquiry_fields.fields[].namestringdata[].inquiry_fields.fields[].typestringdata[].inquiry_fields.fields[].requiredboolean- Whether the buyer must provide this field.
data[].inquiry_fields.fields[].optionsarray- For selection fields, display label and submit value.
data[].inquiry_fields.fields[].options[].labelstringdata[].inquiry_fields.fields[].options[].valuestring | numberdata[].inquiry_fields.fields[].placeholderstring | nulldata[].inquiry_fields.noticeobject | null- Instructions by language. Read notice.id or notice.en; null when absent.
data[].inquiry_fields.notice.idstringdata[].inquiry_fields.notice.enstringdata[].brand_namestring | null- Included directly only in the regular legacy format.
data[].brand_orderinteger | null- Included directly only in the regular legacy format.
data[].brand_thumbnailstring | null- Included directly only in the regular legacy format.
Regular prices by brand
/api/v1/h2h/pricelists/{category_slug}Prices for your account tier. Choose an active sku_code and use POST /transaction. Use bulk order_count for repeated orders. For one brand, pass brand_id from the matching brands list or use its slug in the path. Follow next_cursor until has_more=false.
Both headers: X-API-Key and X-Secret-Key
Parameters
| Name | Type / source | Required | Description |
|---|---|---|---|
| category_slug | string · path | Required | Brand slug from data[].slug in the matching brands endpoint. Existing category/type slugs remain supported. |
| limit | integer · query | Optional | Items per page, 1–1000. Use 500 for synchronisation. Without limit, regular responses return all matching products; send limit to paginate. min=1 max=1000 |
| cursor | string · query | Optional | Copy meta.pagination.next_cursor from the previous page. Keep the same filters while paging. |
| server | string · query | Optional | Product catalogue server_code, not the customer account zone. Omit or leave empty to include all catalogue servers. |
| format | compact | legacy · query | Optional | Omit or leave empty for compact. legacy embeds inquiry_fields and returns all matching items without cursor pagination. |
| brand_id | integer · query | Optional | Filter by data[].brand_id from the matching catalogue brands endpoint. Omit to fetch all brands. min=1 |
curl -X GET 'https://api.melostore.id/api/v1/h2h/pricelists/example-brand?limit=500' \
-H 'Accept: application/json' \
-H "X-API-Key: $H2H_API_KEY" \
-H "X-Secret-Key: $H2H_SECRET_KEY"Request succeeded.
{
"success": true,
"data": [
{
"name": "Example product",
"sku_code": "EXAMPLE-10",
"price": 1000,
"category_name": "Example category",
"brand_id": 10,
"type_name": "Example type",
"category_order": 1,
"type_order": 1,
"server_code": "global",
"server_name": "Global",
"status": "active",
"inquiry_form_key": "example-form",
"h2h_description": null,
"description": null,
"id": 1
}
],
"meta": {
"brands": [
{
"id": 10,
"name": "Example brand",
"thumbnail": null,
"order": 1
}
],
"inquiry_forms": {
"example-form": {
"fields": [
{
"key": "customer_target",
"name": "Player ID",
"type": "text"
},
{
"key": "customer_target_zone",
"name": "Zone",
"type": "text"
}
],
"notice": null
}
},
"pagination": {
"cursor": null,
"next_cursor": null,
"limit": 100,
"total": 1,
"has_more": false
}
},
"category": {
"name": "Example brand",
"slug": "example-brand"
}
}successbooleanRequireddataarrayRequireddata[].namestringRequireddata[].sku_codestringRequireddata[].pricenumberRequireddata[].category_namestringRequireddata[].brand_idinteger | nullRequired- Brand ID; full brand discovery is available from the matching categories endpoint.
data[].type_namestringRequireddata[].category_orderintegerRequireddata[].type_orderintegerRequireddata[].server_codestring | nullRequired- Catalogue product server. This is separate from customer_target_zone.
data[].server_namestring | nullRequireddata[].statusactive | out_of_stockRequired- Order only active products. out_of_stock means unavailable.
data[].inquiry_form_keystring- Look up this key in meta.inquiry_forms on this same page.
data[].h2h_descriptionstring | nullRequireddata[].descriptionstring | nullRequireddata[].idintegermetaobjectmeta.brandsarray- Brand metadata for this page only. Use the categories endpoint for the full list.
meta.brands[].idintegermeta.brands[].namestring | nullmeta.brands[].thumbnailstring | nullmeta.brands[].orderinteger | nullmeta.inquiry_formsobject / array- Product input forms keyed by inquiry_form_key. An empty result may be {} or [].
meta.inquiry_forms.{key}.fieldsarraymeta.inquiry_forms.{key}.fields[].keystringmeta.inquiry_forms.{key}.fields[].namestringmeta.inquiry_forms.{key}.fields[].typestringmeta.inquiry_forms.{key}.fields[].requiredboolean- Whether the buyer must provide this field.
meta.inquiry_forms.{key}.fields[].optionsarray- For selection fields, display label and submit value.
meta.inquiry_forms.{key}.fields[].options[].labelstringmeta.inquiry_forms.{key}.fields[].options[].valuestring | numbermeta.inquiry_forms.{key}.fields[].placeholderstring | nullmeta.inquiry_forms.{key}.noticeobject | null- Instructions by language. Read notice.id or notice.en; null when absent.
meta.inquiry_forms.{key}.notice.idstringmeta.inquiry_forms.{key}.notice.enstringmeta.paginationobjectmeta.pagination.cursorstring | nullmeta.pagination.next_cursorstring | nullmeta.pagination.limitintegermeta.pagination.totalintegermeta.pagination.has_morebooleancategoryobjectcategory.namestringRequiredcategory.slugstringRequireddata[].inquiry_fieldsobject- Included directly only in the regular legacy format.
data[].inquiry_fields.fieldsarraydata[].inquiry_fields.fields[].keystringdata[].inquiry_fields.fields[].namestringdata[].inquiry_fields.fields[].typestringdata[].inquiry_fields.fields[].requiredboolean- Whether the buyer must provide this field.
data[].inquiry_fields.fields[].optionsarray- For selection fields, display label and submit value.
data[].inquiry_fields.fields[].options[].labelstringdata[].inquiry_fields.fields[].options[].valuestring | numberdata[].inquiry_fields.fields[].placeholderstring | nulldata[].inquiry_fields.noticeobject | null- Instructions by language. Read notice.id or notice.en; null when absent.
data[].inquiry_fields.notice.idstringdata[].inquiry_fields.notice.enstringdata[].brand_namestring | null- Included directly only in the regular legacy format.
data[].brand_orderinteger | null- Included directly only in the regular legacy format.
data[].brand_thumbnailstring | null- Included directly only in the regular legacy format.
Smart catalogue
/api/v1/h2h/smart-pricelistsPrices for your account tier. Choose an active smart_sku_code and use POST /smart-transaction. For one brand, pass brand_id from the matching brands list or use its slug in the path. Follow next_cursor until has_more=false.
Both headers: X-API-Key and X-Secret-Key
Parameters
| Name | Type / source | Required | Description |
|---|---|---|---|
| limit | integer · query | Optional | Items per page, 1–1000. Use 500 for synchronisation. Default 500. min=1 max=1000 |
| cursor | string · query | Optional | Copy meta.pagination.next_cursor from the previous page. Keep the same filters while paging. |
| brandCompatibility | integer / string · query | Optional | Compatibility alias: brand ID or exact brand name. Prefer brand_id. |
| categoryCompatibility | integer / string · query | Optional | Compatibility filter for a root category ID or exact name. Prefer brand_id or a brand slug. |
| search | string · query | Optional | Search product name or Smart SKU. |
| brand_id | integer · query | Optional | Filter by data[].brand_id from the matching catalogue brands endpoint. Omit to fetch all brands. min=1 |
curl -X GET 'https://api.melostore.id/api/v1/h2h/smart-pricelists?limit=500' \
-H 'Accept: application/json' \
-H "X-API-Key: $H2H_API_KEY" \
-H "X-Secret-Key: $H2H_SECRET_KEY"Request succeeded.
{
"success": true,
"data": [
{
"name": "Example product",
"category_name": "Example category",
"brand_id": 10,
"type_name": "Example type",
"category_order": 1,
"type_order": 1,
"status": "active",
"inquiry_form_key": "example-form",
"h2h_description": null,
"description": null,
"smart_sku_code": "smartEXAMPLE",
"min_price": 1000,
"max_price": 1000,
"variant_count": 1,
"variants": [
{
"sku_code": "EXAMPLE-10",
"price": 1000,
"status": "active"
}
]
}
],
"meta": {
"brands": [
{
"id": 10,
"name": "Example brand",
"thumbnail": null,
"order": 1
}
],
"inquiry_forms": {
"example-form": {
"fields": [
{
"key": "customer_target",
"name": "Player ID",
"type": "text"
},
{
"key": "customer_target_zone",
"name": "Zone",
"type": "text"
}
],
"notice": null
}
},
"pagination": {
"cursor": null,
"next_cursor": null,
"limit": 100,
"has_more": false
}
}
}successbooleanRequireddataarrayRequireddata[].namestringRequireddata[].category_namestringRequireddata[].brand_idinteger | nullRequired- Brand ID; full brand discovery is available from the matching categories endpoint.
data[].type_namestringRequireddata[].category_orderintegerRequireddata[].type_orderintegerRequireddata[].statusactive | out_of_stockRequired- Order only active products. out_of_stock means unavailable.
data[].inquiry_form_keystringRequired- Look up this key in meta.inquiry_forms on this same page.
data[].h2h_descriptionstring | nullRequireddata[].descriptionstring | nullRequireddata[].smart_sku_codestringRequireddata[].min_priceintegerRequireddata[].max_priceintegerRequireddata[].variant_countintegerRequireddata[].variantsarrayRequireddata[].variants[].sku_codestringRequireddata[].variants[].priceinteger | nullRequireddata[].variants[].statusstringRequiredmetaobjectRequiredmeta.brandsarrayRequired- Brand metadata for this page only. Use the categories endpoint for the full list.
meta.brands[].idintegerRequiredmeta.brands[].namestring | nullRequiredmeta.brands[].thumbnailstring | nullRequiredmeta.brands[].orderinteger | nullRequiredmeta.inquiry_formsobject / arrayRequired- Product input forms keyed by inquiry_form_key. An empty result may be {} or [].
meta.inquiry_forms.{key}.fieldsarraymeta.inquiry_forms.{key}.fields[].keystringmeta.inquiry_forms.{key}.fields[].namestringmeta.inquiry_forms.{key}.fields[].typestringmeta.inquiry_forms.{key}.fields[].requiredboolean- Whether the buyer must provide this field.
meta.inquiry_forms.{key}.fields[].optionsarray- For selection fields, display label and submit value.
meta.inquiry_forms.{key}.fields[].options[].labelstringmeta.inquiry_forms.{key}.fields[].options[].valuestring | numbermeta.inquiry_forms.{key}.fields[].placeholderstring | nullmeta.inquiry_forms.{key}.noticeobject | null- Instructions by language. Read notice.id or notice.en; null when absent.
meta.inquiry_forms.{key}.notice.idstringmeta.inquiry_forms.{key}.notice.enstringmeta.paginationobjectRequiredmeta.pagination.cursorstring | nullRequiredmeta.pagination.next_cursorstring | nullRequiredmeta.pagination.limitintegerRequiredmeta.pagination.has_morebooleanRequired
Smart prices by brand
/api/v1/h2h/smart-pricelists/{category_slug}Prices for your account tier. Choose an active smart_sku_code and use POST /smart-transaction. For one brand, pass brand_id from the matching brands list or use its slug in the path. Follow next_cursor until has_more=false.
Both headers: X-API-Key and X-Secret-Key
Parameters
| Name | Type / source | Required | Description |
|---|---|---|---|
| category_slug | string · path | Required | Brand slug from data[].slug in the matching brands endpoint. Existing category/type slugs remain supported. |
| limit | integer · query | Optional | Items per page, 1–1000. Use 500 for synchronisation. Default 500. min=1 max=1000 |
| cursor | string · query | Optional | Copy meta.pagination.next_cursor from the previous page. Keep the same filters while paging. |
| brandCompatibility | integer / string · query | Optional | Compatibility alias: brand ID or exact brand name. Prefer brand_id. |
| categoryCompatibility | integer / string · query | Optional | Compatibility filter for a root category ID or exact name. Prefer brand_id or a brand slug. |
| search | string · query | Optional | Search product name or Smart SKU. |
| brand_id | integer · query | Optional | Filter by data[].brand_id from the matching catalogue brands endpoint. Omit to fetch all brands. min=1 |
curl -X GET 'https://api.melostore.id/api/v1/h2h/smart-pricelists/example-brand?limit=500' \
-H 'Accept: application/json' \
-H "X-API-Key: $H2H_API_KEY" \
-H "X-Secret-Key: $H2H_SECRET_KEY"Request succeeded.
{
"success": true,
"data": [
{
"name": "Example product",
"category_name": "Example category",
"brand_id": 10,
"type_name": "Example type",
"category_order": 1,
"type_order": 1,
"status": "active",
"inquiry_form_key": "example-form",
"h2h_description": null,
"description": null,
"smart_sku_code": "smartEXAMPLE",
"min_price": 1000,
"max_price": 1000,
"variant_count": 1,
"variants": [
{
"sku_code": "EXAMPLE-10",
"price": 1000,
"status": "active"
}
]
}
],
"meta": {
"brands": [
{
"id": 10,
"name": "Example brand",
"thumbnail": null,
"order": 1
}
],
"inquiry_forms": {
"example-form": {
"fields": [
{
"key": "customer_target",
"name": "Player ID",
"type": "text"
},
{
"key": "customer_target_zone",
"name": "Zone",
"type": "text"
}
],
"notice": null
}
},
"pagination": {
"cursor": null,
"next_cursor": null,
"limit": 100,
"has_more": false
}
}
}successbooleanRequireddataarrayRequireddata[].namestringRequireddata[].category_namestringRequireddata[].brand_idinteger | nullRequired- Brand ID; full brand discovery is available from the matching categories endpoint.
data[].type_namestringRequireddata[].category_orderintegerRequireddata[].type_orderintegerRequireddata[].statusactive | out_of_stockRequired- Order only active products. out_of_stock means unavailable.
data[].inquiry_form_keystringRequired- Look up this key in meta.inquiry_forms on this same page.
data[].h2h_descriptionstring | nullRequireddata[].descriptionstring | nullRequireddata[].smart_sku_codestringRequireddata[].min_priceintegerRequireddata[].max_priceintegerRequireddata[].variant_countintegerRequireddata[].variantsarrayRequireddata[].variants[].sku_codestringRequireddata[].variants[].priceinteger | nullRequireddata[].variants[].statusstringRequiredmetaobjectRequiredmeta.brandsarrayRequired- Brand metadata for this page only. Use the categories endpoint for the full list.
meta.brands[].idintegerRequiredmeta.brands[].namestring | nullRequiredmeta.brands[].thumbnailstring | nullRequiredmeta.brands[].orderinteger | nullRequiredmeta.inquiry_formsobject / arrayRequired- Product input forms keyed by inquiry_form_key. An empty result may be {} or [].
meta.inquiry_forms.{key}.fieldsarraymeta.inquiry_forms.{key}.fields[].keystringmeta.inquiry_forms.{key}.fields[].namestringmeta.inquiry_forms.{key}.fields[].typestringmeta.inquiry_forms.{key}.fields[].requiredboolean- Whether the buyer must provide this field.
meta.inquiry_forms.{key}.fields[].optionsarray- For selection fields, display label and submit value.
meta.inquiry_forms.{key}.fields[].options[].labelstringmeta.inquiry_forms.{key}.fields[].options[].valuestring | numbermeta.inquiry_forms.{key}.fields[].placeholderstring | nullmeta.inquiry_forms.{key}.noticeobject | null- Instructions by language. Read notice.id or notice.en; null when absent.
meta.inquiry_forms.{key}.notice.idstringmeta.inquiry_forms.{key}.notice.enstringmeta.paginationobjectRequiredmeta.pagination.cursorstring | nullRequiredmeta.pagination.next_cursorstring | nullRequiredmeta.pagination.limitintegerRequiredmeta.pagination.has_morebooleanRequired
Regular catalogue brands
/api/v1/h2h/pricelists/categoriesBrands represented in the regular catalogue for your tier. Each record contains brand_id, name, slug, its parent category, and direct child product types in type[]. Sorted by parent category ID ascending, then brand name A-Z; brands without a parent category appear last. Use brand_id in the catalogue query or slug in /pricelists/{category_slug}. A listed brand or type does not guarantee an active SKU; check the matching pricelist.
Both headers: X-API-Key and X-Secret-Key
Parameters
No parameters.
curl -X GET 'https://api.melostore.id/api/v1/h2h/pricelists/categories' \
-H 'Accept: application/json' \
-H "X-API-Key: $H2H_API_KEY" \
-H "X-Secret-Key: $H2H_SECRET_KEY"Request succeeded.
{
"success": true,
"data": [
{
"brand_id": 10,
"name": "Example brand",
"slug": "example-brand",
"category": {
"id": 1,
"name": "Games",
"slug": "games"
},
"type": [
{
"id": 11,
"name": "Diamonds",
"slug": "example-diamonds"
},
{
"id": 12,
"name": "Passes",
"slug": "example-passes"
}
]
}
]
}successbooleanRequireddataarrayRequireddata[].brand_idintegerRequired- Brand identifier to use in the brand_id catalogue filter. Replaces the former id field in this directory.
data[].namestringRequireddata[].slugstringRequired- Brand slug for the matching /pricelists/{category_slug} or /smart-pricelists/{category_slug} path.
data[].categoryobject | nullRequired- Parent category of this brand, or null when no parent category is assigned.
data[].category.idintegerRequireddata[].category.namestringRequireddata[].category.slugstring | nullRequireddata[].typearrayRequired- All direct product-type children of this brand, sorted by name A-Z. Empty when the brand has no types. A type entry does not guarantee an available SKU; check the matching pricelist.
data[].type[].idintegerRequireddata[].type[].namestringRequireddata[].type[].slugstring | nullRequired
Smart catalogue brands
/api/v1/h2h/smart-pricelists/categoriesBrands represented in the Smart catalogue for your tier. Each record contains brand_id, name, slug, its parent category, and direct child product types in type[]. Sorted by parent category ID ascending, then brand name A-Z; brands without a parent category appear last. Use brand_id in the catalogue query or slug in /smart-pricelists/{category_slug}. A listed brand or type does not guarantee an active SKU; check the matching pricelist.
Both headers: X-API-Key and X-Secret-Key
Parameters
No parameters.
curl -X GET 'https://api.melostore.id/api/v1/h2h/smart-pricelists/categories' \
-H 'Accept: application/json' \
-H "X-API-Key: $H2H_API_KEY" \
-H "X-Secret-Key: $H2H_SECRET_KEY"Request succeeded.
{
"success": true,
"data": [
{
"brand_id": 10,
"name": "Example brand",
"slug": "example-brand",
"category": {
"id": 1,
"name": "Games",
"slug": "games"
},
"type": [
{
"id": 11,
"name": "Diamonds",
"slug": "example-diamonds"
},
{
"id": 12,
"name": "Passes",
"slug": "example-passes"
}
]
}
]
}successbooleanRequireddataarrayRequireddata[].brand_idintegerRequired- Brand identifier to use in the brand_id catalogue filter. Replaces the former id field in this directory.
data[].namestringRequireddata[].slugstringRequired- Brand slug for the matching /pricelists/{category_slug} or /smart-pricelists/{category_slug} path.
data[].categoryobject | nullRequired- Parent category of this brand, or null when no parent category is assigned.
data[].category.idintegerRequireddata[].category.namestringRequireddata[].category.slugstring | nullRequireddata[].typearrayRequired- All direct product-type children of this brand, sorted by name A-Z. Empty when the brand has no types. A type entry does not guarantee an available SKU; check the matching pricelist.
data[].type[].idintegerRequireddata[].type[].namestringRequireddata[].type[].slugstring | nullRequired
Failed requests and safe retries
Timeout, HTTP 500, or a disconnected request: keep buyer_trx_id and check status using that reference. If needed, retry the same request with the same reference. Do not immediately create a new reference because the first order may have been accepted.
HTTP 401: check both credential headers. HTTP 403: check account access and the applicable IP whitelist. HTTP 422: inspect errors and parameters; MAX_PRICE_TOO_LOW requires a limit covering the catalogue price. HTTP 429: wait Retry-After before retrying.
HTTP 201 only acknowledges receipt. pending/processing means in progress; success means delivered; failed means unsuccessful and deducted funds are refunded. Replace an order only after its previous result is final.
PROVIDER_PRICE_CHANGED: after a failed result, fetch fresh prices before creating a new order. HTTP 409 on bulk may indicate a changed payload using an existing reference, or an unavailable invoice; read the endpoint message.
Need Action & Two-Factor Authentication
Customer actions are independent of transaction status. Regular and Smart orders remain processing during verification: required → submitted → resolved. need_action=true only in required; show a code form only when action.can_submit=true.
When required, collect the buyer verification code and submit POST /api/v1/h2h/transactions/2fa with buyer_trx_id and code. Include the latest action_id and action_version to prevent submission to an outdated request. Code is a trimmed 1–128 character string without control characters; it is not restricted to six digits.
HTTP 202 means receipt for manual admin handling, not proof that the code is valid. Hide the form in submitted and show awaiting admin. Hide the flag in resolved. Admin can request another code: state returns to required with a higher version. Collect a new code from the buyer then.
Use the same authentication headers and IP whitelist. Limit: 5 requests/minute per owner and transaction. Identical-code retries return the previous receipt without another notification; a different code returns 409. Stale versions, final orders, and unavailable actions return 409. A buyer reference belonging to another profile returns 404.
A transaction.action_change webhook can arrive before the final result. Verify X-H2H-Signature on the raw body using the Webhook Secret. Deduplicate by event_id and version, and ignore older action.version values. Store actions separately: action events must never finalize orders, change balances, or trigger refunds. Final result webhooks also include the latest action snapshot.
Status, refunds, and references
HTTP 201/200 means a request was accepted or its status was read. Check data.status for the order result: pending/processing are still in progress, success is delivered, and failed is unsuccessful. Balance deducted for failed orders is refunded. serial_number may be empty until a result is available; account-check details may use null. Read error_code/message for failure details.
Regular orders optionally accept max_price as a positive integer in MC/IDR. It must cover the current price for your tier; otherwise the request returns HTTP 422 with MAX_PRICE_TOO_LOW before any debit. An equal limit is allowed. Omit/null to use automatic pricing.
When max_price is supplied, your balance must cover the full limit, which is held upfront. A successful final charge never exceeds max_price. Unused hold is returned on success; a failed order receives a full refund.
Example: max_price of 1,100 MC holds 1,100 MC upfront. If the final charge is 1,000 MC, 100 MC is returned. These numbers are illustrative; choose your limit using your account catalogue price.
PROVIDER_PRICE_CHANGED means the price changed and the order could not be completed. Once the previous order is failed, fetch current prices and use a new buyer_trx_id to order again. Reusing the failed reference returns the original result.
Regular and Smart orders require buyer_trx_id as your unique order reference. If a request times out, query GET /transaction/{id} using buyer_trx_id or repeat the same request with that reference. A previously used reference returns the original transaction. Use a new reference for a new order. Smart holds max_bid and refunds the difference once the final price is known.
Store the ordered SKU with id and buyer_trx_id. Match status responses and callbacks using id/buyer_trx_id because sku_code in Smart results may differ from the SKU submitted at creation. fulfilled_sku_code identifies the successfully fulfilled SKU. Paid account-check transaction details have null buyer_trx_id and sku_code.
While status is pending/processing, wait for the final result through a callback or status lookup; do not create a replacement for the same request. Transaction responses use price, while callbacks use price_charged. The initial Smart price may show max_bid, and a capped regular order shows its max_price hold before the final price is available. price_charged on a failed order does not represent the final cost after refund; check your balance and invoice for reconciliation.
Create regular order
/api/v1/h2h/transactionCreate one regular order using an exact regular sku_code. HTTP 201 acknowledges receipt; read data.status for fulfillment. Repeat the same request with the same buyer_trx_id if the response is uncertain. Use a new reference for a new order. Optional max_price sets your maximum final charge and the full limit is held upfront. Unused hold is refunded on success; failed orders receive a full refund.
Both headers: X-API-Key and X-Secret-Key
Parameters
| Name | Type / source | Required | Description |
|---|---|---|---|
| sku_code | string · body | Required | Exact sku_code from the regular catalogue. |
| customer_target | string · body | Required | Customer ID, phone number or email according to the first catalogue inquiry field. Keep it as a string. maxLength=255 |
| customer_target_zone | string | null · body | Optional | Required only when the product/game form requires a second zone field. Use its option value, not its label. maxLength=100 |
| additional_data | object | null · body | Optional | Other product form values, keyed by the field key in meta.inquiry_forms. Omit when not required. |
| buyer_trx_id | string · body | Required | Your unique order reference. Keep it for retries of the same order; use a new reference for each new order. maxLength=64 |
| sandbox_mode | boolean | null · body | Optional | true simulates an order without charging your wallet. false uses live mode. Omit/null uses your account setting. |
| max_price | integer | null · body | Optional | Optional maximum final charge in MC/IDR. Must cover the current price for your tier. Your balance must cover the whole limit, which is held upfront. Unused hold is returned on success; failed orders receive a full refund. Omit/null for automatic pricing. min=1 max=99999999999 |
curl -X POST 'https://api.melostore.id/api/v1/h2h/transaction' \
-H 'Accept: application/json' \
-H "X-API-Key: $H2H_API_KEY" \
-H "X-Secret-Key: $H2H_SECRET_KEY" \
-H 'Content-Type: application/json' \
--data '{
"sku_code": "EXAMPLE-10",
"customer_target": "123456789",
"customer_target_zone": "1234",
"buyer_trx_id": "example-order-001",
"sandbox_mode": true,
"additional_data": {
"email": "[email protected]"
}
}'Request accepted. Read data.status for the order result.
{
"success": true,
"message": "Transaction successfully created and queued.",
"data": {
"id": "11111111-1111-4111-8111-111111111111",
"buyer_trx_id": "example-order-001",
"sku_code": "EXAMPLE-10",
"product_name": "Example product",
"customer_target": "123456789",
"customer_target_zone": "1234",
"price": 1000,
"status": "processing",
"serial_number": "",
"webhook_delivery_status": "pending",
"is_sandbox": true,
"created_at": "2026-10-02T03:00:00+00:00"
}
}successbooleanRequireddataobjectRequireddata.idstringRequireddata.buyer_trx_idstring | nullRequired- Buyer reference for product orders; null for paid account-check transactions.
data.sku_codestring | nullRequired- Product SKU in the transaction result. For Smart, it may differ from the ordered SKU; match by id/buyer_trx_id and use fulfilled_sku_code for successful fulfillment. May be null, including on paid account checks.
data.product_namestring | nulldata.customer_targetstringRequireddata.customer_target_zonestring | nullRequireddata.pricenumberRequired- MC/IDR. While processing, Smart shows max_bid and a capped regular order shows its max_price hold. On success this is the final charge; a failed order is refunded.
data.statuspending | processing | success | failed | refundedRequireddata.serial_numberstring | nullRequireddata.webhook_delivery_statusstringRequireddata.is_sandboxbooleanRequireddata.created_atstringRequireddata.error_codestring | nulldata.messagestring | nulldata.max_priceinteger- Present only on regular orders submitted with max_price. Original hold and maximum final charge in MC/IDR.
data.need_actionboolean- True only when customer action is required. Independent of transaction status.
data.actionobject / null- Current customer action. Submit only when can_submit=true; compare version before applying updates.
data.action.idstringdata.action.typetwo_factordata.action.staterequired | submitted | resolveddata.action.versionintegerdata.action.messagestringdata.action.can_submitbooleandata.action.updated_atstring
Create Smart Order
/api/v1/h2h/smart-transactionCreate one Smart order using smart_sku_code as sku_code and max_bid as your budget. The whole budget is held upfront. The successful final charge never exceeds max_bid; unused hold is returned. Failed orders receive a full refund. For PROVIDER_PRICE_CHANGED, refresh the catalogue and only place a new order after the previous order is final.
Both headers: X-API-Key and X-Secret-Key
Parameters
| Name | Type / source | Required | Description |
|---|---|---|---|
| sku_code | string · body | Required | Use the smart_sku_code value from the Smart catalogue. maxLength=100 |
| customer_target | string · body | Required | Customer ID, phone number or email according to the first catalogue inquiry field. Keep it as a string. maxLength=255 |
| customer_target_zone | string | null · body | Optional | Required only when the product/game form requires a second zone field. Use its option value, not its label. maxLength=100 |
| additional_data | object | null · body | Optional | Other product form values, keyed by the field key in meta.inquiry_forms. Omit when not required. |
| buyer_trx_id | string · body | Required | Your unique order reference. Keep it for retries of the same order; use a new reference for each new order. maxLength=64 |
| sandbox_mode | boolean | null · body | Optional | true simulates an order without charging your wallet. false uses live mode. Omit/null uses your account setting. |
| max_bid | integer · body | Required | Maximum Smart order budget in MC/IDR. The whole amount is held upfront; unused hold is returned. min=1 |
curl -X POST 'https://api.melostore.id/api/v1/h2h/smart-transaction' \
-H 'Accept: application/json' \
-H "X-API-Key: $H2H_API_KEY" \
-H "X-Secret-Key: $H2H_SECRET_KEY" \
-H 'Content-Type: application/json' \
--data '{
"sku_code": "smartEXAMPLE",
"customer_target": "123456789",
"customer_target_zone": "1234",
"buyer_trx_id": "example-smart-order-001",
"sandbox_mode": true,
"additional_data": {
"email": "[email protected]"
},
"max_bid": 1100
}'Request succeeded.
{
"success": true,
"message": "Transaction successfully created and queued.",
"data": {
"id": "11111111-1111-4111-8111-111111111111",
"buyer_trx_id": "example-smart-order-001",
"sku_code": "smartEXAMPLE",
"product_name": "Example product",
"customer_target": "123456789",
"customer_target_zone": "1234",
"price": 1100,
"status": "processing",
"serial_number": "",
"webhook_delivery_status": "pending",
"is_sandbox": true,
"created_at": "2026-10-02T03:00:00+00:00",
"max_bid": 1100
}
}successbooleanRequireddataobjectRequireddata.idstringRequireddata.buyer_trx_idstringRequireddata.sku_codestringRequired- Smart order results may use a different sku_code from creation. Store the ordered SKU and match by id/buyer_trx_id; use fulfilled_sku_code for successful fulfillment.
data.product_namestring | nullRequireddata.customer_targetstringRequireddata.customer_target_zonestring | nullRequireddata.pricenumberRequireddata.statusstringRequireddata.serial_numberstringRequireddata.webhook_delivery_statusstringRequireddata.is_sandboxbooleanRequireddata.created_atstringRequireddata.max_bidinteger | nullRequireddata.error_codestring | nulldata.messagestring | null
Transaction status
/api/v1/h2h/transaction/{id}Look up your transaction by UUID or buyer_trx_id. Supports regular, Smart and individual bulk orders; use billing.transaction_id for paid account checks. Match Smart results using id/buyer_trx_id, as sku_code may differ from the ordered SKU. fulfilled_sku_code identifies the successfully fulfilled SKU. Account-check buyer_trx_id, sku_code and serial_number are null. Smart/bulk fields appear only for their order type.
Both headers: X-API-Key and X-Secret-Key
Parameters
| Name | Type / source | Required | Description |
|---|---|---|---|
| id | string · path | Required | Owned transaction UUID or buyer_trx_id |
curl -X GET 'https://api.melostore.id/api/v1/h2h/transaction/11111111-1111-4111-8111-111111111111' \
-H 'Accept: application/json' \
-H "X-API-Key: $H2H_API_KEY" \
-H "X-Secret-Key: $H2H_SECRET_KEY"Order details. Read data.status for the current result.
{
"success": true,
"data": {
"id": "11111111-1111-4111-8111-111111111111",
"buyer_trx_id": "example-order-001",
"sku_code": "EXAMPLE-10",
"product_name": "Example product",
"customer_target": "123456789",
"customer_target_zone": "1234",
"price": 1000,
"status": "processing",
"serial_number": "",
"webhook_delivery_status": "pending",
"is_sandbox": true,
"created_at": "2026-10-02T03:00:00+00:00"
}
}successbooleanRequireddataobject / objectRequireddata.idstringRequireddata.buyer_trx_idstring | nullRequired- Buyer reference for product orders; null for paid account-check transactions.
data.sku_codestring | nullRequired- Product SKU in the transaction result. For Smart, it may differ from the ordered SKU; match by id/buyer_trx_id and use fulfilled_sku_code for successful fulfillment. May be null, including on paid account checks.
data.product_namestring | nulldata.customer_targetstringRequireddata.customer_target_zonestring | nullRequireddata.pricenumberRequired- MC/IDR. While processing, Smart shows max_bid and a capped regular order shows its max_price hold. On success this is the final charge; a failed order is refunded.
data.statuspending | processing | success | failed | refundedRequireddata.serial_numberstring | nullRequireddata.webhook_delivery_statusstringRequireddata.is_sandboxbooleanRequireddata.created_atstringRequireddata.error_codestring | nulldata.messagestring | nulldata.max_priceinteger- Present only on regular orders submitted with max_price. Original hold and maximum final charge in MC/IDR.
data.need_actionboolean- True only when customer action is required. Independent of transaction status.
data.actionobject / null- Current customer action. Submit only when can_submit=true; compare version before applying updates.
data.action.idstringdata.action.typetwo_factordata.action.staterequired | submitted | resolveddata.action.versionintegerdata.action.messagestringdata.action.can_submitbooleandata.action.updated_atstringdata.max_bidinteger | nulldata.fulfilled_sku_codestring | nulldata.attempts_countinteger
Submit a two-factor verification code
/api/v1/h2h/transactions/2faSubmit a requested code for manual admin handling, for regular or Smart orders. Requires X-API-Key and X-Secret-Key and the existing IP whitelist. Limit: 5 requests/minute per owner and transaction. HTTP 202 confirms receipt, not validity. Trim code to 1–128 characters without control characters. Retry the same code if delivery is uncertain; a different code is rejected until a new request. This endpoint never creates an order or changes transaction/financial status.
Both headers: X-API-Key and X-Secret-Key
Parameters
| Name | Type / source | Required | Description |
|---|---|---|---|
| buyer_trx_id | string · body | Required | buyer_trx_id of the existing order that requests this code. Use the same order reference. maxLength=255 |
| code | string · body | Required | Latest verification code from the buyer, trimmed, 1–128 characters without control characters. maxLength=128 |
| action_id | string · body | Optional | Current action.id from this order status or action webhook. Recommended to avoid stale submissions. |
| action_version | integer · body | Optional | Current action.version from the same order. Send as a JSON integer. min=1 |
curl -X POST 'https://api.melostore.id/api/v1/h2h/transactions/2fa' \
-H 'Accept: application/json' \
-H "X-API-Key: $H2H_API_KEY" \
-H "X-Secret-Key: $H2H_SECRET_KEY" \
-H 'Content-Type: application/json' \
--data '{
"buyer_trx_id": "example-order-001",
"code": "fictional-alpha-91",
"action_id": "11111111-1111-4111-8111-111111111111",
"action_version": 1
}'Code accepted for admin handling.
{
"success": true,
"message": "Your verification code has been received and is awaiting admin handling.",
"data": {
"need_action": false,
"action": {
"id": "11111111-1111-4111-8111-111111111111",
"type": "two_factor",
"state": "submitted",
"version": 2,
"message": "Your verification code has been received and is awaiting admin handling.",
"can_submit": false,
"updated_at": "2026-10-10T08:02:00+07:00"
}
}
}successbooleanRequiredmessagestringRequireddataobjectRequireddata.need_actionbooleanRequired- True only when customer action is required. Independent of transaction status.
data.actionobject / nullRequired- Current customer action. Submit only when can_submit=true; compare version before applying updates.
data.action.idstringdata.action.typetwo_factordata.action.staterequired | submitted | resolveddata.action.versionintegerdata.action.messagestringdata.action.can_submitbooleandata.action.updated_atstring
Bulk order guide
A bulk contains one regular SKU, one target, and 1–100 orders. The price is locked when the order is accepted. Each order has its own reference and result. The full bulk cost is deducted upfront. Ensure your balance covers the whole bulk; insufficient balance prevents its creation.
Use order_count for the number of bulk orders. Omit quantity, max_bid, and order_mode from bulk requests; populated values for these parameters are rejected with HTTP 422.
Use a unique buyer_bulk_id for each bulk and save it before sending the request. The first request returns HTTP 201. Repeating the same request with the same reference returns HTTP 200 without another balance deduction. A different request with the same reference returns HTTP 409. Use a new reference for a new bulk.
Orders within a bulk are processed in sequence. Follow each result in the bulk details; failed orders are refunded. To reorder, use a new transaction reference after the previous result is final. Bulk is available only for the regular catalogue with one SKU and one target; pausing and cancellation are not available.
Read success_count and failed_count for finished orders, and remaining_count for unfinished orders. processing_count counts orders in progress and queued_count counts waiting orders. Bulk statuses are queued, processing, completed, partially_failed, or failed. Use GET /bulk-orders/{id} for the progress summary; callbacks are sent per transaction.
Download the invoice PDF after all orders have finished. HTTP 409 means the invoice is not yet available or the bulk uses sandbox. A live bulk has one invoice summarising costs and refunds, including a full refund and zero final cost if all orders fail. The invoice is emailed after completion. Each order is also available in the Transactions menu. Sandbox does not produce invoices or invoice emails.
| Aspect | Regular transaction | Bulk order |
|---|---|---|
| Transactions | One | order_count child transactions |
| Buyer reference | buyer_trx_id | buyer_bulk_id + unique child references |
| Balance | One transaction debit | All children debited upfront |
| Processing | One async transaction | Sequential within a bulk |
| History | Transaction detail | Progress summary and child detail |
| Bulk invoice/email | Not applicable | One document and final email |
Create bulk order
/api/v1/h2h/bulk-ordersCreates 1–100 separate orders for one regular SKU and one target. The full cost is deducted upfront; insufficient balance prevents creation. Use a unique buyer_bulk_id for each bulk and keep it unchanged on retries. Set order_count and omit quantity, max_bid, and order_mode; populated values for these parameters return HTTP 422.
Both headers: X-API-Key and X-Secret-Key
Parameters
| Name | Type / source | Required | Description |
|---|---|---|---|
| sku_code | string · body | Required | Exact sku_code from the regular catalogue. maxLength=100 |
| customer_target | string · body | Required | Customer ID, phone number or email according to the first catalogue inquiry field. Keep it as a string. maxLength=255 |
| customer_target_zone | string | null · body | Optional | Required only when the product/game form requires a second zone field. Use its option value, not its label. maxLength=100 |
| additional_data | object | null · body | Optional | Other product form values, keyed by the field key in meta.inquiry_forms. Omit when not required. |
| sandbox_mode | boolean | null · body | Optional | true simulates an order without charging your wallet. false uses live mode. Omit/null uses your account setting. |
| buyer_bulk_id | string · body | Required | Your unique bulk reference. Identical retries return the original bulk; a changed payload returns HTTP 409. maxLength=64 |
| order_count | integer · body | Required | Number of separate orders, 1–100, processed sequentially for one regular SKU and target. min=1 max=100 |
curl -X POST 'https://api.melostore.id/api/v1/h2h/bulk-orders' \
-H 'Accept: application/json' \
-H "X-API-Key: $H2H_API_KEY" \
-H "X-Secret-Key: $H2H_SECRET_KEY" \
-H 'Content-Type: application/json' \
--data '{
"sku_code": "EXAMPLE-10",
"customer_target": "123456789",
"customer_target_zone": "1234",
"sandbox_mode": true,
"additional_data": {
"email": "[email protected]"
},
"order_count": 3,
"buyer_bulk_id": "example-bulk-001"
}'Request accepted. Read data.status for the order result.
{
"success": true,
"message": "Bulk order created and queued.",
"data": {
"id": "22222222-2222-4222-8222-222222222222",
"buyer_bulk_id": "example-bulk-001",
"sku_code": "EXAMPLE-10",
"product_name": "Example product",
"customer_target": "123456789",
"customer_target_zone": "1234",
"order_count": 3,
"unit_price": 1000,
"total_debit": 0,
"status": "queued",
"via": "api",
"is_sandbox": true,
"invoice_available": false,
"created_at": "2026-10-02T03:00:00+00:00",
"completed_at": null,
"total_count": 3,
"success_count": 0,
"failed_count": 0,
"processed_count": 0,
"remaining_count": 3,
"processing_count": 0,
"queued_count": 3
}
}successbooleanRequiredmessagestringRequireddataobjectRequireddata.idstringRequireddata.buyer_bulk_idstringRequireddata.sku_codestringRequireddata.product_namestringRequireddata.customer_targetstringRequireddata.customer_target_zonestring | nullRequireddata.order_countintegerRequireddata.unit_pricenumberRequireddata.total_debitnumberRequireddata.statusqueued | processing | completed | partially_failed | failedRequireddata.viastringRequireddata.is_sandboxbooleanRequireddata.invoice_availablebooleanRequireddata.created_atstringRequireddata.completed_atstring | nullRequireddata.total_countintegerRequireddata.success_countintegerRequireddata.failed_countintegerRequireddata.processed_countintegerRequireddata.remaining_countintegerRequireddata.processing_countintegerRequireddata.queued_countintegerRequired
Bulk history
/api/v1/h2h/bulk-ordersOwned orders, 15 per page. Date filters use Asia/Jakarta inclusive start/end dates.
Both headers: X-API-Key and X-Secret-Key
Parameters
| Name | Type / source | Required | Description |
|---|---|---|---|
| page | integer · query | Optional | |
| status | queued | processing | completed | partially_failed | failed · query | Optional | |
| search | string · query | Optional | Buyer reference, target, SKU or product name |
| start_date | string · query | Optional | YYYY-MM-DD |
| end_date | string · query | Optional | YYYY-MM-DD, not earlier than start_date |
curl -X GET 'https://api.melostore.id/api/v1/h2h/bulk-orders?page=1' \
-H 'Accept: application/json' \
-H "X-API-Key: $H2H_API_KEY" \
-H "X-Secret-Key: $H2H_SECRET_KEY"Bulk order data and progress.
{
"success": true,
"data": {
"current_page": 1,
"data": [
{
"id": "22222222-2222-4222-8222-222222222222",
"buyer_bulk_id": "example-bulk-001",
"sku_code": "EXAMPLE-10",
"product_name": "Example product",
"customer_target": "123456789",
"customer_target_zone": "1234",
"order_count": 3,
"unit_price": 1000,
"total_debit": 0,
"status": "processing",
"via": "api",
"is_sandbox": true,
"invoice_available": false,
"created_at": "2026-10-02T03:00:00+00:00",
"completed_at": null,
"total_count": 3,
"success_count": 1,
"failed_count": 1,
"processed_count": 2,
"remaining_count": 1,
"processing_count": 1,
"queued_count": 0
}
],
"first_page_url": "https://api.melostore.id/api/v1/h2h/bulk-orders?page=1",
"from": 1,
"last_page": 1,
"last_page_url": "https://api.melostore.id/api/v1/h2h/bulk-orders?page=1",
"links": [
{
"url": null,
"label": "Previous",
"active": false
}
],
"next_page_url": null,
"path": "https://api.melostore.id/api/v1/h2h/bulk-orders",
"per_page": 15,
"prev_page_url": null,
"to": 1,
"total": 1
}
}successbooleanRequireddataobjectRequireddata.current_pageintegerRequireddata.dataarrayRequireddata.data[].idstringRequireddata.data[].buyer_bulk_idstringRequireddata.data[].sku_codestringRequireddata.data[].product_namestringRequireddata.data[].customer_targetstringRequireddata.data[].customer_target_zonestring | nullRequireddata.data[].order_countintegerRequireddata.data[].unit_pricenumberRequireddata.data[].total_debitnumberRequireddata.data[].statusqueued | processing | completed | partially_failed | failedRequireddata.data[].viastringRequireddata.data[].is_sandboxbooleanRequireddata.data[].invoice_availablebooleanRequireddata.data[].created_atstringRequireddata.data[].completed_atstring | nullRequireddata.data[].total_countintegerRequireddata.data[].success_countintegerRequireddata.data[].failed_countintegerRequireddata.data[].processed_countintegerRequireddata.data[].remaining_countintegerRequireddata.data[].processing_countintegerRequireddata.data[].queued_countintegerRequireddata.first_page_urlstringRequireddata.frominteger | nullRequired- Item position, or null when this page contains no items.
data.last_pageintegerRequireddata.last_page_urlstringRequireddata.linksarrayRequireddata.links[].urlstring | nullRequireddata.links[].labelstringRequireddata.links[].activebooleanRequireddata.next_page_urlstring | nullRequireddata.pathstringRequireddata.per_pageintegerRequireddata.prev_page_urlstring | nullRequireddata.tointeger | nullRequired- Item position, or null when this page contains no items.
data.totalintegerRequired
Bulk progress
/api/v1/h2h/bulk-orders/{id}Read the bulk status and order counts. processed_count includes successful and failed orders; remaining_count includes orders still waiting or processing.
Both headers: X-API-Key and X-Secret-Key
Parameters
| Name | Type / source | Required | Description |
|---|---|---|---|
| id | string · path | Required | Bulk UUID returned by POST /bulk-orders. Use the bulk ID, not buyer_bulk_id or a child transaction ID. |
curl -X GET 'https://api.melostore.id/api/v1/h2h/bulk-orders/22222222-2222-4222-8222-222222222222' \
-H 'Accept: application/json' \
-H "X-API-Key: $H2H_API_KEY" \
-H "X-Secret-Key: $H2H_SECRET_KEY"Bulk order data and progress.
{
"success": true,
"data": {
"id": "22222222-2222-4222-8222-222222222222",
"buyer_bulk_id": "example-bulk-001",
"sku_code": "EXAMPLE-10",
"product_name": "Example product",
"customer_target": "123456789",
"customer_target_zone": "1234",
"order_count": 3,
"unit_price": 1000,
"total_debit": 0,
"status": "processing",
"via": "api",
"is_sandbox": true,
"invoice_available": false,
"created_at": "2026-10-02T03:00:00+00:00",
"completed_at": null,
"total_count": 3,
"success_count": 1,
"failed_count": 1,
"processed_count": 2,
"remaining_count": 1,
"processing_count": 1,
"queued_count": 0
}
}successbooleanRequireddataobjectRequireddata.idstringRequireddata.buyer_bulk_idstringRequireddata.sku_codestringRequireddata.product_namestringRequireddata.customer_targetstringRequireddata.customer_target_zonestring | nullRequireddata.order_countintegerRequireddata.unit_pricenumberRequireddata.total_debitnumberRequireddata.statusqueued | processing | completed | partially_failed | failedRequireddata.viastringRequireddata.is_sandboxbooleanRequireddata.invoice_availablebooleanRequireddata.created_atstringRequireddata.completed_atstring | nullRequireddata.total_countintegerRequireddata.success_countintegerRequireddata.failed_countintegerRequireddata.processed_countintegerRequireddata.remaining_countintegerRequireddata.processing_countintegerRequireddata.queued_countintegerRequired
Bulk child transactions
/api/v1/h2h/bulk-orders/{id}/transactionsSequence-ordered child orders, 15 per page. Each has its own transaction UUID and buyer_trx_id.
Both headers: X-API-Key and X-Secret-Key
Parameters
| Name | Type / source | Required | Description |
|---|---|---|---|
| id | string · path | Required | Bulk UUID returned by POST /bulk-orders. Use the bulk ID, not buyer_bulk_id or a child transaction ID. |
| page | integer · query | Optional |
curl -X GET 'https://api.melostore.id/api/v1/h2h/bulk-orders/22222222-2222-4222-8222-222222222222/transactions?page=1' \
-H 'Accept: application/json' \
-H "X-API-Key: $H2H_API_KEY" \
-H "X-Secret-Key: $H2H_SECRET_KEY"Bulk order data and progress.
{
"success": true,
"data": {
"current_page": 1,
"data": [
{
"id": "11111111-1111-4111-8111-111111111111",
"buyer_trx_id": "BULK-22222222-2222-4222-8222-222222222222-1",
"bulk_order_id": "22222222-2222-4222-8222-222222222222",
"buyer_bulk_id": "example-bulk-001",
"bulk_sequence": 1,
"sku_code": "EXAMPLE-10",
"customer_target": "123456789",
"customer_target_zone": "1234",
"price": 1000,
"status": "success",
"serial_number": "EXAMPLE-SN-001",
"error_code": null,
"message": null,
"created_at": "2026-10-02T03:00:00+00:00",
"updated_at": "2026-10-02T03:00:00+00:00"
}
],
"first_page_url": "https://api.melostore.id/api/v1/h2h/bulk-orders/22222222-2222-4222-8222-222222222222/transactions?page=1",
"from": 1,
"last_page": 1,
"last_page_url": "https://api.melostore.id/api/v1/h2h/bulk-orders/22222222-2222-4222-8222-222222222222/transactions?page=1",
"links": [
{
"url": null,
"label": "Previous",
"active": false
}
],
"next_page_url": null,
"path": "https://api.melostore.id/api/v1/h2h/bulk-orders/22222222-2222-4222-8222-222222222222/transactions",
"per_page": 15,
"prev_page_url": null,
"to": 1,
"total": 1
}
}successbooleanRequireddataobjectRequireddata.current_pageintegerRequireddata.dataarrayRequireddata.data[].idstringRequireddata.data[].buyer_trx_idstringRequireddata.data[].bulk_order_idstringRequireddata.data[].buyer_bulk_idstringRequireddata.data[].bulk_sequenceintegerRequireddata.data[].sku_codestring | nullRequireddata.data[].customer_targetstringRequireddata.data[].customer_target_zonestring | nullRequireddata.data[].pricenumberRequireddata.data[].statuspending | processing | success | failed | refundedRequireddata.data[].serial_numberstringRequireddata.data[].error_codestring | nullRequireddata.data[].messagestring | nullRequireddata.data[].created_atstringRequireddata.data[].updated_atstringRequireddata.first_page_urlstringRequireddata.frominteger | nullRequired- Item position, or null when this page contains no items.
data.last_pageintegerRequireddata.last_page_urlstringRequireddata.linksarrayRequireddata.links[].urlstring | nullRequireddata.links[].labelstringRequireddata.links[].activebooleanRequireddata.next_page_urlstring | nullRequireddata.pathstringRequireddata.per_pageintegerRequireddata.prev_page_urlstring | nullRequireddata.tointeger | nullRequired- Item position, or null when this page contains no items.
data.totalintegerRequired
Download final bulk invoice
/api/v1/h2h/bulk-orders/{id}/invoiceDownload one PDF summarising the completed bulk's costs and refunds. HTTP 409 means the invoice is not yet available or the bulk uses sandbox. A live bulk where all orders fail shows a full refund and zero final cost.
Both headers: X-API-Key and X-Secret-Key
Parameters
| Name | Type / source | Required | Description |
|---|---|---|---|
| id | string · path | Required | Bulk UUID returned by POST /bulk-orders. Use the bulk ID, not buyer_bulk_id or a child transaction ID. |
curl -X GET 'https://api.melostore.id/api/v1/h2h/bulk-orders/22222222-2222-4222-8222-222222222222/invoice' \
-H 'Accept: application/json' \
-H "X-API-Key: $H2H_API_KEY" \
-H "X-Secret-Key: $H2H_SECRET_KEY" \
--fail-with-body --output bulk-invoice.pdfFinal PDF attachment, stable number H2H-BULK-{UUID}
Content-Type: application/pdf · Content-Disposition: attachment
Account-check billing
See GET /check-nickname/plan for the free allowance and current rates. Nickname and purchase-limit checks share the allowance. Found and not-found results use free allowance or incur a charge when paid checks are enabled on a live account. Every repeated check still uses allowance or incurs a charge.
Found and not-found results include billing.charged_mc, free_remaining_today, and transaction_id when charged. Uncertain verification is not treated as account-not-found. HTTP 402 is insufficient check balance; 429 quota; 404 not found; 422 validation; 502 unavailable. Sandbox account mode uses free allowance only.
Use rates from the latest plan response before offering paid checks. If rates are unavailable, retry before continuing. Example response numbers are illustrative; current rates appear in the account-check plan section.
…
Zone references are a snapshot. Check zone requirements through the games endpoint and use the correct game-specific zone code.
Zone code reference
Choose a game to see the codes used in customer_target_zone. Search by zone name or code.
Supported accounts/games
/api/v1/h2h/check-nickname/gamesPublic list and zone requirements. Zone reference in these docs is a snapshot.
Public, no authentication
Parameters
No parameters.
curl -X GET 'https://api.melostore.id/api/v1/h2h/check-nickname/games' \
-H 'Accept: application/json'Request succeeded.
{
"success": true,
"games": [
{
"game_code": "mobile-legends",
"name": "Mobile Legends",
"type": "game",
"zone_required": true,
"zone_values": null,
"params": [
{
"name": "customer_target"
},
{
"name": "customer_target_zone"
}
],
"status": "available"
}
]
}successbooleanRequiredgamesarrayRequiredgames[].game_codestringRequiredgames[].namestringRequiredgames[].typestringRequiredgames[].zone_requiredbooleanRequiredgames[].zone_valuesstring | nullRequiredgames[].paramsarrayRequiredgames[].params[].namestringRequiredgames[].statusstringRequired
Account-check plan
/api/v1/h2h/check-nickname/planPublic current allowance/rates. Values in examples are fictional snapshots, not an active quote.
Public, no authentication
Parameters
No parameters.
curl -X GET 'https://api.melostore.id/api/v1/h2h/check-nickname/plan' \
-H 'Accept: application/json'Request succeeded.
{
"success": true,
"data": {
"free_daily_limit": 40,
"free_minute_limit": 20,
"paid_price_mc": 1,
"paid_minute_limit": 20,
"resets_at": "00:00 UTC"
}
}successbooleanRequireddataobjectRequireddata.free_daily_limitintegerRequireddata.free_minute_limitintegerRequireddata.paid_price_mcintegerRequireddata.paid_minute_limitintegerRequireddata.resets_atstringRequired
Check account nickname
/api/v1/h2h/check-nicknameAccount mode governs billing; no per-request sandbox override. Found and not-found consume allowance or paid charge, including repeated checks.
Both headers: X-API-Key and X-Secret-Key
Parameters
| Name | Type / source | Required | Description |
|---|---|---|---|
| game_code | string · body | Required | Copy games[].game_code from GET /check-nickname/games. It is separate from a product brand slug. maxLength=100 |
| customer_target | string · body | Required | Account ID or customer number for the selected game/account check. Send it as a string. maxLength=255 |
| customer_target_zone | string | null · body | Optional | Account zone/server according to zone_required and zone_values from GET /check-nickname/games. Required for the Mobile Legends purchase-limit check. maxLength=100 |
curl -X POST 'https://api.melostore.id/api/v1/h2h/check-nickname' \
-H 'Accept: application/json' \
-H "X-API-Key: $H2H_API_KEY" \
-H "X-Secret-Key: $H2H_SECRET_KEY" \
-H 'Content-Type: application/json' \
--data '{
"game_code": "mobile-legends",
"customer_target": "123456789",
"customer_target_zone": "1234"
}'Request succeeded.
{
"success": true,
"data": {
"game_code": "mobile-legends",
"customer_target": "123456789",
"customer_target_zone": "1234",
"username": "Example Player",
"region": "ID",
"region_type": "country"
},
"billing": {
"charged_mc": 0,
"free_remaining_today": 39,
"transaction_id": null
}
}successbooleanRequireddataobjectRequireddata.game_codestringRequireddata.customer_targetstringRequireddata.customer_target_zonestring | nullRequireddata.usernamestringRequireddata.regionstring | nullRequireddata.region_typestring | nullRequiredbillingobjectRequiredbilling.charged_mcnumberRequiredbilling.free_remaining_todayintegerRequiredbilling.transaction_idstring | nullRequired
Mobile Legends purchase limit
/api/v1/h2h/mobile-legends/purchase-limitSeparate account-check endpoint. Returns per-package flags; do not infer numeric limits from failures. Uses the same billing/quota as nickname.
Both headers: X-API-Key and X-Secret-Key
Parameters
| Name | Type / source | Required | Description |
|---|---|---|---|
| customer_target | string · body | Required | Account ID or customer number for the selected game/account check. Send it as a string. maxLength=255 |
| customer_target_zone | string · body | Required | Account zone/server according to zone_required and zone_values from GET /check-nickname/games. Required for the Mobile Legends purchase-limit check. maxLength=100 |
curl -X POST 'https://api.melostore.id/api/v1/h2h/mobile-legends/purchase-limit' \
-H 'Accept: application/json' \
-H "X-API-Key: $H2H_API_KEY" \
-H "X-Secret-Key: $H2H_SECRET_KEY" \
-H 'Content-Type: application/json' \
--data '{
"customer_target": "123456789",
"customer_target_zone": "1234"
}'Request succeeded.
{
"success": true,
"data": {
"game_code": "mobile-legends",
"customer_target": "123456789",
"customer_target_zone": "1234",
"username": "Example Player",
"weekly_pass": {
"limit_reached": false,
"items": [
{
"package_code": "weekly-pass",
"limit_reached": false
}
]
},
"double_diamonds": {
"eligible": true,
"items": [
{
"package_code": "double-diamonds-50",
"limit_reached": false
}
]
}
},
"billing": {
"charged_mc": 0,
"free_remaining_today": 39,
"transaction_id": null
}
}successbooleanRequireddataobjectRequireddata.game_codestringRequireddata.customer_targetstringRequireddata.customer_target_zonestringRequireddata.usernamestringRequireddata.weekly_passobjectRequireddata.weekly_pass.limit_reachedbooleanRequireddata.weekly_pass.itemsarrayRequireddata.weekly_pass.items[].package_codestringRequireddata.weekly_pass.items[].limit_reachedbooleanRequireddata.double_diamondsobjectRequireddata.double_diamonds.eligiblebooleanRequireddata.double_diamonds.itemsarrayRequireddata.double_diamonds.items[].package_codestringRequireddata.double_diamonds.items[].limit_reachedbooleanRequiredbillingobjectRequiredbilling.charged_mcnumberRequiredbilling.free_remaining_todayintegerRequiredbilling.transaction_idstring | nullRequired
Regular, Smart, and bulk child callbacks
Enter only the Webhook URL in the dashboard. The Webhook Secret is generated automatically with the API Key and Secret Key through Rotate Keys, shown once, and saved on your webhook receiver. Each rotation replaces all three; update the receiver secret to keep verifying subsequent callbacks.
Final callbacks are sent for success/failed; transaction.action_change events can also arrive while processing. Orders placed manually through the dashboard do not send callbacks. For bulk, callbacks are sent per transaction; use GET /bulk-orders/{id} for the progress summary. The same callback may arrive more than once.
Common fields: id, buyer_trx_id, sku_code, customer_target, customer_target_zone, price_charged, status, serial_number, is_sandbox, created_at, updated_at. Match callbacks to orders using id/buyer_trx_id. For Smart, store the ordered SKU and read fulfilled_sku_code for the successfully fulfilled SKU. Prices use price in transaction responses and price_charged in callbacks; check your balance and invoice for the final cost after refunds.
Only Smart adds max_bid, fulfilled_sku_code (null before success), attempts_count. Only bulk children add bulk_order_id, buyer_bulk_id, bulk_sequence. Failed adds error_code and message when available. Regular callbacks have none of the Smart/bulk additions.
Verify X-H2H-Signature as a hex HMAC-SHA256 over the raw body with webhook_secret. Record action event identity by event_id and version, and final results by id:status before returning 2xx. Ensure repeated deliveries do not process an order or wallet effect twice. Expand the advanced receiver examples below and adapt event storage to your application.
{
"id": "11111111-1111-4111-8111-111111111111",
"buyer_trx_id": "example-order-001",
"sku_code": "EXAMPLE-10",
"customer_target": "123456789",
"customer_target_zone": "1234",
"status": "success",
"serial_number": "EXAMPLE-SN-001",
"is_sandbox": true,
"created_at": "2026-10-02T03:00:00+00:00",
"price_charged": 1000,
"updated_at": "2026-10-02T03:00:00+00:00"
}These examples use PostgreSQL to persist events before responding. Adapt storage to your application while preserving signature verification and duplicate handling.
CREATE TABLE h2h_callback_inbox (
event_key VARCHAR(128) PRIMARY KEY,
payload JSONB NOT NULL,
received_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE h2h_action_state (
transaction_id VARCHAR(255) PRIMARY KEY,
action_id UUID NOT NULL,
version BIGINT NOT NULL,
payload JSONB NOT NULL
);// npm install express pg; apply the PostgreSQL schema below first.
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';
import pg from 'pg';
const secret = process.env.H2H_WEBHOOK_SECRET;
if (!secret) throw new Error('H2H_WEBHOOK_SECRET is required');
const db = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const app = express();
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const validAction = a => a && uuid.test(a.id) && a.type === 'two_factor'
&& Number.isSafeInteger(a.version) && a.version > 0
&& ['required', 'submitted', 'resolved'].includes(a.state)
&& a.can_submit === (a.state === 'required') && typeof a.message === 'string';
// Register the raw-body route before express.json().
app.post('/h2h-callback', express.raw({ type: 'application/json', limit: '1mb' }), async (req, res) => {
const signature = req.get('X-H2H-Signature') || '';
if (!Buffer.isBuffer(req.body) || !/^[a-f0-9]{64}$/i.test(signature)) return res.sendStatus(401);
const expected = createHmac('sha256', secret).update(req.body).digest();
if (!timingSafeEqual(expected, Buffer.from(signature, 'hex'))) return res.sendStatus(401);
let event;
try { event = JSON.parse(req.body.toString('utf8')); } catch { return res.sendStatus(400); }
if (!event || typeof event.id !== 'string') return res.sendStatus(422);
const actionEvent = event.event === 'transaction.action_change';
if (actionEvent) {
if (!uuid.test(event.event_id) || !validAction(event.action)
|| !['pending', 'processing', 'success', 'failed'].includes(event.status)
|| event.need_action !== (event.action.state === 'required')) return res.sendStatus(422);
} else if (event.event || !['success', 'failed'].includes(event.status)) return res.sendStatus(422);
if (event.action && !validAction(event.action)) return res.sendStatus(422);
const key = actionEvent ? 'action:' + event.event_id + ':' + event.action.version : event.id + ':' + event.status;
let client;
try {
client = await db.connect(); await client.query('BEGIN');
const inserted = await client.query('INSERT INTO h2h_callback_inbox(event_key, payload) VALUES ($1, $2::jsonb) ON CONFLICT (event_key) DO NOTHING RETURNING event_key', [key, JSON.stringify(event)]);
if (inserted.rowCount && event.action) {
await client.query('INSERT INTO h2h_action_state(transaction_id, action_id, version, payload) VALUES ($1, $2, $3, $4::jsonb) ON CONFLICT (transaction_id) DO UPDATE SET version=EXCLUDED.version, payload=EXCLUDED.payload WHERE h2h_action_state.action_id=EXCLUDED.action_id AND h2h_action_state.version < EXCLUDED.version', [event.id, event.action.id, event.action.version, JSON.stringify(event.action)]);
}
await client.query('COMMIT');
// Inbox workers handle action events separately from final results.
// An action event never settles an order or changes balances. Read the
// latest h2h_action_state; older versions must not reopen the buyer form.
return res.sendStatus(200); // Includes duplicates and older action versions.
} catch { if (client) await client.query('ROLLBACK'); return res.sendStatus(500); }
finally { client?.release(); }
});
app.listen(3000);<?php
// Apply the PostgreSQL schema below first.
$secret = getenv('H2H_WEBHOOK_SECRET');
if (!$secret) { http_response_code(500); exit; }
$raw = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_H2H_SIGNATURE'] ?? '';
if (!preg_match('/^[a-f0-9]{64}$/i', $signature) || !hash_equals(hash_hmac('sha256', $raw, $secret), strtolower($signature))) { http_response_code(401); exit; }
try { $event = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $e) { http_response_code(400); exit; }
$uuid = fn($s) => is_string($s) && preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $s);
$validAction = fn($a) => is_array($a) && $uuid($a['id'] ?? null) && ($a['type'] ?? '') === 'two_factor'
&& is_int($a['version'] ?? null) && $a['version'] > 0
&& in_array($a['state'] ?? '', ['required', 'submitted', 'resolved'], true)
&& ($a['can_submit'] ?? null) === ($a['state'] === 'required') && is_string($a['message'] ?? null);
if (!is_array($event) || !is_string($event['id'] ?? null)) { http_response_code(422); exit; }
$actionEvent = ($event['event'] ?? '') === 'transaction.action_change';
if ($actionEvent) {
if (!$uuid($event['event_id'] ?? null) || !$validAction($event['action'] ?? null)
|| !in_array($event['status'] ?? '', ['pending','processing','success','failed'], true)
|| ($event['need_action'] ?? null) !== ($event['action']['state'] === 'required')) { http_response_code(422); exit; }
} elseif (isset($event['event']) || !in_array($event['status'] ?? '', ['success','failed'], true)) { http_response_code(422); exit; }
if (isset($event['action']) && !$validAction($event['action'])) { http_response_code(422); exit; }
$key = $actionEvent ? 'action:'.$event['event_id'].':'.$event['action']['version'] : $event['id'].':'.$event['status'];
try {
$db = new PDO(getenv('DATABASE_DSN'), getenv('DATABASE_USER'), getenv('DATABASE_PASSWORD'), [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$db->beginTransaction();
$stmt = $db->prepare('INSERT INTO h2h_callback_inbox(event_key, payload) VALUES (:key, CAST(:payload AS jsonb)) ON CONFLICT (event_key) DO NOTHING RETURNING event_key');
$stmt->execute(['key' => $key, 'payload' => $raw]);
if ($stmt->fetchColumn() && isset($event['action'])) {
$upsert = $db->prepare('INSERT INTO h2h_action_state(transaction_id, action_id, version, payload) VALUES (:trx, :action, :version, CAST(:payload AS jsonb)) ON CONFLICT (transaction_id) DO UPDATE SET version=EXCLUDED.version, payload=EXCLUDED.payload WHERE h2h_action_state.action_id=EXCLUDED.action_id AND h2h_action_state.version < EXCLUDED.version');
$upsert->execute(['trx' => $event['id'], 'action' => $event['action']['id'], 'version' => $event['action']['version'], 'payload' => json_encode($event['action'])]);
}
$db->commit();
// Separate action events from final results in inbox workers. Never settle
// an order for an action event; use the latest action version for the form.
http_response_code(200); echo 'OK';
} catch (Throwable $e) { if (isset($db) && $db->inTransaction()) $db->rollBack(); http_response_code(500); }
Import collections and environment
Download OpenAPI 3.1.0 or Postman Collection v2.1 and the environment template. In Postman choose Import file, import the environment, and select it. Fill api_key/secret_key from Generate/Rotate Keys; template credentials are empty. Save webhook_secret from the same modal on your callback receiver. base_url points to the API without a locale prefix.
Fetch the catalogue first. Set sku_code for regular/bulk, smart_sku_code for Smart, customer_target, zone, and additional_data from the inquiry fields. Set buyer_trx_id for regular, smart_buyer_trx_id for Smart, and buyer_bulk_id for bulk once per order; scripts never rotate them on retry. Create scripts store transaction_id/bulk_order_id for subsequent requests. The matching brands request saves brand_id and category_slug from the first result. For 2FA, first check the correct order status to populate action_buyer_trx_id, action_id, and action_version, then enter the buyer code as two_factor_code.
The callback folder contains saved examples and receiver guidance, with no automatic request to a partner URL. webhook_secret belongs to the receiver, not API authentication. Sandbox bulk invoice requests return 409. For a final live bulk use Send and Download on the invoice operation or the Bulk Order menu.