Developers
Sales API · v1

Report a sale. Trendly does the rest.

Send Trendly each order. It matches the order to the creator whose code brought it in, works out what they earned, holds it through your refund window and pays it out on schedule. Codes, commission, clawbacks, statements and both dashboards follow from that one request.

One required endpoint

POST /v1/orders. Everything else is optional.

Test keys store orders

Marked as test, kept out of every figure and payout.

Safe to retry

Matched on your order id. Nobody gets paid twice.

OVERVIEW

What this API is for

A brand runs an affiliate program on Trendly. Creators join, the brand approves them, and each approved creator gets a personal discount code. When a customer uses that code in your store, the creator earns commission, and Trendly pays it out on the schedule the brand sets.

Trendly needs one thing from you: the orders.

What you build

One call to POST /v1/orders when an order is placed, and again whenever it changes. That is the integration. Approvals, codes, commission rules, caps, hold periods, clawbacks, payout runs, statements and both dashboards are handled for you.

Two rules apply to everything you send.

RULEWHY
Money is a decimal string. "240.00", never 240.00.JSON numbers are doubles. 240.10 round-trips as 240.09999999999999. Across a few thousand orders a month that becomes a gap between your books and ours that nobody can trace.
Writes are matched on your order id.Retry freely. Replay a whole day. Run a webhook and a nightly job side by side. The same order is stored once and earns commission once. That is a database constraint, not careful code.

Not writing code?

If your affiliate program runs on Impact.com, Trendly reads the sales itself: see Impact.com. If your site runs on Cloudflare, the Cloudflare Worker reports sales for you and tracks creator links as well.

QUICKSTART

From nothing to a working call

  1. Step 1: Get a test key

    In the dashboard, open Developers, API keys and create a test key. It starts with trk_test_ and is shown once, so copy it somewhere safe. No Trendly login? Your Trendly contact can issue one.

  2. Step 2: Check it works

    GET /v1/ping says which program the key reached and the three settings that decide what a creator takes home.

    cURL

    curl https://api.trendly.com.sa/v1/ping \
      -H "Authorization: Bearer trk_test_YOUR_KEY"

    Response

    {
      "ok": true,
      "environment": "test",
      "key_name": "Calo checkout",
      "scopes": ["codes:read", "orders:read", "orders:write", "program:read"],
      "program": {
        "id": "3b0c8f14-0a2e-4c77-9b31-6f5f2a1d4e88",
        "name": "Calo Market Partners",
        "currency": "SAR",
        "status": "active",
        "commission_basis": "net_ex_vat_ex_shipping",
        "hold_period_days": 14,
        "payout_cycle_days": 30
      },
      "server_time": "2026-09-18T09:14:22Z"
    }
  3. Step 3: Report an order

    This is the whole integration.

    cURL

    curl -X POST https://api.trendly.com.sa/v1/orders \
      -H "Authorization: Bearer trk_test_YOUR_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "order_id": "ord_1001",
        "discount_code": "SARA10",
        "status": "confirmed",
        "currency": "SAR",
        "subtotal_amount": "240.00",
        "discount_amount": "24.00",
        "shipping_amount": "15.00",
        "tax_amount": "34.65",
        "total_amount": "265.65",
        "placed_at": "2026-09-08T11:42:03+03:00"
      }'

    Response

    {
      "mode": "test",
      "recorded": true,
      "created": true,
      "updated": false,
      "order_id": "ord_1001",
      "status": "confirmed",
      "attribution": {
        "attributed": true,
        "code": "SARA10",
        "creator_name": "Sara A.",
        "membership_id": "9c1e77a2-4b3d-4e91-8f22-11c0a7d5b6e3"
      },
      "commission": {
        "basis_amount": "216.00",
        "basis": "net_ex_vat_ex_shipping",
        "rate_type": "percentage",
        "rate_value": "10.00",
        "rate_source": "program_default",
        "amount": "21.60",
        "currency": "SAR",
        "capped": false,
        "payable_after": "2026-09-22T08:42:03Z"
      },
      "warnings": [
        "Recorded as a test order. It appears in Trendly marked as test, and is excluded from the program's figures and from every payout. Send the same request with a trk_live_ key to record it for real."
      ]
    }

    Check the commission block

    basis_amount is what the rate was applied to: 240.00 of goods minus the 24.00 discount, because this program leaves out VAT and shipping. rate_source says which rate won, and payable_after is when the hold period ends. These are the numbers that get disputed at month end, so check them now against an order you can work out by hand.

    A test key stores the order marked as test. Open the program's Orders in the dashboard and you will see it there, left out of every figure and payout.

  4. Step 4: Go live

    Create a live key (trk_live_) and swap it in. The same request now counts for real. That is the only difference between the two environments.

AUTHENTICATION

Keys

Send your key on every request as a bearer token. If your gateway strips the Authorization header, send the same value as X-API-Key instead.

Either header

Authorization: Bearer trk_live_7Qa4xN2pLmK8vR3sT1wY6z
X-API-Key: trk_live_7Qa4xN2pLmK8vR3sT1wY6z
KEYWHAT IT DOES
trk_test_…Validates, attributes and stores the order, marked as test. Same parser, code lookup and commission calculation as live. Test orders show in Trendly and are left out of every figure and payout. The brand can clear them from the dashboard at any time.
trk_live_…Counts for real. Creators get paid from these calls.

Keys are shown once

Trendly stores a hash, not the key, so nobody can read it back to you. If you lose one, rotate it. Rotating issues a replacement and leaves the old key working, so you can deploy the swap with no downtime and revoke the old key afterwards.

Scopes

A key carries some or all of four scopes. A new key gets all four unless you narrow it. Use codes:read on its own for anything that runs in a checkout: that is the most exposed place a key lives, and a key that can only answer "is this code valid?" is worth little to whoever takes it.

SCOPEGRANTS
orders:writeReport orders, one at a time or in batches. This is the one that moves money.
orders:readRead back an order and its commission.
codes:readList codes, read one, and validate one against a cart.
program:readReading the program's settings. GET /v1/ping returns them to any valid key today.

Response headers

HEADERMEANS
X-Request-IdThe id of this call. Also in every error body as request_id. Quote it to Trendly, or look the call up with GET /v1/events/{request_id}.
X-Trendly-Environmenttest or live, after the key is accepted. A quick way to see which key a deployed service is really using.

PROGRAMS

Which program a call is about

A key belongs to your Trendly account, and an account can run more than one affiliate program. Every call lands in exactly one program, chosen in this order.

WHENTHEN
1. The key is pinned to a programEvery call uses that program. You send nothing. Naming a different program returns 403 program_not_allowed.
2. The call names a programprogram_id in the body (orders and batches), ?program= on the query string, or an X-Trendly-Program header. A program your account cannot reach returns 404.
3. The account has one programThat one. Most integrations never send a program id.

Otherwise the call is refused with 400 program_required, and the hint lists your programs with their ids. An account with no program yet gets 400 no_program.

Naming the program

# In the body, for POST /v1/orders and /v1/orders/batch:
#   { "program_id": "3b0c8f14-0a2e-4c77-9b31-6f5f2a1d4e88", "order_id": "ord_1001", ... }
# Or as a header, on any call:

curl https://api.trendly.com.sa/v1/codes \
  -H "Authorization: Bearer trk_live_YOUR_KEY" \
  -H "X-Trendly-Program: 3b0c8f14-0a2e-4c77-9b31-6f5f2a1d4e88"

# Or on the query string:
curl "https://api.trendly.com.sa/v1/codes?program=3b0c8f14-0a2e-4c77-9b31-6f5f2a1d4e88" \
  -H "Authorization: Bearer trk_live_YOUR_KEY"

Find the id with ping

GET /v1/ping returns the program a key reached, with its id. Call it with the same header or query string you plan to use and check the name.

ORDERS

Reporting an order

POST

/v1/orders

Call this when an order is placed, and again whenever it changes: confirmed, cancelled, refunded, partially refunded, flagged. Same call, same order_id. Trendly matches on it and updates the order it already has.

Reporting changes is not optional

If you report an order as confirmed and never update it, Trendly pays commission on it after a refund you knew about. This is the most common way an affiliate integration loses a merchant money.

Fields

FIELDREQUIREDNOTES
order_idYesYour permanent order identifier. Never reused. This is what makes a retry safe.
statusYesOne of the six under Order statuses. Anything else returns 400.
subtotal_amountYesGoods value before discount, without VAT and shipping.
total_amountYesWhat the customer actually paid.
discount_codeStronglyThe code as applied. Matched without regard to case. Without it the order is stored but belongs to nobody.
currencyStronglyISO 4217. Defaults to the program's currency, with a warning. An order in another currency returns 400 currency_mismatch rather than being converted.
discount_amountStronglyWhat the code took off. Part of the commission basis under most program settings.
shipping_amountStrongly"0.00" if none.
tax_amountStronglyVAT charged.
placed_atStronglyISO 8601 with an offset. Needed for backfills. Without it Trendly uses the time of the call.
refunded_amountOn refundTotal refunded so far, not the latest refund.
status_changed_atOn updatesWhen the status changed on your side. An update older than what Trendly holds is ignored. updated_at works too.
order_numberNoHuman-readable reference, shown in support screens.
customer_refNoPseudonymous only. See Privacy.
is_new_customerNoTrue for the customer's first order with you. A program that pays more for a new customer pays that rate on it; false or absent earns the standard rate. Also drives the new and returning split in the dashboard.
commissionable_amountNoOnly read when the program's commission basis is merchant_declared.
program_idSometimesOnly when your key can reach more than one program. See Which program.
metadataNoAny object. Stored with the order, never parsed.

Common aliases are accepted

id for order_id; code, coupon, coupon_code or promo_code for discount_code; total for total_amount; subtotal for subtotal_amount. Unknown fields are ignored, so your own extras will not break the call.

Warnings

A successful response may include a warnings array. The order was accepted, so never retry because of a warning, but each one is worth fixing. Trendly warns when amounts do not add up to subtotal − discount + shipping + tax = total, when a timestamp has no UTC offset (Asia/Riyadh is assumed), when the currency was missing, when placed_at was missing, and when a refunded order arrives without a refunded amount.

STATUSES

Order statuses

Map your own statuses onto these six and send your Trendly contact the mapping. confirmed matters most: it starts the clock on a creator's earnings.

STATUSMEANSEFFECT ON COMMISSION
pendingPlaced, not yet paid or acceptedNothing accrues
confirmedPaid and accepted. The state that counts.Accrues, held until the hold period ends
cancelledCancelled before fulfilmentReversed
refundedFully refunded after fulfilmentReversed in full
partially_refundedPartly refundedReversed in proportion
fraudFlagged as fraudulentReversed. The creator is flagged for review.

Unknown statuses return 400

Trendly does not guess. Mapping an unknown status to something plausible is how orders stop earning with no error anywhere, until a creator asks why their earnings stalled two months ago.

REFUNDS

Refunds and cancellations

Send the same order_id with the new status. Commission is held through the program's hold period before it can be paid, so most refunds arrive before any money moves.

Partial refund

# Same order_id, new status. Trendly reverses the refunded share of the commission.
# If it was already paid out, the reversal comes off the creator's next payout.

curl -X POST https://api.trendly.com.sa/v1/orders \
  -H "Authorization: Bearer trk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "order_id": "ord_1001",
    "status": "partially_refunded",
    "currency": "SAR",
    "subtotal_amount": "240.00",
    "discount_amount": "24.00",
    "shipping_amount": "15.00",
    "tax_amount": "34.65",
    "total_amount": "265.65",
    "refunded_amount": "120.00",
    "placed_at": "2026-09-08T11:42:03+03:00",
    "status_changed_at": "2026-09-14T10:02:00+03:00"
  }'

If a refund arrives after a payout has gone out, nothing is rewritten. The original commission stands and a reversal is added, which comes off the creator's next payout. That is what keeps a statement from three months ago reconciling.

Order of delivery does not matter

Send status_changed_at with each update. An update that describes an older state than the one Trendly holds is ignored, so a late retry cannot undo a refund. Sending the same update twice changes nothing.

BATCH

Batches and backfill

POST

/v1/orders/batch

Up to 500 orders per call, for backfills and nightly reconciliation. Every order gets its own result, so one malformed row does not reject the rest, and the response names the row to fix. A batch belongs to one program: put program_id at the top level of the body if your key needs one.

Request

# For backfills and nightly reconciliation. Up to 500 orders per call.
# Each order gets its own result, so one bad row does not reject the rest.

curl -X POST https://api.trendly.com.sa/v1/orders/batch \
  -H "Authorization: Bearer trk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "orders": [
      {
        "order_id": "ord_1002",
        "discount_code": "SARA10",
        "status": "confirmed",
        "currency": "SAR",
        "subtotal_amount": "180.00",
        "discount_amount": "18.00",
        "shipping_amount": "0.00",
        "tax_amount": "24.30",
        "total_amount": "186.30",
        "placed_at": "2026-09-09T19:05:00+03:00"
      },
      {
        "order_id": "ord_1003",
        "discount_code": "NOURA10",
        "status": "pending",
        "currency": "SAR",
        "subtotal_amount": "95.00",
        "total_amount": "SAR 95",
        "placed_at": "2026-09-09T21:40:00+03:00"
      }
    ]
  }'

The response is 200 whenever the batch itself was read, even if some orders were rejected. Each entry carries orderId and an HTTP-style status, then either the result a single call would have returned, attribution and commission included, or errorCode, message and hint. The entry's own keys are camelCase; the keys inside result are snake_case, as in the single-order answer.

Response

{
  "mode": "live",
  "accepted": 1,
  "rejected": 1,
  "results": [
    {
      "orderId": "ord_1002",
      "status": 200,
      "result": {
        "mode": "live",
        "recorded": true,
        "created": true,
        "updated": false,
        "order_id": "ord_1002",
        "status": "confirmed",
        "attribution": { "attributed": true, "code": "SARA10", "creator_name": "Sara A.", "membership_id": "9c1e77a2-4b3d-4e91-8f22-11c0a7d5b6e3" },
        "commission": { "basis_amount": "95.00", "basis": "net_ex_vat_ex_shipping", "rate_type": "percentage", "rate_value": "10.00", "rate_source": "program_default", "amount": "9.50", "currency": "SAR", "capped": false, "payable_after": "2026-09-23T21:40:00Z" }
      }
    },
    {
      "orderId": "ord_1003",
      "status": 400,
      "errorCode": "invalid_amount",
      "message": "'total_amount' is not a decimal amount: SAR 95",
      "hint": "Send money as a decimal string. Use \"95.00\", not 95.0 and not \"SAR 95\"."
    }
  ]
}

More than 500 orders returns 400 batch_too_large, and an empty list returns 400 empty_batch.

Backfilling is safe

Replay as far back as you like. Orders already stored come back to the same row and no commission is written twice. An order keeps the creator it was first credited to: a resend updates its status and amounts, never who it is credited to, even if it carries another code. You do not need to track where a previous run stopped.

CODES

Discount codes

GET

/v1/codes

When the brand approves a creator, Trendly issues their code as pending: the code exists in Trendly, but a customer cannot use it until it exists at your checkout. Your part is three calls, and until you make the last one the creator is told their code is on its way, not the code itself, so nobody shares a code that fails at checkout.

  1. Step 1: List the codes to add

    GET /v1/codes?status=pending returns each code with its discount and creator.

  2. Step 2: Create them in your discount engine

    Use the code text exactly, with its discount_type and discount_value.

  3. Step 3: Confirm each one

    POST /v1/codes/{code}/confirm. The code goes live, any orders that already used it are credited to its creator, and the creator is told it is ready to share. Confirming twice is harmless. A real order reported with a pending code confirms it too.

The three calls

# 1. The codes Trendly issued to newly approved creators, not yet in your checkout.
curl "https://api.trendly.com.sa/v1/codes?status=pending&limit=200" \
  -H "Authorization: Bearer trk_live_YOUR_KEY"

# 2. Create each one in your discount engine, with its discount_type and discount_value.

# 3. Tell Trendly it exists. The code goes live and its creator is told it is ready.
curl -X POST https://api.trendly.com.sa/v1/codes/SARA10/confirm \
  -H "Authorization: Bearer trk_live_YOUR_KEY"
POST

/v1/codes/{code}/confirm

Needs codes:read, the same key that lists the codes. Answers with the code, now active. A code that is disabled or failed cannot go live and answers 409 code_not_confirmable. If nobody on the brand's side writes code, the dashboard does the same: each program's Creators tab lists the codes to add, with Mark added.

Keeping your engine in step

To mirror every change (new codes, and codes switched off when a creator leaves or a code is replaced), poll GET /v1/codes with updated_since set to the time of your last successful run.

Request

# Mirror Trendly's codes into your own discount engine.
# updated_since, not a creation date: a code disabled this morning comes back
# even though it was created months ago.

curl "https://api.trendly.com.sa/v1/codes?updated_since=2026-09-01T00:00:00%2B03:00&limit=200" \
  -H "Authorization: Bearer trk_live_YOUR_KEY"

updated_since, not a creation date

Results come back oldest change first, so a code disabled this morning is returned even though it was created months ago. Filter on creation date instead and you never learn a code was switched off, so a suspended creator keeps earning.

limit defaults to 100, up to 200. When has_more is true, pass next_cursor back unchanged as cursor. The cursor is stable, so codes changing while you page cannot make you skip or repeat one.

Kinds

KINDMEANING
memberA creator's permanent personal code. Shared publicly, no usage limit.
campaignA code for one campaign, usually at a better discount than the personal one, often with an end date.
gift_single_useA one-time code for a free product on a campaign. Treat it as a separate object. Handling it like a member code is how a free-product code ends up posted to 40,000 followers.

Filter with ?kind=, or leave it out to get all three. Filter by state with ?status=.

Statuses

STATUSMEANING
pendingIssued to an approved creator, waiting for you. Create it in your engine with its discount, then confirm it.
activeLive at checkout. If you mirror codes, it exists in your engine.
disabledIts creator was removed from the program, or the code was replaced. It stays in this list. Switch it off in your engine too.
failedCould not be created in the brand's store. Validation reports it as not_provisioned.
GET

/v1/codes/{code}

One code, by its text. Returns 404 not_found if the program has no such code.

CODES

Where codes come from

Every approved creator gets one personal code. It can start in Trendly or in your own coupon engine, and you can mix the two within a program.

WHO MAKES ITHOWWHAT YOU BUILD
TrendlyWhen the brand approves a creator, Trendly makes their code from the program's code template, usually from the creator's handle.List GET /v1/codes?status=pending, create each code in your engine, then confirm it with POST /v1/codes/{code}/confirm. Or check codes live at checkout with POST /v1/codes/validate.
Your coupon engineYou create the code in your own engine, then register it on the creator in the Trendly dashboard.Nothing in the API. Orders that already used the code are credited to the creator when it is registered.

A code Trendly does not know yet

An order with a code that matches nobody is still stored, as unattributed. The brand sees it in the program's Unattributed list and can assign the code to a creator, which credits every order that code already brought in. Nothing is lost by reporting an order early.

Replacing a code

The brand can reissue a creator's code from the dashboard. The old code is disabled, not deleted, and stays in GET /v1/codes with status disabled. Orders keep the code that brought them in, so last month's figures do not move when a code is replaced today. If you mirror codes, switch off the disabled ones.

Removing a creator from the program disables every code they hold the same way: their personal code and any code a campaign gave them. A code's text is unique within a program across every status, disabled included, so it is never issued again, to anyone. A returning creator or a replaced code always gets new text.

The API does not make or change codes

Trendly picks each code and its discount; there is no endpoint to create, rename or reprice one, and no codes:write scope. Confirming only says a code now exists at your checkout. Codes are replaced in the dashboard and reach you through GET /v1/codes.

CHECKOUT

Validating a code at checkout

POST

/v1/codes/validate

If your checkout does not keep its own copy of the codes, ask Trendly in real time. Send the code and, if you have it, the cart total as cart_total. The answer says whether the code can be used and how much to take off.

cURL

curl -X POST https://api.trendly.com.sa/v1/codes/validate \
  -H "Authorization: Bearer trk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "code": "SARA10", "cart_total": "240.00" }'

Always 200, even for a bad code

A rejected coupon is a normal checkout outcome, not an error. You get valid: false with a reason to branch on and a message you can show the shopper. A 404 would mean wrapping this in a try/catch, which is how a network blip ends up telling a real customer their real code is bad.

Each rejection has its own reason: unknown_code, disabled, not_provisioned, not_yet_valid, expired, below_minimum. A shopper who mistyped one character and one whose code genuinely ran out should not be sent down the same path.

Use the discount_amount in the answer rather than working out your own. It is rounded half up to the halala. Rounding the other way puts you a halala out on some orders, which shows up later as a reconciliation gap nobody can explain.

ERRORS

When something is wrong

Every failure has the same shape, including authentication failures.

400 Bad Request

{
  "error": {
    "code": "invalid_amount",
    "message": "'total_amount' is not a decimal amount: SAR 240",
    "field": "total_amount",
    "hint": "Send money as a decimal string. Use \"240.00\", not 240.0 and not \"SAR 240\". JSON numbers are doubles and lose halalas at volume.",
    "documentation_url": "https://trendly.com.sa/developers/affiliate#orders"
  },
  "request_id": "8f14d2a1-7c3b-4e55-9a20-3d6b1f0c9e47"
}

Branch on error.code; it is stable. error.message and error.hint are written for whoever reads your logs, error.field names the part of your payload that is wrong, and documentation_url links to the section of this page that explains it.

HTTPMEANSRETRY?
400The request cannot be usedNo. Fix it and resend.
401Key missing, malformed, unknown, revoked or expiredNo. The code says which. Fix the key.
403Valid key, not allowed to do thisNo. Use a key with the right scope or program.
404No such object for this keyNo. Check the id, the key and the program.
409Idempotency-Key reused with a different bodyNot with that key. Use a new one.
5xxTrendly failedYes. Every write is safe to repeat. Quote the request id if it persists.

Error codes

ERROR.CODEHTTPWHAT TO DO
invalid_json400The body is not valid JSON, or a field has the wrong JSON type. Check for trailing commas and unquoted money.
empty_body400Send a JSON object describing one order.
missing_field400Add the field named in error.field. order_id, status, subtotal_amount and total_amount are required.
invalid_amount400Send money as a decimal string: "240.00".
negative_amount400Amounts are never negative. Report a refund with a refund status and a positive refunded_amount.
amount_too_large400At most ten digits before the decimal point.
invalid_timestamp400Use ISO 8601 with an offset, such as 2026-09-08T11:42:03+03:00. In a query string, encode + as %2B.
unknown_status400Use one of the six order statuses.
currency_mismatch400The order's currency is not the program's. A program settles in one currency; orders are not converted.
pii_in_customer_ref400Send a hash or an internal id, not an email or phone number. See Privacy.
order_rejected400The order could not be stored. The message says why; fix it and resend.
empty_batch400Send {"orders": [ ... ]} with at least one order.
batch_too_large400Split it into pages of 500.
program_required400Your account has several programs. Name one; see Which program.
no_program400The account has no affiliate program yet. Create one in Trendly; the key is fine.
invalid_program400Program ids are UUIDs.
invalid_kind400Use member, campaign or gift_single_use, or leave kind out.
invalid_cursor400Pass next_cursor back exactly as you got it.
invalid_parameter400A query parameter is the wrong type. Check it against the docs.
idempotency_key_too_long400Keep Idempotency-Key to 128 characters. A UUID is ideal.
missing_credentials401Send the key as Authorization: Bearer trk_… or X-API-Key.
malformed_token401Keys start with trk_live_ or trk_test_. Check you did not send a dashboard session token or half a key.
invalid_token401No such key. Check it was copied whole and has not been rotated away.
revoked_token401The key was revoked. Create a new one and swap it in.
expired_token401The key expired. Create a replacement.
insufficient_scope403Use a key with the scope named in the message.
program_not_allowed403This key is pinned to another program. Drop program_id, or use a key that is not pinned.
not_found404No such order, code, request or program for this key. Check you are using the right key and program.
idempotency_key_reused409The Idempotency-Key was used before with a different body. Use a new key for a new request.
internal_error500Trendly failed. Safe to retry. If it keeps failing, send the request id.

Idempotency

Optional. Send an Idempotency-Key header (up to 128 characters) on a write and a retry with the same key and the same body gets the original response back. The same key with a different body returns 409. Writes are already matched on order_id, so this is for your retry logic, not for the ledger: without it a retry still cannot pay twice, it just reports created: false the second time.

TESTING

Testing, and what go-live looks like

A test key runs the same code as a live one: the same parser, code lookup, commission basis and rate chain. There is no fixture data and no mock. A clean 200 from a test key proves your payload works, not that a sandbox was willing to accept it.

Test orders are stored and appear in Trendly marked as test. Send one, then open the program's Orders in the dashboard and look at it. Test orders are left out of the program's figures and every payout, and the brand can clear them from Developers whenever they want a clean slate.

Replay from the dashboard

Every order call shows up in the request log with its full request and response. If one fails, fix your mapping and press Replay. The same payload is sent again with the same key, and both attempts stay in the history.
GET

/v1/events

Your own call history for the last 30 days, with request and response bodies, for the key's environment. It is the same log the brand sees in Trendly, reachable with your key, so you do not need a Trendly login to debug your integration. Filter with failures_only=true and search; limit defaults to 25. To page back, pass the next_before you got as before.

Your last ten failures

curl "https://api.trendly.com.sa/v1/events?failures_only=true&limit=10" \
  -H "Authorization: Bearer trk_test_YOUR_KEY"
GET

/v1/events/{request_id}

One call in full, by the id from its X-Request-Id header.

Before you switch to a live key

CHECKWHY IT MATTERS
An order reports and attributes to the right creatorConfirms your code field reaches Trendly and matches.
The commission block matches what you expectAgree the basis, rate and rounding while it is still cheap to change.
A cancellation reverses itProves your change events reach Trendly, not just your new orders.
A partial refund reverses in proportionThe case most integrations forget.
The same order sent twice changes nothingProves your retries are safe.
An order placed at 23:50 lands on the right dayCatches timestamps sent without an offset.
A lowercase code still attributesCustomers do not type codes the way your admin stores them.

Run one cycle without paying

Before real money moves, run a full payout cycle in parallel and reconcile Trendly's totals against your finance team's. A gap found in a practice cycle costs nothing. The same gap found during a real payout costs a week.

PRIVACY

What Trendly does and does not want

Trendly does not want customer personal data. No names, emails, phone numbers or addresses, in any field. A customer_ref that looks like an email or a phone number is rejected with 400 pii_in_customer_ref, not stored.

customer_ref must be pseudonymous: a hash or an internal key, the same for each customer and meaningless outside your system. It is used for the new and returning split and to spot a creator buying through their own code. Leave it out if you prefer; you lose those two features and nothing else.

Order data reaches the brand that owns the program. The creator whose code was used sees their own orders only: date, status, order value and their commission. Never another creator's orders, and never anything about the customer. Keys are stored hashed and kept out of logs.

REFERENCE

Everything, in one place

ENDPOINTSCOPEDOES
GET /v1/pingAny keyCheck a key and read the program it reached.
POST /v1/ordersorders:writeReport one order. The only endpoint you must use.
POST /v1/orders/batchorders:writeReport up to 500, with a result for each.
GET /v1/orders/{order_id}orders:readRead back what Trendly stored, with its commission.
GET /v1/codescodes:readList codes to mirror into your store.
GET /v1/codes/{code}codes:readRead one code.
POST /v1/codes/{code}/confirmcodes:readSay a pending code now exists at your checkout.
POST /v1/codes/validatecodes:readCheck a code against a cart, at checkout.
GET /v1/eventsAny keyYour own call history, with bodies.
GET /v1/events/{request_id}Any keyOne call in full.
GET /v1/openapi.jsonNo keyThe machine-readable spec.

Something not covered here?

Send your Trendly contact the request id from a failing call. Trendly can then see exactly what you sent and exactly what came back, which usually settles it in one message. The full contract is in the OpenAPI spec, where the Sales API goes by its older name, the Trendly Partner API.