Payment Links+ API
Topics covered on this page
Payment Links+ API
Payment Links+ lets you create secure, branded payment links. With Payment Links+, you can set up and share payment links effortlessly, regardless of the sales channel or payment method. To learn more, check out Payment Links+.
This document explains the Payment Links+ API functions.
Authorization
The Payment Links+ API requires an API key to authenticate calls. Retrieve this key from Settings > API keys on the Payment Links+ dashboard, and pass it in the Authorization header.
curl --location 'https://linksplus-api.omise.co/external/links' \
--header 'Authorization: {{key}}'
Roll your key with care: Once you roll a key from the API keys screen, the dashboard never shows the old value again. If you lose track of the current key, generate a new one by rolling it again — there's no way to retrieve a rolled key afterward.
Objects
Every endpoint on this page returns one of the objects this section describes. Rather than repeating the full shape under each endpoint, this page defines each object once and references it by name throughout.
The Link object
| Field | Type | Description |
|---|---|---|
id |
integer | The link's unique ID. |
name |
string | The link's display name. Up to 45 characters. |
currency |
string | The payment currency. |
amount |
integer | The charge amount, in the smallest unit of currency. Minimum 100. |
multiple_usage |
boolean | Whether the link can be used more than once. |
additional |
string, nullable | A free-text note about the link, up to 255 characters. The payment form hides it by default. |
expired_at |
string, ISO 8601, nullable | When the link expires. Returns null when the link has no expiry. |
features |
object | Key-value pairs matching the product detail fields that the link's template defines. |
transaction_id |
string | The ID portion of url. |
url |
string | The URL to share with the customer to complete payment. |
state |
string | The link's lifecycle stage: "active", "pending", "disabled", "expired", or "used". |
template_id |
integer | The template you used to create the link. |
kind |
string, nullable | null for a standard link, "quick_link" for a link created without a manually selected template. |
created_at |
string, ISO 8601 | The time the system created the link. |
updated_at |
string, ISO 8601 | The time the system last updated the link. |
{
"id": 20417,
"name": "Tiger Paw School Wear",
"state": "active",
"amount": 4000,
"currency": "THB",
"multiple_usage": true,
"additional": "Order #12345 - priority shipping",
"expired_at": "2026-12-31T23:59:59Z",
"features": {
"Type": "School Bags",
"Size": "Medium"
},
"transaction_id": "Hq3TzWm8Rc",
"url": "https://linksplus.omise.co/Hq3TzWm8Rc",
"template_id": 4108,
"kind": null,
"created_at": "2026-10-01T08:42:17.604Z",
"updated_at": "2026-10-01T08:42:17.604Z"
}
The Template object
| Field | Type | Description |
|---|---|---|
id |
integer | The template's unique ID. |
name |
string | The template's display name. |
features |
array of objects | The product detail fields that the template defines. Each entry has display_name, value (always null here, reflecting the field definition rather than a filled-in value), and type. |
capabilities |
array of Capability objects | The payment methods, currencies, and card brands available on links created from this template. |
zero_interest_installment |
boolean | Whether the template enables zero-interest installments. |
created_at |
string, ISO 8601 | The time the system created the template. |
updated_at |
string, ISO 8601 | The time the system last updated the template. |
Note: a template's features is an array of field definitions (label, type, and an empty value). A link's features is a flat object of the actual values entered for that link. Don't expect them to have the same shape.
{
"id": 4108,
"name": "Tiger Paw Bags",
"features": [
{ "display_name": "Type", "value": null, "type": "string" },
{ "display_name": "Size", "value": null, "type": "string" }
],
"capabilities": [
"... see The Capability object for the \"card\" entry ...",
"... a second entry with \"payment_method\": \"promptpay\" ..."
],
"zero_interest_installment": false,
"created_at": "2026-08-19T10:16:49.689Z",
"updated_at": "2026-08-19T10:16:49.689Z"
}
The Capability object
Each entry in a Template object's capabilities array describes one payment method available on it. A template supporting multiple payment methods has one Capability object per method — the example following shows a single card entry, followed by what the full array looks like when a template also accepts PromptPay.
| Field | Type | Description |
|---|---|---|
payment_method |
string | e.g. "card", "internet_banking_bbl", "promptpay", "truemoney". |
currencies |
array of strings | Currencies supported for this payment method. |
card_brands |
array of strings | e.g. "JCB", "Visa", "MasterCard". Present even for non-card methods, though it doesn't apply to them. |
installment_terms |
array | Available installment terms, if any. |
banks |
array | Available banks, if any. |
manual |
boolean | Card capability only. |
saveCard |
boolean | Card capability only. |
{
"payment_method": "card",
"currencies": ["THB", "JPY", "USD", "EUR", "GBP", "SGD", "AUD", "CHF", "CNY", "DKK", "HKD", "MYR"],
"card_brands": ["JCB", "Visa", "MasterCard"],
"installment_terms": [],
"banks": [],
"manual": false,
"saveCard": false
}
A capabilities array with more than one payment method looks like this — note that manual and saveCard only appear on the card entry, matching the preceding table:
[
{
"payment_method": "card",
"currencies": ["THB", "JPY", "USD", "EUR", "GBP", "SGD", "AUD", "CHF", "CNY", "DKK", "HKD", "MYR"],
"card_brands": ["JCB", "Visa", "MasterCard"],
"installment_terms": [],
"banks": [],
"manual": false,
"saveCard": false
},
{
"payment_method": "promptpay",
"currencies": ["THB"],
"card_brands": ["JCB", "Visa", "MasterCard"],
"installment_terms": [],
"banks": []
}
]
Errors
Every error response shares the same shape:
{
"object": "error",
"code": "bad_request",
"message": "human-readable description of what went wrong"
}
The 401 authentication error additionally includes a location field linking to the API errors reference:
{
"object": "error",
"location": "https://www.omise.co/api-errors#authentication-failure",
"code": "authentication_failure",
"message": "authentication failed"
}
| HTTP Status | code |
Meaning |
|---|---|---|
400 |
bad_request |
The request is malformed, or a parameter failed validation. The message field describes the specific problem — see each endpoint's own section for examples unique to it. |
401 |
authentication_failure |
The API key is missing or invalid. |
403 |
forbidden |
Your API key doesn't have permission for this action, or doesn't have access to the company that owns the resource. |
404 |
not_found |
The resource in the URL path (a link, template, or team ID) doesn't exist. |
Individual endpoints repeat only the unique error cases. Every endpoint can also return 400, 401, and 403, whether or not they're repeated there.
Endpoints
Retrieve a template
GET /external/templates/:template_id
Returns the Template object for the specified ID. Find a template's ID on the Payment Links+ dashboard, under Create template — the 4-digit number that appears under each template thumbnail.
Path parameter: template_id (integer, required)
curl 'https://linksplus-api.omise.co/external/templates/4091' \
-X GET \
-H 'Authorization: {{key}}'
Returns: a Template object on success (200):
{
"id": 4091,
"name": "Jelly Bean Sneakers",
"features": [],
"capabilities": ["... see The Capability object ..."],
"zero_interest_installment": false,
"created_at": "2026-08-17T08:35:55.390Z",
"updated_at": "2026-08-17T08:35:55.390Z"
}
Unique errors: 404 if the template ID doesn't exist ("message": "template not found").
Retrieve all templates for a team
GET /external/:team_id/templates
Returns the templates belonging to the specified team. Find a team's ID on the dashboard, under Settings > Teams.
Path parameter: team_id (integer, required)
curl 'https://linksplus-api.omise.co/external/5512/templates' \
-X GET \
-H 'Authorization: {{key}}'
Returns: {"templates": [Template, ...], "total": N}:
{
"templates": [
{
"id": 4108,
"name": "Tiger Paw Bags",
"features": [
{ "display_name": "Type", "value": null, "type": "string" },
{ "display_name": "Size", "value": null, "type": "string" }
],
"capabilities": [
"... see The Capability object for the \"card\" entry ...",
"... a second entry with \"payment_method\": \"promptpay\" ..."
],
"zero_interest_installment": false,
"created_at": "2026-08-19T10:16:49.689Z",
"updated_at": "2026-08-19T10:16:49.689Z"
},
{
"id": 4091,
"name": "Jelly Bean Sneakers",
"features": [],
"capabilities": ["... see The Capability object ..."],
"zero_interest_installment": false,
"created_at": "2026-08-17T08:35:55.390Z",
"updated_at": "2026-08-17T08:35:55.390Z"
}
],
"total": 2
}
Unique errors: 404 if the team ID doesn't exist ("message": "team not found").
Retrieve a link
GET /external/links/:link_id
Returns the Link object for the specified ID. Click Overview, then a payment link, to find its ID in the dashboard URL.
Path parameter: link_id (integer, required)
curl 'https://linksplus-api.omise.co/external/links/20395' \
-X GET \
-H 'Authorization: {{key}}'
Returns: a Link object on success (200):
{
"id": 20395,
"name": "Tiger Paw Bags",
"state": "active",
"amount": 200000,
"currency": "THB",
"multiple_usage": true,
"additional": null,
"expired_at": null,
"features": {
"Type": "School Bags",
"Size": "Medium"
},
"transaction_id": "Vb9LdXe2Ko",
"url": "https://linksplus.omise.co/Vb9LdXe2Ko",
"template_id": 4108,
"kind": null,
"created_at": "2026-09-28T10:58:37.429Z",
"updated_at": "2026-09-28T10:58:37.429Z"
}
Unique errors: 404 if the link ID doesn't exist ("message": "link not found").
Retrieve all links for a team
GET /external/:team_id/links
Returns the links belonging to the specified team. Find a team's ID on the dashboard, under Settings > Teams.
Path parameter: team_id (integer, required)
curl 'https://linksplus-api.omise.co/external/5512/links' \
-X GET \
-H 'Authorization: {{key}}'
Returns: {"links": [Link, ...], "total": N}:
{
"links": [
{
"id": 20395,
"name": "Tiger Paw Bags",
"state": "active",
"amount": 200000,
"currency": "THB",
"multiple_usage": true,
"additional": null,
"expired_at": null,
"features": {
"Type": "School Bags",
"Size": "Medium"
},
"transaction_id": "Vb9LdXe2Ko",
"url": "https://linksplus.omise.co/Vb9LdXe2Ko",
"template_id": 4108,
"kind": null,
"created_at": "2026-09-28T10:58:37.429Z",
"updated_at": "2026-09-28T10:58:37.429Z"
}
],
"total": 1
}
Unique errors: 404 if the team ID doesn't exist ("message": "team not found").
Create a payment link
POST /external/links
Creates a link from the specified template. If the template defines product detail fields, you must pass the value of each as a features key/value pair.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
template_id |
integer | Yes | The template to use for the link. |
team_id |
integer | Yes | The team that will own the link. Find a team's ID on the dashboard, under Settings > Teams. |
name |
string | Yes | The link's display name. Up to 45 characters. |
currency |
string | Yes | The payment currency. |
amount |
integer | Yes | The charge amount, in the smallest unit of currency. Minimum 100. |
multiple_usage |
boolean | Yes | Whether the link can be used more than once. |
additional |
string, nullable | No | A free-text note about the link, up to 255 characters. The payment form hides it by default. |
expired_at |
string, nullable | No | An ISO 8601 date-time with any timezone offset (e.g. 2026-12-31T23:59:59Z). The API stores and returns the value in UTC. Must be a future date. Same format as on Update a link. |
features |
object | Conditional | Required only if the template defines product detail fields. Each key must exactly match a field name that the template defines (e.g. Date, Order_ID) — a key that doesn't match returns an error. |
custom_webhook |
string | No | A custom HTTPS webhook URL for this link — see Webhook configuration, following. |
returnUrl |
string | No | The URL to redirect users to after payment. Priority order: (1) this value, (2) the template's return URL, (3) the account's return URL under Settings > Return URL. If you set none of them, no redirect button appears on the success page. |
curl 'https://linksplus-api.omise.co/external/links' \
-X POST \
-H 'Authorization: {{key}}' \
-d "template_id=4108" \
-d "team_id=5512" \
-d "name=Tiger Paw School Wear" \
-d "currency=THB" \
-d "amount=4000" \
-d "features[Type]=School Bags" \
-d "features[Size]=Medium" \
-d "multiple_usage=true" \
-d "additional=Order #12345 - priority shipping" \
-d "expired_at=2026-12-31T23:59:59Z" \
-d "custom_webhook=https://example.com/payment-webhook" \
-d "returnUrl=https://example.com/payment_status"
Both form-encoded (as shown) and raw JSON request bodies work — Postman's Body → raw → JSON mode sends the same fields as a nested JSON object instead of -d flags.
Returns: a Link object on success (201):
{
"id": 20417,
"name": "Tiger Paw School Wear",
"state": "active",
"amount": 4000,
"currency": "THB",
"multiple_usage": true,
"additional": "Order #12345 - priority shipping",
"expired_at": "2026-12-31T23:59:59Z",
"features": {
"Type": "School Bags",
"Size": "Medium"
},
"transaction_id": "Hq3TzWm8Rc",
"url": "https://linksplus.omise.co/Hq3TzWm8Rc",
"template_id": 4108,
"kind": null,
"created_at": "2026-10-01T08:42:17.604Z",
"updated_at": "2026-10-01T08:42:17.604Z"
}
Unique errors: 400 if the template has product detail fields but a required features key is missing ("message": "invalid features. \"Date\" is required").
Webhook configuration
Payment Links+ supports up to three webhook endpoints per payment:
- Default System Webhook — Payment Links+ configures it automatically.
- Dashboard Webhook — you configure it in your Merchant Dashboard, and Payment Links+ fetches it automatically.
- Custom API Webhook — you set it per link via
custom_webhook(v3.10.0+).
Requirement: the API accepts only HTTPS URLs for custom_webhook. A custom webhook failure never blocks payment processing — the other configured webhooks still fire normally, and all configured webhooks receive the same event notifications.
Update a link
PUT /external/links/:link_id
Updates an existing link. Every body field is optional — send only what you want to change.
Path parameter: link_id (integer, required)
Parameters
| Name | Type | Description |
|---|---|---|
name |
string | Up to 45 characters. |
currency |
string | One of THB, USD, EUR, GBP, JPY, SGD, MYR. Narrower than the currencies available when creating a link — you can't set AUD, CHF, CNY, DKK, or HKD through this endpoint. |
features |
object | Link-specific feature values. |
additional |
string | Up to 255 characters. The payment form hides it by default. |
expired_at |
string, nullable | An ISO 8601 date-time with any timezone offset (e.g. 2026-12-31T23:59:59Z). The API stores and returns the value in UTC. Must be a future date. Pass null to clear an existing expiry. |
returnUrl |
string | The URL to redirect users to after payment. |
Note: expired_at uses the same ISO 8601 date-time format here as on Create a payment link.
Note: team_id can't be changed through this endpoint — you can't move a link to a different team after creation.
curl 'https://linksplus-api.omise.co/external/links/5206' \
-X PUT \
-H 'Authorization: {{key}}' \
-H 'Content-Type: application/json' \
-d '{
"name": "Updated Payment for Order #12345",
"expired_at": "2026-12-31T23:59:59Z"
}'
Returns: the updated Link object on success (200):
{
"id": 5206,
"name": "Updated Payment for Order #12345",
"state": "active",
"amount": 15000,
"currency": "THB",
"multiple_usage": false,
"additional": null,
"expired_at": "2026-12-31T23:59:59Z",
"features": {},
"transaction_id": "Ny6RpJt1Ua",
"url": "https://linksplus.omise.co/Ny6RpJt1Ua",
"template_id": 3977,
"kind": null,
"created_at": "2026-09-15T06:21:37.251Z",
"updated_at": "2026-09-30T09:26:21.416Z"
}
Unique errors: 404 if the link ID doesn't exist.
Disable a link
POST /external/links/:link_id/disable
Disables the specified link. You can disable only a link whose state is active or pending. Disabling a link that's already disabled returns it unchanged, so repeating the call is safe.
Path parameter: link_id (integer, required). Find a link's ID the same way as on Retrieve a link.
The request takes no body.
curl 'https://linksplus-api.omise.co/external/links/20395/disable' \
-X POST \
-H 'Authorization: {{key}}'
Returns: the disabled Link object on success (200):
{
"id": 20395,
"name": "Tiger Paw Bags",
"state": "disabled",
"amount": 200000,
"currency": "THB",
"multiple_usage": true,
"additional": null,
"expired_at": null,
"features": {
"Type": "School Bags",
"Size": "Medium"
},
"transaction_id": "Vb9LdXe2Ko",
"url": "https://linksplus.omise.co/Vb9LdXe2Ko",
"template_id": 4108,
"kind": null,
"created_at": "2026-09-28T10:58:37.429Z",
"updated_at": "2026-10-05T03:14:52.118Z"
}
Unique errors: 400 if the link has expired or has already been paid, and 404 if the link ID doesn't exist ("message": "link not found").
Tracking payments after creating a link
Once you've created a link, send its url to the customer to complete the payment.
Dashboard timing differs between Payment Links+ and Omise: a created link appears immediately in the Payment Links+ dashboard. In the Omise Payments dashboard, the corresponding charge doesn't appear until the customer actually attempts the transaction — a completed payment shows as successful, a customer who drops off during the OTP step leaves the charge pending, and a customer who cancels during OTP leaves it failed.
Finding a charge on the Omise dashboard
After a customer attempts or completes a transaction, find the matching charge using either the link's id or its features:
- Go to Charges on the Omise Payments dashboard.
- Select Advanced Search.
- Choose Metadata as the filter.
- Enter the
idorfeaturesvalue from the API response.
Webhook field mapping: webhook payloads use link_id rather than id — link_id in a webhook corresponds to the id field returned by the API.
FAQ
Q: Does the create-link endpoint accept JSON, or only form-encoded data?
A: Both. Send parameters as form-encoded -d fields in curl, or as a raw JSON body — Postman's Body → raw → JSON mode works the same way.
Q: What format does expired_at expect?
A: Create a payment link and Update a link both expect an ISO 8601 date-time string with any timezone offset (e.g., 2026-12-31T23:59:59Z). The API stores and returns the value in UTC, and the date must be in the future.
Q: Can I change which team a link belongs to?
A: No. team_id is required when creating a link but isn't accepted when updating one — a link can't move between teams after creation.
Q: I passed a features value and still got a validation error. Why?
A: The keys must exactly match the product detail field names that the template defines, not any name you choose. Check the template's configuration in Create template on the dashboard for the exact field names.
Q: What values can a link's state have?
A: active, pending, disabled, expired, or used — see The Link object.
Q: What happens if I disable a link that's already disabled?
A: Nothing changes. The API returns the link as it is with a 200 response, so retrying a disable call is safe — see Disable a link.
Q: Can I disable a link that has expired or already been paid?
A: No. The API accepts a disable request only for a link in the active or pending state, and returns 400 for an expired or paid link.
Q: I don't see a charge on the Omise dashboard right after creating a link. Is something wrong? A: No — a charge only appears there once the customer attempts the transaction. It appears immediately on the Payment Links+ dashboard.
Q: How do I set a custom webhook without going through the dashboard?
A: Pass an HTTPS URL in custom_webhook when creating the link — see Webhook configuration.