Myela Payments API
Version v1 — one API, whichever provider settles the money. 58 endpoints across 10 sections.
Overview
Start with GET /v1/capabilities. It returns every operation and whether it is active, unavailable (your key lacks the entitlement) or not_supported (the provider cannot do it). Branch on that at integration time rather than discovering a 403 in production.
Authentication
Authorization: Bearer myela_sk_test_…
Charging needs no publishable key and no browser credential. POST /v1/payments is a server-to-server call: a test key charges with its bearer token alone, and a live key additionally HMAC-signs each request (below). The publishable key belongs to exactly one optional integration style (Myela Elements, the browser checkout below); if you are not using Elements, you never need it. It is not a setup prerequisite.
Secret keys are shown once, at issuance — stored hashed, unrecoverable; a revoked key stops working immediately. The publishable key is different on purpose: it is not hashed, and the portal (Settings → API keys) shows it in full, any time.
A test key authenticates with the bearer token alone. A live key must additionally HMAC-sign every request — an unsigned live key is refused with
401 This key requires request signing. That is why curl and Postman are fine for test keys and unsuitable for live ones.| Key | Prefix | Lives | Can |
|---|---|---|---|
| Secret | myela_sk_ | your server | everything, including moving money |
| Publishable | myela_pk_ | the browser | start a checkout session and exchange a token — nothing else, and only from an origin on its allow-list |
| The card field’s own key | not yours to handle | inside the Myela-provided capture field | minting card tokens in the browser — it belongs to the payment provider, is configured by Myela, and never touches your server |
Never put a secret key in a browser, a mobile app, or a repository. And note the last row: two unrelated credentials get called a “public key” — your Myela publishable key, and the capture field’s own key. You only ever handle the first, and only if you use Elements.
Collections
Every route on this page, ready to run. Both are generated from the same route table as the reference below, so they cannot describe an endpoint that no longer exists.
No credentials are included. Each ships a MYELA_API_KEY placeholder — paste a test key from Settings → API. A live key additionally requires request signing, which neither tool does out of the box.
The sandbox environment points at https://payments-api-sandbox.merchantservicedepot.com; the local one athttp://localhost:4002. Import whichever matches what you are running, or pointBASE_URL at your own host.
Money
4999 is $49.99.
Sending
49.99 is rejected rather than coerced — a float that silently becomes 4999 today becomes 4998 the day it arrives from an arithmetic expression, and that is a settlement discrepancy nobody traces back to the API.Card capture
Your page mounts the card field with a publishable key; the shopper types their card; you receive a single-use
paymentToken; your server sends that token to POST /v1/payments to charge it, or POST /v1/payment_methods to store it. A token is single-use and short-lived — exactly how short varies by the provider behind your account, so mint a fresh one per operation, immediately before the request that spends it.
That page is the only place a card becomes a token: a card can only be captured in a browser, and no Myela endpoint accepts a card number — which is what keeps card numbers off every server. (
POST /v1/tokens is an exchange, not a mint: it turns the field’s own token into the mtok_ your server spends.) No browser integration yet? Ask your Myela contact for a test capture page.Myela Elements
Server contract live; hosted frame not yet. The two routes below — POST /v1/checkout_sessions and POST /v1/tokens — are deployed and can be called today, and they are documented in the collections above. What is not serving yet is js.myela.com, so the middle step (mounting the fields and getting a provider token) has no hosted implementation. Until it does, capture a card with the field your Myela contact provides and exchange the result at POST /v1/tokens, or send it straight to POST /v1/payments as a paymentToken.
A session can only start from an origin you have registered. Your publishable key carries an allow-list you manage. It starts EMPTY, and an empty list refuses every origin — a checkout that returns 401 until you add your domain is recoverable; one open to the whole internet is not.
<script type="module">
import { Myela } from 'https://js.myela.com/v1/elements.js';
const myela = Myela({ publishableKey: 'myela_pk_test_…' });
const card = myela.elements().create('card');
await card.mount('#card-field');
card.on('change', (e) => (payButton.disabled = !e.complete));
payButton.addEventListener('click', async () => {
const { token, error } = await card.tokenize();
if (error) return showError(error.message);
// token is opaque: "mtok_…". Send it to YOUR server.
await fetch('/checkout', { method: 'POST', body: JSON.stringify({ token }) });
});
</script>
Then, server-side, the token is just a paymentToken:
await myela.payments.create({ amount: 4999, currency: 'USD', paymentToken: token });
Use the publishable key here (myela_pk_), never the secret one. A publishable key is meant to be readable by anyone who views the page; a secret key in a browser publishes your ability to move money to every visitor, and Elements rejects one outright.
The token is single-use and short-lived — mint a fresh one per payment, immediately before you spend it.
Playground
bun install
bun run playground # → http://localhost:4477
One process doing three jobs: the UI, an in-memory mock of this API, and scenarios that drive the real SDK against that mock over real HTTP. So what you observe is the SDK's genuine behaviour rather than a description of it.
Nothing there can move money. No credentials, no network calls beyond loopback, no settlement.
Test tokens select the outcome, because you cannot rehearse a decline by waiting for a real card to be declined:
tok_ok authorizes in full, tok_partial authorizes half the requested amount (status partially_authorized), tok_insufficient declines with a soft error a retry may clear.The mock is a third dialect. Its outcomes are its own — they do not reproduce against a real provider's sandbox, where the card number or the request decides. Use it to learn the SDK's shape, never to predict what a specific card will do.
Pagination
GET /v1/customers?limit=25
GET /v1/customers?limit=25&startingAfter=cus_abc123
{ "object": "list", "data": [ … ], "hasMore": true, "nextCursor": "cus_xyz789" }
limit defaults to 10 and caps at 100. Follow nextCursor by passing it as startingAfter. It is present only when hasMore is true, so a loop that stops on hasMore always terminates. A cursor naming no row is a 422, never a silent restart at page one.Errors
{ "error": { "code": "invalid_request", "message": "…" } }
Branch on code, never on message. Codes are a contract; messages are prose and get improved.
An object belonging to another merchant is
404, never 403 — confirming it exists would leak that it exists. 501 is a permanent refusal: do not retry it, check capabilities instead.| HTTP | code | Means |
|---|---|---|
401 | authentication_failed | missing, malformed, revoked, or unsigned live key |
403 | permission_denied | valid key, but not permitted to perform that operation |
403 | capability_not_enabled | the operation exists but is not enabled for your account — the capability field names it. Ask your Myela contact to enable it; unlike 501 this is not permanent |
404 | not_found | no such object on your account |
409 | conflict | illegal state transition, e.g. voiding a settled payment |
422 | invalid_request | the request is wrong — bad amount, unknown cursor, missing field |
500 | api_error | our fault; safe to retry |
501 | capability_not_supported | permanent refusal — do not retry |
Idempotency
Idempotency-Key: <unique-string> on POST /v1/payments. A retry with the same key returns the original result instead of charging twice. Use a fresh key per logical operation, not per retry. Only POST /v1/payments honours the key today. Capture, refund and void do not yet accept one — a repeated call is a second operation, so confirm the first attempt's outcome (GET /v1/payments/{id}) before retrying those.Start here
Run Capabilities first — it tells you what this key may do.
Then work down the folders in order. 01 · Customers & cards gets you a card you can charge; 02 · Payments charges it.
## Where a "payment token" comes from
There are two funding sources, and only the first runs from Postman today:
1. A saved card — no browser, runnable now. A card already in the vault is charged by its PAYMENT_METHOD_ID. In 01 · Customers & cards, run *List customers* then *List saved cards* (they capture CUSTOMER_ID and PAYMENT_METHOD_ID for you), then 02 · Payments → *Charge a saved card*. No token anywhere.
2. A brand-new card — needs a browser. A paymentToken (mtok_…) stands for a card a shopper just typed. You cannot mint one from a card number in Postman — by design the number never reaches a server (PCI). The only producer is the browser flow in 08 · Myela Elements: open a checkout session → the hosted card frame captures the card → *Exchange for a payment token*. The hosted frame (js.myela.com) is not serving yet, so a fresh token cannot be produced from Postman alone right now. The token requests (*Charge a new card*, *Add a card from a token*) are documented and will run the moment the frame is up — until then, use path 1.
Requests that need an id or token they do not have yet are skipped rather than failed, so a run reads cleanly even before you have a saved card.
Capabilities
Lists every operation and whether this key can use it. The only route that is not entitlement-gated.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/capabilities" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/capabilities", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/capabilities",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Customers & cards
A customer and their saved cards are one thing: a card is always stored against a customer, and charging one needs both ids.
Run this folder top to bottom. The customer requests fill in CUSTOMER_ID and the card requests fill in PAYMENT_METHOD_ID, so by the time you reach 02 · Payments you can charge without copying anything.
A card is entered by the cardholder in a browser and stored here as a reusable id — your server never sees the number, and that browser step happens once per card, not once per charge.
Create customer
Only email is required. name falls back to the local part of the email when omitted.
externalId is your own id for this customer, carried alongside ours so you do not have to store a second one. It must be unique among your active customers, and it is write-once: set it here, or later on a customer that has none. Changing one already set is 409.
customFields are your own typed fields on the customer — up to 20 rows of {name, type, value}, and nothing invalid is ever silently dropped: a bad row is a 422 naming customFields[i]. name: 1–40 characters, starting with a letter, then letters/digits/spaces/underscores/hyphens; unique per customer, case-insensitively. type: text | number | boolean | date. value by type: text = string up to 500 characters (no control characters); number = a string-encoded decimal like "12" or "-3.14" (up to 50 characters — never a JSON number, so precision survives); boolean = true/false; date = a real calendar day as YYYY-MM-DD.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/customers" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "{{$guid}}@example.com",
"name": "Ada Lovelace",
"phone": "+15555550123",
"externalId": "cust-{{$guid}}",
"customFields": [
{
"name": "Plan tier",
"type": "text",
"value": "gold"
},
{
"name": "Seats",
"type": "number",
"value": "12"
},
{
"name": "VIP",
"type": "boolean",
"value": true
},
{
"name": "Renewal",
"type": "date",
"value": "2027-01-31"
}
]
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/customers", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"email": "{{$guid}}@example.com",
"name": "Ada Lovelace",
"phone": "+15555550123",
"externalId": "cust-{{$guid}}",
"customFields": [
{
"name": "Plan tier",
"type": "text",
"value": "gold"
},
{
"name": "Seats",
"type": "number",
"value": "12"
},
{
"name": "VIP",
"type": "boolean",
"value": true
},
{
"name": "Renewal",
"type": "date",
"value": "2027-01-31"
}
]
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/customers",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
json={
"email": "{{$guid}}@example.com",
"name": "Ada Lovelace",
"phone": "+15555550123",
"externalId": "cust-{{$guid}}",
"customFields": [
{
"name": "Plan tier",
"type": "text",
"value": "gold"
},
{
"name": "Seats",
"type": "number",
"value": "12"
},
{
"name": "VIP",
"type": "boolean",
"value": True
},
{
"name": "Renewal",
"type": "date",
"value": "2027-01-31"
}
]
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Retrieve customer
The path takes the Myela cus_… id only — your own id here is not_found; look up by your id with ?externalId= on List customers. A customer id from another merchant is also not_found, never permission_denied — confirming existence would leak it.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/customers/{customer_id}" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/customers/{customer_id}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/customers/{customer_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Update customer
POST, not PATCH — the update convention across this API. Only the fields you send change.
externalId is the one exception: it can be filled in here if the customer has none, but never changed once set — that is 409. Re-sending the value it already has is fine and changes nothing.
customFields is replace-on-write: sending it replaces the whole array (same rules as on create), so include every field you want kept. null and [] both clear the list. Omitting the key changes nothing.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/customers/{customer_id}" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Ada King"
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/customers/{customer_id}", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Ada King"
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/customers/{customer_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
json={
"name": "Ada King"
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()List customers
Cursor-paginated.
Enable externalId to look a customer up by your own id. It is an exact match, not a search — so it returns either the one customer or an empty list, which is what makes it usable as a lookup.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/customers?limit=10" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/customers?limit=10", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/customers?limit=10",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Start a session to capture a card
Returns a mountUrl — the address YOUR page mounts the card field from. It is not a page to send a cardholder to, and there is no such page in this API.
⚠️ intent: "store" does not save the card. A checkout session takes no customerId and stores none, so there is no customer for it to save against. It tells the card field what it is capturing for; the token it produces still has to be sent to Add a card from a token (POST /v1/payment_methods) by your server — exactly like an intent: "one_time" token. An earlier version of this collection said the card was saved for you. It never was.
Uses the PUBLISHABLE key, not the secret one, and needs ELEMENTS_ORIGIN set to an origin on that key's allow-list (Settings → API keys in the portal). Without it the answer is 401.
This request must send an Origin header — it is already set to {{ELEMENTS_ORIGIN}}. The route refuses a request without one (no_origin_header) before it looks at anything else, because a browser always states an origin and this stands in for the checkout page. That value must also be registered on the publishable key in the portal (Settings → API keys), or the answer is 401.
⚠️ The session is created correctly, but the page that URL points at is not serving yet. Opening it today will not load card fields. This request is here so the flow is clear and testable end-to-end the moment the page is up.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/checkout_sessions" \
-H "Authorization: Bearer $MYELA_PUBLISHABLE_KEY" \
-H "Content-Type: application/json" \
-d '{
"intent": "store"
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/checkout_sessions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_PUBLISHABLE_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"intent": "store"
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/checkout_sessions",
headers={
"Authorization": f"Bearer {os.environ['MYELA_PUBLISHABLE_KEY']}",
},
json={
"intent": "store"
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Add a card from a token
Turns a single-use paymentToken (mtok_…) from browser capture into a reusable saved card. Needs a customer id (create one at the top of this folder) and a FRESH token — see "Where a payment token comes from" in 00 · Start here. With the hosted frame not serving yet, this runs once Elements is up.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/payment_methods" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customerId": "{{CUSTOMER_ID}}",
"paymentToken": "{{PAYMENT_TOKEN}}"
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payment_methods", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"customerId": "{{CUSTOMER_ID}}",
"paymentToken": "{{PAYMENT_TOKEN}}"
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payment_methods",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
json={
"customerId": "{{CUSTOMER_ID}}",
"paymentToken": "{{PAYMENT_TOKEN}}"
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()List saved cards
The cards saved for one customer, default first. Saves the first card id, so 02 · Payments can charge it without you copying anything.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/payment_methods?customerId={{CUSTOMER_ID}}&limit=10" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payment_methods?customerId={{CUSTOMER_ID}}&limit=10", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payment_methods?customerId={{CUSTOMER_ID}}&limit=10",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Set default card
The card charged when a request names none, including a scheduled invoice.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/payment_methods/{payment_method_id}/default" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payment_methods/{payment_method_id}/default", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payment_methods/{payment_method_id}/default",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Payments
Two funding sources, and only the first needs no browser: a SAVED card by paymentMethodId (start here), or a brand-new card via a single-use paymentToken. 00 · Start here explains where each comes from. Requests SKIP rather than fail while the id or token they need is empty.
The money-moving surface. Amounts are integers in the minor unit — 4999 is $49.99. A decimal is rejected rather than silently charged as a different figure.
A payment is created against exactly ONE funding source: a paymentToken from browser capture, or a stored paymentMethodId. Sending both is refused, because which card was charged is not a question to answer afterwards.
Charge a saved card
The no-browser path — start here. Charges a card already in the vault, referenced by id.
Get the id first: in 01 · Customers & cards, run *List customers* then *List saved cards*, which save CUSTOMER_ID and PAYMENT_METHOD_ID for you. This request SKIPS while PAYMENT_METHOD_ID is empty.
A saved card is charged by REFERENCE. There is no token because there is no new card — the browser step happened once, when the card was vaulted, and does not repeat per charge.
Unlike a token, a payment method id is reusable: run this as many times as you like. On success the new payment's id is saved to PAYMENT_ID, so Void, Refund and Retrieve below run without copy-paste.
Amounts are integers in the minor unit — 4999 is $49.99. A decimal is rejected rather than silently charged as a different figure.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/payments" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"amount": 1299,
"currency": "USD",
"paymentMethodId": "{{PAYMENT_METHOD_ID}}",
"capture": true
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payments", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"amount": 1299,
"currency": "USD",
"paymentMethodId": "{{PAYMENT_METHOD_ID}}",
"capture": true
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import uuid
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payments",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"amount": 1299,
"currency": "USD",
"paymentMethodId": "{{PAYMENT_METHOD_ID}}",
"capture": True
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Charge a new card
capture: true authorizes and captures in one call. Set an Idempotency-Key header on retries: the same key returns the original result rather than charging twice.
PAYMENT_TOKEN must be FRESH. It is single-use and expires in about two minutes; it is produced only by the browser flow in 08 · Myela Elements — see "Where a payment token comes from" in 00 · Start here, so with the hosted frame not serving yet this runs once Elements is up. For a card already in the vault use *Charge a saved card* above, which needs no token at all. On success the new payment's id is saved to PAYMENT_ID, so Void, Refund and Retrieve below run without copy-paste.
Amounts are integers in the minor unit — 4999 is $49.99. A decimal is rejected rather than silently charged as a different figure.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/payments" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"amount": 1299,
"currency": "USD",
"paymentToken": "{{PAYMENT_TOKEN}}",
"capture": true
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payments", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"amount": 1299,
"currency": "USD",
"paymentToken": "{{PAYMENT_TOKEN}}",
"capture": true
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import uuid
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payments",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"amount": 1299,
"currency": "USD",
"paymentToken": "{{PAYMENT_TOKEN}}",
"capture": True
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Authorize a new card (no capture)
Holds funds without taking them. Capture it later, or void it. An uncaptured authorization expires on the provider's schedule, not ours.
Needs its own FRESH PAYMENT_TOKEN — the sale above already spent the previous one.
On success the id is saved to AUTH_ID, which is what Capture below uses. Capture operates on an AUTHORIZATION: a sale created with capture: true is already captured and cannot be captured again.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/payments" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"amount": 2500,
"currency": "USD",
"paymentToken": "{{PAYMENT_TOKEN}}",
"capture": false
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payments", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"amount": 2500,
"currency": "USD",
"paymentToken": "{{PAYMENT_TOKEN}}",
"capture": false
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import uuid
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payments",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"amount": 2500,
"currency": "USD",
"paymentToken": "{{PAYMENT_TOKEN}}",
"capture": False
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Capture an authorization
Takes the funds an authorization is holding. Uses the id from *Authorize a new card*, not a charge that already captured. Omit amount to capture it all; whether a partial capture is allowed depends on the account, so check capabilities.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{auth_id}/capture" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"amount": 2500
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{auth_id}/capture", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"amount": 2500
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import uuid
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{auth_id}/capture",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"amount": 2500
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Void a payment
Cancels a payment that has not settled yet. Once it settles, void is refused and Refund is the right operation.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{payment_id}/void" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{payment_id}/void", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{payment_id}/void",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Refund a payment
Returns money to the original payment method — the same card, on the card rails; not a credit note, and there is no standalone-credit operation. Omit amount to refund it all; partial refunds can be repeated up to the captured total. The response confirms acceptance; completion arrives as the payment.refunded webhook.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{payment_id}/refund" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"amount": 500
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{payment_id}/refund", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"amount": 500
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import uuid
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{payment_id}/refund",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"amount": 500
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Retrieve a payment
The authoritative state. status reflects settlement as reported by the provider, not an optimistic local guess — and settled answers the money-mine-yet question as a boolean (it stays true through later refunds). pending_settlement = approved and captured; the dashboard shows it as Approved.
amount is what was charged. To see an outstanding balance compare amountCaptured against it — an authorization you have not fully captured reports a smaller amountCaptured, and that gap is the open balance.
When the charge was composed of parts, the payment also carries amountBase plus any of amountSurcharge, amountTax and amountTip, which sum to amount. They are ABSENT rather than zero on a plain charge — a surcharge: 0 line is a statement, and it would be false.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{payment_id}" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{payment_id}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payments/{payment_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()List payments
Cursor-paginated — see the collection description. startingAfter takes the id of the last row you saw.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/payments?limit=10" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payments?limit=10", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payments?limit=10",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Addresses
Billing and shipping addresses, held by Myela and mirrored to the provider where it can express them — so an address survives a change of provider.
Create address
Fields are allow-listed: anything else you send is not stored.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/addresses" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customerId": "{{CUSTOMER_ID}}",
"firstName": "Ada",
"lastName": "Lovelace",
"line1": "12 Marylebone Road",
"city": "London",
"postalCode": "NW1 5JD",
"country": "GB"
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/addresses", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"customerId": "{{CUSTOMER_ID}}",
"firstName": "Ada",
"lastName": "Lovelace",
"line1": "12 Marylebone Road",
"city": "London",
"postalCode": "NW1 5JD",
"country": "GB"
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/addresses",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
json={
"customerId": "{{CUSTOMER_ID}}",
"firstName": "Ada",
"lastName": "Lovelace",
"line1": "12 Marylebone Road",
"city": "London",
"postalCode": "NW1 5JD",
"country": "GB"
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()List addresses
Scoped to one customer.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/addresses?customerId={{CUSTOMER_ID}}" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/addresses?customerId={{CUSTOMER_ID}}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/addresses?customerId={{CUSTOMER_ID}}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Retrieve address
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/addresses/{address_id}" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/addresses/{address_id}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/addresses/{address_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Update address
Only the fields you send change.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/addresses/{address_id}" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"city": "Manchester"
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/addresses/{address_id}", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"city": "Manchester"
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/addresses/{address_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
json={
"city": "Manchester"
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Set default address
type is required and is either billing or shipping — a customer has one default of each, so a request that does not say which is refused rather than guessed.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/addresses/{address_id}/default" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "billing"
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/addresses/{address_id}/default", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"type": "billing"
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/addresses/{address_id}/default",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
json={
"type": "billing"
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Plans and subscriptions
A plan is the price and cadence; a subscription binds a customer, a plan and a card. Amounts are integers in the minor unit — 4999 is $49.99. A decimal is rejected rather than silently charged as a different figure.
Create plan
interval is one of day, week, month, year; unknown values are refused here rather than upstream. intervalCount is a positive integer, default 1.
One plan per unique (amount, currency, interval, intervalCount): a matching request returns the existing plan with 200 — its name may differ from the one you sent. 201 means the plan was created.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/plans" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "Standard monthly",
"amount": 4999,
"currency": "USD",
"interval": "month",
"intervalCount": 1
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/plans", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"name": "Standard monthly",
"amount": 4999,
"currency": "USD",
"interval": "month",
"intervalCount": 1
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import uuid
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/plans",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"name": "Standard monthly",
"amount": 4999,
"currency": "USD",
"interval": "month",
"intervalCount": 1
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Retrieve plan
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/plans/{plan_id}" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/plans/{plan_id}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/plans/{plan_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()List plans
Cursor-paginated.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/plans?limit=10" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/plans?limit=10", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/plans?limit=10",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Create subscription
Which calendar day a charge lands on is a timezone question, so timezone is explicit and defaults to UTC rather than being guessed.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customerId": "{{CUSTOMER_ID}}",
"planId": "{{PLAN_ID}}",
"paymentMethodId": "{{PAYMENT_METHOD_ID}}",
"startAt": "2026-09-01",
"timezone": "America/New_York"
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"customerId": "{{CUSTOMER_ID}}",
"planId": "{{PLAN_ID}}",
"paymentMethodId": "{{PAYMENT_METHOD_ID}}",
"startAt": "2026-09-01",
"timezone": "America/New_York"
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
json={
"customerId": "{{CUSTOMER_ID}}",
"planId": "{{PLAN_ID}}",
"paymentMethodId": "{{PAYMENT_METHOD_ID}}",
"startAt": "2026-09-01",
"timezone": "America/New_York"
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Retrieve subscription
nextBillingAt is absent once cancelled — a stale date there is the shape of every "we cancelled but it billed again".
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions/{subscription_id}" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions/{subscription_id}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions/{subscription_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()List subscriptions
Cursor-paginated.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions?limit=10" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions?limit=10", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions?limit=10",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Cancel subscription
Cancels immediately.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions/{subscription_id}/cancel" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions/{subscription_id}/cancel", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/subscriptions/{subscription_id}/cancel",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Invoices and scheduled collection
An invoice can be sent for someone to pay, or scheduled to collect itself from a saved card on a date. The schedule is Myela's own: no provider behind this API has a collect-on-a-date primitive.
Create invoice
Send a total, or the lines that make one. With items the invoice totals itself — quantity × unitAmount, plus amountTax, less amountDiscount — and amount becomes optional; send it anyway and it is checked against that sum rather than trusted. amountTax and amountDiscount adjust that line total, so they require items. Lines come back in the order you sent them.
For the due date, send either dueAt or paymentTerms (due_on_receipt, net_7, net_15, net_30, net_60) — not both. Terms are a derivation: the due date is computed when the invoice is ISSUED, counting calendar days from that moment, so a draft written today and sent next week is due relative to next week. The example below sends dueAt because the two are alternatives and a request cannot demonstrate both. Amounts are integers in the minor unit — 4999 is $49.99. A decimal is rejected rather than silently charged as a different figure.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/invoices" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"customerId": "{{CUSTOMER_ID}}",
"currency": "USD",
"description": "Consulting, August",
"amount": 147375,
"items": [
{
"description": "Consulting, August",
"quantity": 20,
"unitAmount": 7500
}
],
"amountTax": 12375,
"amountDiscount": 15000,
"dueAt": "2026-09-01T00:00:00.000Z"
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/invoices", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"customerId": "{{CUSTOMER_ID}}",
"currency": "USD",
"description": "Consulting, August",
"amount": 147375,
"items": [
{
"description": "Consulting, August",
"quantity": 20,
"unitAmount": 7500
}
],
"amountTax": 12375,
"amountDiscount": 15000,
"dueAt": "2026-09-01T00:00:00.000Z"
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import uuid
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/invoices",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"customerId": "{{CUSTOMER_ID}}",
"currency": "USD",
"description": "Consulting, August",
"amount": 147375,
"items": [
{
"description": "Consulting, August",
"quantity": 20,
"unitAmount": 7500
}
],
"amountTax": 12375,
"amountDiscount": 15000,
"dueAt": "2026-09-01T00:00:00.000Z"
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Retrieve invoice
url is a Myela-hosted payment page on our own domain, safe to send to a cardholder.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()List invoices
Cursor-paginated.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/invoices?limit=10" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/invoices?limit=10", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/invoices?limit=10",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Schedule invoice collection
Charges the named card on that date. Omit paymentMethodId to use the customer's default. Failed attempts retry on a backoff and are visible under Attempts.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/schedule" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"collectAt": "2026-09-01T09:00:00.000Z",
"paymentMethodId": "{{PAYMENT_METHOD_ID}}"
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/schedule", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"collectAt": "2026-09-01T09:00:00.000Z",
"paymentMethodId": "{{PAYMENT_METHOD_ID}}"
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/schedule",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
json={
"collectAt": "2026-09-01T09:00:00.000Z",
"paymentMethodId": "{{PAYMENT_METHOD_ID}}"
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Cancel scheduled collection
Leaves the invoice open but stops it collecting itself.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/cancel_schedule" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/cancel_schedule", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/cancel_schedule",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Collect due invoices now (sandbox)
Sandbox only — returns permission_denied anywhere else, where scheduled invoices collect on their own. Collects your invoices that are due right now, up to a batch limit, and answers {claimed, collected, released}; call it again if claimed came back at the limit.
Use it to see a schedule through to a charge without waiting for its date: schedule with a collectAt in the past, then call this. The charge is real, with the same statuses and Attempts rows a dated collection produces — nothing here is simulated. Calling it twice is safe; a second pass reuses the transaction rather than charging again.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/scheduler/run" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/scheduler/run", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/scheduler/run",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()List collection attempts
Every attempt made to collect it, in order — the answer to "why has this not been paid".
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/attempts" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/attempts", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/attempts",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Void invoice
Terminal. A voided invoice cannot be collected or reopened.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/void" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/void", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/invoices/{invoice_id}/void",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Recurring invoices
Bill a customer on a schedule at an amount that can vary each cycle. Not a subscription: each issues an itemised invoice.
Create recurring invoice
startDate and endDate are calendar dates (YYYY-MM-DD), not timestamps, and they mean dates in YOUR timezone — set it in the portal under Settings → Invoicing, because "the 1st of the month" is a claim about your calendar and not about UTC.
A dayOfMonth of 29, 30 or 31 is honoured as asked: a schedule set to the 31st bills the 28th in February and returns to the 31st in March, rather than quietly becoming a 28th-of-the-month schedule. Use lastDayOfMonth: true when the last day is what you actually mean.
mode defaults to auto_send — each cycle issues the invoice. draft_for_review leaves a draft for you to edit and send instead, which is what you want when the amount needs a human look. Amounts are integers in the minor unit — 4999 is $49.99. A decimal is rejected rather than silently charged as a different figure.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/recurring_invoices" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customerId": "{{CUSTOMER_ID}}",
"name": "Monthly retainer",
"cadence": {
"unit": "monthly",
"interval": 1,
"dayOfMonth": 1
},
"startDate": "2026-09-01",
"template": {
"currency": "USD",
"paymentTerms": "net_30",
"items": [
{
"description": "Retainer",
"quantity": 1,
"unitAmount": 120000
}
]
}
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/recurring_invoices", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"customerId": "{{CUSTOMER_ID}}",
"name": "Monthly retainer",
"cadence": {
"unit": "monthly",
"interval": 1,
"dayOfMonth": 1
},
"startDate": "2026-09-01",
"template": {
"currency": "USD",
"paymentTerms": "net_30",
"items": [
{
"description": "Retainer",
"quantity": 1,
"unitAmount": 120000
}
]
}
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/recurring_invoices",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
json={
"customerId": "{{CUSTOMER_ID}}",
"name": "Monthly retainer",
"cadence": {
"unit": "monthly",
"interval": 1,
"dayOfMonth": 1
},
"startDate": "2026-09-01",
"template": {
"currency": "USD",
"paymentTerms": "net_30",
"items": [
{
"description": "Retainer",
"quantity": 1,
"unitAmount": 120000
}
]
}
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Retrieve recurring invoice
nextInvoiceDate is the next date this produces an invoice, in your timezone. It is absent once a schedule is cancelled or has run its course. occurrenceCount is how many it has issued so far.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/recurring_invoices/{recurring_invoice_id}" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/recurring_invoices/{recurring_invoice_id}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/recurring_invoices/{recurring_invoice_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()List recurring invoices
Cursor-paginated.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/recurring_invoices?limit=10" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/recurring_invoices?limit=10", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/recurring_invoices?limit=10",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Update recurring invoice
Editing the template changes the NEXT cycle only — invoices already issued are untouched. That is what makes the amount variable: it is read at the moment the invoice is created, so there is no window in which a stale figure can be billed.
Changing the cadence re-derives the next date from today rather than continuing from the last one, so an edit mid-cycle can legitimately produce two invoices in one calendar month, under two different billingPeriod values.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/recurring_invoices/{recurring_invoice_id}" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"template": {
"items": [
{
"description": "Retainer",
"quantity": 1,
"unitAmount": 150000
}
]
}
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/recurring_invoices/{recurring_invoice_id}", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"template": {
"items": [
{
"description": "Retainer",
"quantity": 1,
"unitAmount": 150000
}
]
}
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/recurring_invoices/{recurring_invoice_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
json={
"template": {
"items": [
{
"description": "Retainer",
"quantity": 1,
"unitAmount": 150000
}
]
}
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Pause recurring invoice
Stops it producing invoices without ending it. Resuming re-derives the next date from today — a schedule paused for three months does not wake up owing three invoices, because not billing them is what pausing was for.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/recurring_invoices/{recurring_invoice_id}/pause" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/recurring_invoices/{recurring_invoice_id}/pause", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/recurring_invoices/{recurring_invoice_id}/pause",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Resume recurring invoice
Also how you restart a schedule that parked itself after repeated failures.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/recurring_invoices/{recurring_invoice_id}/resume" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/recurring_invoices/{recurring_invoice_id}/resume", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/recurring_invoices/{recurring_invoice_id}/resume",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Cancel recurring invoice
Terminal. Invoices it has already issued are unaffected, and it cannot be restarted — create a new one. There is no delete: an invoice holds a reference back to the schedule that produced it.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/recurring_invoices/{recurring_invoice_id}/cancel" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/recurring_invoices/{recurring_invoice_id}/cancel", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/recurring_invoices/{recurring_invoice_id}/cancel",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Payment links
A payment link collects a fixed amount without billing anything — hand the url to a payer, or encode it as a QR. The page it opens is on a Myela domain, never a provider's.
Distinct from the url on an invoice, which prices an invoice. A link carries its own amount.
Create payment link
Send the url from the response to whoever is paying. expiresAt is optional and defaults to 30 days out; a link always expires, and the expiry is checked when the page is opened rather than by a job, so there is no window where a dead link still pays. Amounts are integers in the minor unit — 4999 is $49.99. A decimal is rejected rather than silently charged as a different figure.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/payment_links" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"amount": 2500,
"currency": "USD",
"description": "Deposit"
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payment_links", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"amount": 2500,
"currency": "USD",
"description": "Deposit"
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import uuid
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payment_links",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"amount": 2500,
"currency": "USD",
"description": "Deposit"
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Retrieve payment link
Once paid, paidAt and paymentId name the payment that settled it.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/payment_links/{payment_link_id}" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payment_links/{payment_link_id}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payment_links/{payment_link_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()List payment links
Cursor-paginated.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/payment_links?limit=10" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payment_links?limit=10", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payment_links?limit=10",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Cancel payment link
Stops the link accepting payment. Terminal — mint a new link rather than reopening this one. Cancelling a link that has already been paid is refused.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/payment_links/{payment_link_id}/cancel" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/payment_links/{payment_link_id}/cancel", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/payment_links/{payment_link_id}/cancel",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Webhook endpoints and deliveries
Where your payment events leave Myela. Entitlements here are separate from the rest of /v1 on purpose: a key that may read invoices should not be able to point that stream somewhere new.
Verify every delivery signature before acting on it — see docs/api/WEBHOOK_SIGNATURE.md.
Create webhook endpoint
The signing secret is returned once, at creation, and never again. An omitted or empty enabledEvents subscribes the endpoint to ALL events.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/myela",
"enabledEvents": [
"invoice.paid",
"payment.settled",
"payment.refunded"
]
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"url": "https://example.com/webhooks/myela",
"enabledEvents": [
"invoice.paid",
"payment.settled",
"payment.refunded"
]
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
json={
"url": "https://example.com/webhooks/myela",
"enabledEvents": [
"invoice.paid",
"payment.settled",
"payment.refunded"
]
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()List webhook endpoints
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Retrieve webhook endpoint
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints/{webhook_endpoint_id}" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints/{webhook_endpoint_id}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints/{webhook_endpoint_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Update webhook endpoint
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints/{webhook_endpoint_id}" \
-H "Authorization: Bearer $MYELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"enabledEvents": [
"invoice.paid"
]
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints/{webhook_endpoint_id}", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"enabledEvents": [
"invoice.paid"
]
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints/{webhook_endpoint_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
json={
"enabledEvents": [
"invoice.paid"
]
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Delete webhook endpoint
Request
curl -X DELETE "https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints/{webhook_endpoint_id}" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints/{webhook_endpoint_id}", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.delete(
"https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_endpoints/{webhook_endpoint_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()List deliveries
Every attempt to reach your endpoint, with the response we got. Cursor-paginated: follow hasMore with startingAfter; the endpointId/status filters apply to the cursor too.
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_deliveries?limit=10" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_deliveries?limit=10", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_deliveries?limit=10",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Retrieve delivery
Request
curl -X GET "https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_deliveries/{webhook_delivery_id}" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_deliveries/{webhook_delivery_id}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.get(
"https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_deliveries/{webhook_delivery_id}",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Replay delivery
Re-sends the FROZEN payload, not a rebuilt one — a replay must deliver what the event said when it happened.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_deliveries/{webhook_delivery_id}/replay" \
-H "Authorization: Bearer $MYELA_API_KEY"const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_deliveries/{webhook_delivery_id}/replay", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_API_KEY}`,
},
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/webhook_deliveries/{webhook_delivery_id}/replay",
headers={
"Authorization": f"Bearer {os.environ['MYELA_API_KEY']}",
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Myela Elements (browser)
The only two routes a merchant's own checkout page calls, and the only two authenticated by the PUBLISHABLE key (myela_pk_) rather than the secret one. Set MYELA_PUBLISHABLE_KEY in the environment; the secret key must never reach a browser.
A session can only be started from an origin on that key's allow-list, which the merchant manages. An empty list refuses every origin — "not configured yet" fails closed, because a checkout that 401s until someone adds a domain is recoverable and one open to the internet is not.
The hosted frame at js.myela.com is not serving yet, so the middle step — mounting the fields and getting an upstream token — cannot be done from these two requests alone. The server contract below is live and can be exercised today.
Create checkout session (embedded)
Opens a checkout session from the merchant's page. intent is one_time (the token will be charged) or store (the token will be attached to a customer with POST /v1/payment_methods — the session itself saves nothing); the whole body is optional and defaults to one_time.
Set ELEMENTS_ORIGIN in the environment to an origin on this key's allow-list — you manage that list in the portal (Settings → API keys). The request sends it as the Origin header, standing in for the checkout page a browser would name; without it (or with an origin not on the list) the answer is 401 before the body is even read.
Returns sessionId, a mountUrl to iframe, and modifiers — your own surcharge/tax/tip configuration, so your page can render a tip control. Nothing in the response names a provider or differs by which one settles the account — that uniformity is the contract, not an implementation detail.
The card frame fetches its own configuration from the session id; it does not pass through your page. That is why nothing here identifies a provider even indirectly.
401 means the Origin header is missing or not on this key's allow-list.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/checkout_sessions" \
-H "Authorization: Bearer $MYELA_PUBLISHABLE_KEY" \
-H "Content-Type: application/json" \
-d '{
"intent": "one_time"
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/checkout_sessions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_PUBLISHABLE_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"intent": "one_time"
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/checkout_sessions",
headers={
"Authorization": f"Bearer {os.environ['MYELA_PUBLISHABLE_KEY']}",
},
json={
"intent": "one_time"
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()Exchange for a payment token
Exchanges the provider token the fields produced for an opaque mtok_, which is what POST /v1/payments accepts as paymentToken.
The session id is the capability — there is no key on this call beyond the publishable one. A session exchanges ONCE; a second attempt is refused, and so is a replay of the resulting mtok_.
Both variables come from the frame, so this request cannot be driven from the collection alone until js.myela.com is serving.
Request
curl -X POST "https://payments-api-sandbox.merchantservicedepot.com/v1/tokens" \
-H "Authorization: Bearer $MYELA_PUBLISHABLE_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "{{CHECKOUT_SESSION_ID}}",
"upstreamToken": "{{UPSTREAM_TOKEN}}"
}'const res = await fetch("https://payments-api-sandbox.merchantservicedepot.com/v1/tokens", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MYELA_PUBLISHABLE_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"sessionId": "{{CHECKOUT_SESSION_ID}}",
"upstreamToken": "{{UPSTREAM_TOKEN}}"
}),
});
if (!res.ok) {
const { error } = await res.json();
// Branch on error.code — never on error.message.
throw new Error(error.code);
}
const data = await res.json();import os
import requests
res = requests.post(
"https://payments-api-sandbox.merchantservicedepot.com/v1/tokens",
headers={
"Authorization": f"Bearer {os.environ['MYELA_PUBLISHABLE_KEY']}",
},
json={
"sessionId": "{{CHECKOUT_SESSION_ID}}",
"upstreamToken": "{{UPSTREAM_TOKEN}}"
},
)
if not res.ok:
# Branch on error["code"] — never on error["message"].
raise RuntimeError(res.json()["error"]["code"])
data = res.json()