Returning.AIDocs
v1

API reference / Gamification / Loyalty Points

.md

Create loyalty points export

Request a CSV or JSON file of recorded reward-coin history for a date range.

Last updated 12 Oct 2026API v1

Method
POST
Path
https://api.returning.ai/v1/loyalty-exports
Permission
customerSuccess
Retries
Retry uncertain requests with the same API key, Idempotency-Key and body.

When to use this

  • Load daily reward-coin history into your reporting system.
  • Extract a historical period across all customers or selected customer identifiers.

Authentication#

Header
Authorization: Bearer <API_KEY>
Permission
customerSuccessShown in the dashboard as Customer Success.

Use a Community API key with customerSuccess, shown as Customer Success in the dashboard. Keep the key on your backend and send Authorization: Bearer <API_KEY>. The key selects the community; do not send a community ID. Personal API keys are not supported.

All three calls use https://api.returning.ai and return Cache-Control: no-store. They accept no query parameters. A valid community key with the required permission can read that community's export jobs, including jobs created by another key in the same community.

Successful responses contain meta, message and data. Read job state from data.status. An HTTP 200 means the status request succeeded, even when the job is failed. Errors put their machine-readable code in meta.code; do not branch on message.

Behaviour#

Request export -> save export_id -> poll status -> get download link -> download the file.

This creates a report job and does not change balances. The dataset excludes XP and premium currency. It is not a store-orders feed, current-balance snapshot or pre-calculated BI totals.

Start here: a daily export

You need a backend process, a Community API key with Customer Success permission, and somewhere private to store the downloaded file. The examples on this page use synthetic dates and identifiers; choose a completed period that contains your own activity.

StepCallSave or check
1. Request the filePOST /v1/loyalty-exportsSave your request body and idempotency key before sending. Save data.export_id from the response.
2. Wait for completionGET /v1/loyalty-exports/{exportId}Continue only for queued or processing. Download only after completed.
3. Get a temporary linkGET /v1/loyalty-exports/{exportId}/downloadUse data.url promptly. Do not log it.
4. Download and importHTTPS GET to data.urlSend no API key. Check row count and byte size against status, then import.

The three API calls need your key. The file download uses the temporary URL instead. There is no webhook, list-jobs or public cancel call in this three-endpoint workflow, so retain the export ID yourself and poll status.

Choose the date window

The filter uses when each coin-history entry was recorded, not when its original trade or activity happened. start_at is included and end_at is excluded. Both must be UTC timestamps ending in Z, with at most three fractional-second digits.

Reporting daystart_atend_at
1 September in UTC2026-09-01T00:00:00.000Z2026-09-02T00:00:00.000Z
1 September in a UTC+08:00 business timezone2026-08-31T16:00:00.000Z2026-09-01T16:00:00.000Z

For a timezone that observes daylight saving, convert each local midnight separately; a local day can be 23 or 25 hours. The API does not accept a timezone parameter. Run your daily process after the interval closes: an end time in the future is rejected.

Adjacent days share one boundary. A record exactly at 2026-09-02T00:00:00.000Z belongs to the next UTC day, not both. Historical backfills can use wider intervals, but use sequential windows for easier recovery. There is no maximum range duration in the request validator and no promised completion time or unlimited file size.

Copy-and-run CSV example

Download the complete Python example. It requires Python 3.10 or newer and no third-party packages. Supply your API key through your backend's environment or secret manager as RETURNING_API_KEY.

BASH
python3 export-loyalty-points.py \
  --start '2026-09-01T00:00:00.000Z' \
  --end '2026-09-02T00:00:00.000Z' \
  --state export-2026-09-01.state.json \
  --out points-2026-09-01.csv

Omit --client-ref to include all customers. To select customers, add --client-ref '000101' --client-ref '000102'. For a single date range, use the same selection on every resume.

The example persists an idempotency key before creation, saves the export ID, polls with backoff, downloads without forwarding the API key, and checks the CSV header, row count and byte size. It streams CSV to a temporary file and replaces the output only after those checks pass. An empty report still produces a valid header-only CSV.

If it times out, rerun the same command with the same state file and API key. The default ten-minute polling budget is a client choice, not an API guarantee. --max-wait 1200 changes that budget. If a request is accepted but the response is lost, the saved key lets the next run recover the original job.

Use one process per state file and serialize exports for each community. Keep state and CSV files private. Use a distinct state file for each deliberately new report; deleting state or changing API keys after an uncertain request can create a different job. The example is a starting point for a backend process, not a scheduler or warehouse importer. It replaces the chosen output file after verification, so choose an export-specific output path.

Manual status and download calls

After the create request shown below returns an export ID, use it in both follow-up URLs:

BASH
curl --fail-with-body \
  'https://api.returning.ai/v1/loyalty-exports/66f000000000000000000701' \
  --header 'Authorization: Bearer <API_KEY>'

Wait until data.status is completed, then request the link:

BASH
curl --fail-with-body \
  'https://api.returning.ai/v1/loyalty-exports/66f000000000000000000701/download' \
  --header 'Authorization: Bearer <API_KEY>'

curl --fail --output points.csv '<TEMPORARY_DOWNLOAD_URL>'

Replace the synthetic export ID with your returned ID and the final URL with data.url. Copy the full URL, including its query string, and keep it quoted. The last command has no Authorization header.

Request#

Headers#

Authorization#stringREQUIRED
Community API key with Customer Success permission. The key selects the community.

RuleBearer <API_KEY>

Idempotency-Key#stringREQUIRED
Unique key for one intended export; reuse with the same body after an uncertain response.

Rule1-128 characters; first ASCII letter, digit, _ or -; remaining characters may also include . and :

Eg9b629318-5c67-4799-8135-27a64f1fc529

Content-Type#stringREQUIRED
Request body format.

Ruleapplication/json

Body#

The body must be JSON and at most 1 MiB. Unknown body fields are rejected. An empty client_refs array is invalid; it does not mean all customers.

To select customers and request JSON, use:

JSON
{
  "dataset": "points_transactions",
  "format": "json",
  "start_at": "2026-09-01T00:00:00.000Z",
  "end_at": "2026-09-02T00:00:00.000Z",
  "client_refs": ["000101", "000102"]
}

References use your community's active customer identifier. Do not assume this is the Returning.AI user ID. Text identifiers preserve case, spaces and leading zeroes. Numeric identifiers use canonical numeric text: 000101 does not match a numeric identifier stored as 101.

Selection is resolved during processing. An unknown or ambiguous selected identifier fails the job rather than returning a partial selection. Without a selection, unresolved identifiers can appear as null in JSON or blank in CSV. Identifier values are resolved when the report runs, not as they were at the historical event time.

dataset#stringREQUIRED
Recorded ordinary reward-coin history. XP and premium currency are excluded.

Rulepoints_transactions

format#stringREQUIRED
Output file format. JSON has additional detail fields.

Rulecsv or json

Eg"csv"

start_at#stringREQUIRED
Inclusive UTC recording time, ending in Z.

RuleAt most three fractional-second digits

Eg"2026-09-01T00:00:00.000Z"

end_at#stringREQUIRED
Exclusive UTC recording time after start_at and no later than now.

RuleNo maximum range duration

Eg"2026-09-02T00:00:00.000Z"

client_refs#string[]OPTIONAL
Optional exact customer identifiers from the community's active identifier field. Omit for all customers.

Rule1-1,000 unique nonblank strings, each at most 1,024 characters; no control characters

curl --request POST \
  --url https://api.returning.ai/v1/loyalty-exports \
  --header 'Authorization: Bearer <API_KEY>' \
  --header 'Idempotency-Key: 9b629318-5c67-4799-8135-27a64f1fc529' \
  --header 'Content-Type: application/json' \
  --data '{
    "dataset": "points_transactions",
    "format": "csv",
    "start_at": "2026-09-01T00:00:00.000Z",
    "end_at": "2026-09-02T00:00:00.000Z"
  }'

Response#

New jobs return 202. An exact replay returns 200 with the original job's current state. Save data.export_id; acceptance does not mean the file is ready. There is no success meta.code or returned polling interval.

File contents and daily loads

CSV is UTF-8 with these five columns, in this order:

ColumnMeaning
client_refConfigured customer identifier; blank if unresolved. Import it as text.
event_idStable recorded-history identifier for recognizing the same event across overlapping exports. Treat it as opaque.
recorded_at_utcUTC recording time used by the date filters. It is not necessarily the original trade or activity time.
operationincrease, decrease, overwrite, expiry or unknown.
points_deltaStored numeric amount as text, or blank when unknown. For overwrite, this is the assigned balance, not a movement.

Ordinary zero-movement rows are excluded. An overwrite that sets the balance to zero is retained. Do not sum every points_delta as points earned: handle overwrites separately and treat unknown values as unknown, not zero.

Rows are newest first by recording time, with a stable identifier tie-breaker. The date interval is inclusive at start_at and exclusive at end_at.

JSON is an array, not JSONL. It includes the five CSV fields plus schema_version, community_id, returning_user_id, occurred_at_utc, currency_type, absolute_balance, raw_action, source_reference and quality_notes. schema_version is 1, currency_type is reward, and occurred_at_utc is null. absolute_balance is populated for overwrites. raw_action is JSON-encoded text, while quality_notes is an array of strings. Extra JSON fields may contain personal data; protect the whole file.

source_reference is not necessarily a trade or order ID. operation does not reliably classify trade, mission or redemption reasons. Quality notes identify uncertainties such as missing identifiers or original activity times.

CSV text that could be interpreted as a spreadsheet formula receives a leading apostrophe; signed amounts do not. Use JSON when your automated import needs original text without spreadsheet escaping.

Example CSV records

These illustrative records show the five-column layout. Real event_id values are opaque strings; keep them exactly as received. The rows are ordered newest first.

CSV
client_ref,event_id,recorded_at_utc,operation,points_delta
000101,<eventId-3>,2026-09-01T14:00:00.000Z,overwrite,40
000101,<eventId-2>,2026-09-01T13:00:00.000Z,decrease,-10
000101,<eventId-1>,2026-09-01T12:00:00.000Z,increase,25

For an empty CSV, only the header line is present. For empty JSON, the file contains [].

JSON field reference

JSON is useful when you need extra context or original identifier text without CSV spreadsheet escaping. It is a single array of objects. Read numeric amounts as numeric text and preserve identifier strings; avoid converting client_ref to a number.

JSON fieldTypeMeaning
schema_versionintegerCurrently 1.
community_idstringCommunity owning the recorded history.
client_refstring or nullConfigured customer identifier resolved when the report runs.
returning_user_idstringReturning.AI user identifier represented as text.
event_idstringOpaque history identifier. Use it to recognize the same event in overlapping files.
recorded_at_utcstringUTC timestamp used by the date filter.
occurred_at_utcnullOriginal activity time is not available in this dataset.
currency_typestringreward.
operationstringincrease, decrease, overwrite, expiry or unknown.
points_deltastring or nullRecorded amount. For overwrite, the assigned balance rather than a movement.
absolute_balancestring or nullAssigned balance for an overwrite; otherwise null.
raw_actionstringJSON-encoded original action data; parse only if your use case needs it.
source_referencestring or nullOptional correlation reference, not a guaranteed trade/order identifier.
quality_notesarray of stringsQualifications on the record's interpretation.

Amounts use the source's native numeric precision and may appear in scientific notation. A string does not imply arbitrary-precision accounting. A missing or invalid amount becomes null in JSON or blank in CSV; do not silently turn it into zero.

Interpreting points correctly

For increase and decrease, keep the amount's sign. Do not make every amount positive. For overwrite, the amount assigns a balance and cannot be added to earned points.

For example, if a balance was 100 and an overwrite assigns 40, a row with operation: "overwrite" and points_delta: "40" does not mean the trader earned 40. You cannot calculate its -60 effect from that row alone unless you have a trustworthy prior balance. In JSON, absolute_balance also contains "40".

expiry is distinguished from ordinary decreases. unknown means the amount or classification is not known well enough; retain it for review. No operation value reliably tells you whether the business reason was trading, a mission or a store purchase.

Quality noteInterpretation
original_event_time_not_recordedUse recording time; do not infer the original trade/activity time.
business_reason_not_verifiedDo not infer a business category from the row alone.
client_ref_unresolvedThis row could not be mapped to a configured customer identifier.
legacy_number_precision_unverifiedNumeric precision of the original value is not certified.
points_amount_invalidTreat the amount as unknown, not zero.
absolute_overwrite_not_an_earnThis row assigns a balance; exclude it from a simple earned-points sum.
expiry_sign_unexpectedReview the expiry amount's sign before aggregating.

Treat quality notes as an extensible list: retain unrecognized notes instead of rejecting the whole file.

Daily loads

Request consecutive UTC recording-time windows. Use event_id to avoid duplicate rows when intentionally repeating or overlapping windows. A new request rereads history; an idempotent replay returns the same job.

The export is not a transactional snapshot or a guaranteed change feed. Late records and historical edits can make a fresh export differ. Deleted historical events are not supplied as deletion records. coverage: observed_history_only means recorded history, not certified financial completeness. The export does not reconstruct every historical balance.

For current balance summaries, use Get loyalty overview. For order details and fulfilment state, use List redemption orders by community.

Make a daily import reliable

  1. Choose the closed recording-time window and persist the request body, API key identity, idempotency key and eventual export ID in your own job record.
  2. Run only one active export for the community. On an uncertain create response, retry exactly; do not advance to the next window.
  3. After completion, download the file promptly, check its row count and byte size, and store it privately. A byte-size check detects an incomplete transfer; the API does not supply a cryptographic checksum.
  4. Upsert by event_id in your reporting store. Preserve client_ref as text, retain null/unknown values and quality notes, and handle overwrite rows separately.
  5. Advance your own import checkpoint only after the entire file has been imported successfully. If an import partially fails, reuse the saved file or download it again while available, and repeat the upsert.
  6. If your business needs reconciliation, deliberately re-export overlapping closed windows with new idempotency keys and deduplicate by event_id. Choose the overlap from your own source delays; the API does not promise a maximum lateness interval or a deletion feed.

This avoids checking each event against your database before requesting the API: the date filter selects the period and your import's upsert handles replayed rows. It does not add date filtering to the separate store-orders API.

Common questions

QuestionAnswer
Can I request yesterday only?Yes. Send yesterday's UTC recording-time boundaries, or convert the boundaries of your business timezone to UTC.
Does it return one row per customer with their balance?No. It returns recorded coin-history events. Use Get loyalty overview for current balance summaries.
Does it return store orders or ready-made BI totals?No. Use the order endpoints for orders and aggregate the exported history in your reporting system with the interpretation rules above.
Can I export all customers without paging?Yes. Omit client_refs; one job creates one file for the matching history. These endpoints have no page or cursor parameter.
Why is a successful file empty?No included history matched the requested interval and selection. Current balances alone do not imply activity in that interval.
Does retrying refresh old data?An idempotent replay returns the original job. Use a new key for a deliberately fresh extraction.
Can I recover a lost export ID?Retry the original body with the original key and API key. This family has no list-jobs endpoint, so save the ID yourself.
Can I use the file after its link expires?If the file is still available, get a new link. After the 24-hour file window expires, request a new export.
meta#objectALWAYS
Request result and support reference.

Eg{ ... }

status#stringALWAYS
Result of the request.

Rulesuccess

statusCode#integerALWAYS
HTTP status of the request.

Eg200

requestId#stringALWAYS
Support reference for this request.

Eg"example-request"

message#stringALWAYS
Human-readable summary. Do not branch on it.

Eg"Loyalty export accepted success."

data#objectALWAYS
Export job details.

Eg{ ... }

export_id#stringALWAYS
Save this ID for status and download requests.

Eg"66f000000000000000000701"

status#stringALWAYS
Current job state. Acceptance does not mean the file is ready.

Rulequeued, processing, completed, failed, cancelled or expired

Eg"queued"

dataset#stringALWAYS
The dataset requested.

Rulepoints_transactions

format#stringALWAYS
The requested file format.

Rulecsv or json

Eg"csv"

created_at#stringALWAYS
Job creation timestamp in UTC.

Eg"2026-09-03T01:00:00.000Z"

coverage#stringALWAYS
Recorded history only, not certified financial completeness.

Ruleobserved_history_only

{
  "meta": {
    "status": "success",
    "statusCode": 200,
    "requestId": "example-request"
  },
  "message": "Loyalty export accepted success.",
  "data": {
    "export_id": "66f000000000000000000701",
    "status": "queued",
    "dataset": "points_transactions",
    "format": "csv",
    "created_at": "2026-09-03T01:00:00.000Z",
    "coverage": "observed_history_only"
  }
}

Errors#

Fix the request13

400VALIDATION_ERRORFix the request
Remove unsupported query parameters and correct the request fields.
401AUTHENTICATION_REQUIREDFix the request
Send a valid Community API key in Authorization: Bearer <API_KEY>.
403API_KEY_PERMISSION_DENIEDFix the request
Use a Community API key with Customer Success permission.
403COMMUNITY_ACCESS_DENIEDFix the request
Use a Community API key, not a personal API key.
400INVALID_JSONFix the request
Send valid JSON.
409IDEMPOTENCY_CONFLICTFix the request
Restore the original body for a retry. Use a new key only for a deliberately new report.
413REQUEST_TOO_LARGEFix the request
Reduce the JSON body to at most 1 MiB.
415UNSUPPORTED_MEDIA_TYPEFix the request
Send UTF-8 application/json with a supported content encoding.
400Fix the request
Invalid request or unsupported query parameters.
401Fix the request
Missing or invalid API key.
403Fix the request
A Community API key with the required permission is needed.
413Fix the request
Request body exceeds 1 MiB.
415Fix the request
Use UTF-8 application/json.

Fix the data02

404COMMUNITY_NOT_FOUNDFix the data
Contact support about the community selected by your API key.
409Fix the data
Same key was used with different input.

Retry with backoff05

503EXPORTS_UNAVAILABLERetry with backoff
Retry with backoff. For create, preserve the original body and idempotency key.
500AUTHENTICATION_FAILEDRetry with backoff
Retry with backoff; contact support if authentication checks keep failing.
429EXPORT_IN_PROGRESSRetry with backoff
Wait at least Retry-After, currently 30 seconds. Check the active export status.
429Retry with backoff
A points export is already active; Retry-After: 30.
503Retry with backoff
Exports disabled or unavailable. Retry creation with the same idempotency key.
{
  "meta": {
    "status": "error",
    "statusCode": 400,
    "code": "VALIDATION_ERROR",
    "requestId": "req_example"
  },
  "message": "Loyalty export request error.",
  "detail": "Invalid request or unsupported query parameters."
}

Next step#

Get loyalty points export statusGET/v1/loyalty-exports/{exportId}Save export_id and poll until the job reaches a terminal state.