Trigger postcards from your own system with the TouchDrop API
You don't need Shopify or Klaviyo to use TouchDrop. If your customers live in a CRM, a subscription platform or your own database, your system can tell TouchDrop who to mail and when. TouchDrop handles the artwork, printing, postage, address checks, suppression and the unique discount code on each card.
A subscription publisher has triggered cards for its Australian subscribers from its own systems through this endpoint since the end of July 2026.
What you build
Two calls from your system, and one read when you need it.
- Send us a person. One request per person, when an event happens in your system or on a schedule you choose. TouchDrop posts nothing without it.
- Tell us when that person comes back. A purchase, a reactivated subscription, whatever counts as a return for you. Without it, your reporting shows what a campaign cost and no revenue against it.
- Ask what happened to a person. Optional. The answer confirms whether a card was produced and returns the discount code printed on it, which your checkout needs if you have no connected store.
Before you start
- Create a campaign in TouchDrop and choose Webhook Trigger. Its Trigger URL appears on the campaign page. TouchDrop reviews each new campaign once before its first cards print.
- If you will report orders, generate an API token in Settings, on the Integrations tab, under API access.
- Set your website in Settings, on the Brand tab, so the QR code on each card has a page of yours to land on.
Authentication
There are two tokens. Treat both like passwords: they can queue printed, paid-for mail.
| Token | Starts with | Scope | Where you find it |
|---|---|---|---|
| Campaign trigger token | wht_ | One campaign | The campaign page: the last part of the Trigger URL |
| API token | tda_ | Your whole account, for conversions and refunds | Settings, Integrations tab, API access |
Send the token in a header:
POST /api/webhooks/campaign-trigger HTTP/1.1
Host: touchdrop.com.au
Authorization: Bearer wht_your_token
Content-Type: application/json
On the trigger and status endpoints, X-TouchDrop-Token: wht_your_token works as well, for platforms that reserve the Authorization header.
If your platform cannot set headers, put the token in the path instead: POST /api/webhooks/campaign-trigger/wht_your_token. Prefer the header. A token in a URL ends up in access logs, proxies and browser history along the way.
Server only
Call the API from your server. Never put a token in a storefront theme, a browser script or a mobile app, where anyone can read it. Rotating a token in TouchDrop stops the old one at once; update your system with the new value.
Trigger a postcard
POST https://touchdrop.com.au/api/webhooks/campaign-trigger
Enrols one person into one campaign. The campaign already holds the artwork, the offer and the sending rules; this call says who receives it.
| Field | Type | Required | Notes |
|---|---|---|---|
email | string | Yes | Valid email, up to 320 characters. Also the key that stops duplicates |
first_name | string | No | Up to 100 characters. firstName and firstname also work |
last_name | string | No | Up to 100 characters. lastName and lastname also work |
external_id | string | No | Your own ID for this person, up to 200 characters |
source | string | No | A label for your reporting, such as winback-flow, up to 50 characters |
address | object | Yes, with one exception | See the table below. You can leave it out when TouchDrop can find an address from your connected Shopify store or from a previous card to this person |
If your platform cannot send a nested object, send the address fields at the top level instead (address1, address2, city, state, postcode or zip, country).
Address fields by account region:
| Account | Required | Notes |
|---|---|---|
| Australia | address1, city, state, postcode | state is NSW, VIC, QLD, WA, SA, TAS, ACT or NT. Postcode has 4 digits |
| United Kingdom | address1, city, postcode | city is the post town. No county needed. Country GB if you send one |
| Netherlands | address1, city, postcode | address1 holds street and house number. Postcode in the form 1012 AB |
Example request:
curl -X POST https://touchdrop.com.au/api/webhooks/campaign-trigger \
-H 'Authorization: Bearer wht_your_token' \
-H 'Content-Type: application/json' \
-d '{
"email": "jane@example.com.au",
"first_name": "Jane",
"external_id": "customer_123",
"address": {
"address1": "12 Example Street",
"city": "Fitzroy",
"state": "VIC",
"postcode": "3065",
"country": "AU"
}
}'
Example response (202 Accepted):
{
"status": "queued",
"entryId": "3f2b8c1e-0d4a-4b8e-9c61-2a7f5e9d1b40",
"campaignStatus": "active",
"monthlyAllowance": { "cap": 500, "used": 12, "remaining": 488 },
"requestId": "req_a1b2c3d4e5f60718"
}
Store the entryId against your customer record. You need it to check the outcome later.
| Code | status or code | Meaning |
|---|---|---|
| 202 | queued | Accepted. Processing starts within about 5 minutes |
| 200 | duplicate_pending | This person is already queued for this campaign. Nothing to do |
| 200 | already_mailed | This campaign already mailed this person |
| 400 | The payload is invalid. details names each field | |
| 401 | unauthorized | Missing, wrong or rotated token |
| 409 | The campaign is completed, stopped or archived | |
| 422 | invalid_address | The address cannot produce a card. Nothing was queued |
| 422 | wrong_region | The address is outside your account's country |
| 422 | undeliverable_address | A parcel locker or parcel collection point |
| 422 | monthly_cap_reached | Your plan's monthly postcard allowance is used up |
| 429 | Too many requests. Wait for Retry-After |
What a 202 tells you. TouchDrop accepted the person. If your account has no other source for the address, it also means the address passed the check, because an unusable one gets a 422 instead. A 202 does not confirm a printed card: suppression, frequency caps and the monthly allowance apply during processing a few minutes later. The status endpoint confirms the outcome.
While a campaign is in draft, awaiting review or paused, triggers still return 202 and the response carries a notice. A campaign that has never gone live holds its queue until it does. Once a campaign has gone live, a queued person who waits more than 7 days for it to resume (for example after the postage balance ran out) expires without a card, and TouchDrop alerts you.
Treat any response outside 200 and 202 as a failed action. Many platforms report only the status code, so route failures to a queue someone reviews.
Check what happened to a person
GET https://touchdrop.com.au/api/webhooks/campaign-trigger/status?entry_id=3f2b8c1e-0d4a-4b8e-9c61-2a7f5e9d1b40
Use the campaign trigger token, in the header as above. You can look up by email instead of entry_id, but prefer the ID: an email in a query string travels into proxy and CDN logs.
{
"entryId": "3f2b8c1e-0d4a-4b8e-9c61-2a7f5e9d1b40",
"email": "jane@example.com.au",
"entryStatus": "processed",
"skipReason": null,
"receivedAt": "2026-10-01T10:00:02Z",
"processedAt": "2026-10-01T10:04:11Z",
"campaignStatus": "active",
"mailing": {
"status": "shipped",
"scheduledAt": "2026-10-01T10:04:11Z",
"shippedAt": "2026-10-02T09:00:00Z",
"isHoldout": false
},
"discount": {
"code": "WB-4KD2",
"status": "active",
"expiresAt": "2026-12-01T00:00:00Z"
}
}
shipped means the card was printed and handed to the postal service on that date. Letter post is not tracked to the door, so no one can confirm the day it arrived. If you have no connected store, read discount.code and load it into your checkout, or your checkout will reject the code printed on the card.
| skipReason | Meaning |
|---|---|
no_valid_address | No usable address |
wrong_region | The address is outside your account's country |
undeliverable | A parcel locker or collection point |
suppressed | Your suppression or frequency rules excluded this person |
already_mailed | This campaign already mailed them |
converted | They already converted, so the campaign stopped |
expired | They waited more than 7 days for a paused campaign |
Other skip reasons can appear. Treat one you don't recognise as information and keep processing.
Report a conversion
POST https://touchdrop.com.au/api/webhooks/conversion
Use your API token (tda_). You need this call when your orders do not come from a store connected to TouchDrop.
| Field | Type | Required | Notes |
|---|---|---|---|
order_id | string | Yes | Your ID for the order, invoice or subscription, up to 200 characters. Counted once |
value | number or string | Yes | The total the customer paid: after discounts, including tax and shipping. A decimal amount such as 120.50, never pence or cents |
email | string | One of these three | The email you sent in the trigger |
discount_code | string | One of these three | The code from the card, if used |
address | object | One of these three | Same shape as the trigger |
currency | string | No | ISO code such as AUD. Defaults to your account currency. TouchDrop does not convert currencies |
occurred_at | string | No | ISO 8601 time of the order, up to 90 days back. Defaults to now |
type | string | No | one_time (default), subscription_initial or subscription_renewal |
source_system | string | No | Only if two of your systems number orders the same way. Letters, numbers, dot, dash and underscore, up to 50 characters |
curl -X POST https://touchdrop.com.au/api/webhooks/conversion \
-H 'Authorization: Bearer tda_your_token' \
-H 'Content-Type: application/json' \
-d '{
"order_id": "SUB-1001",
"email": "jane@example.com.au",
"value": 120.50,
"currency": "AUD",
"type": "subscription_initial"
}'
{ "status": "attributed", "orderId": "SUB-1001", "revenueTracked": 120.5, "requestId": "req_9f8e7d6c5b4a3921" }
How an order counts towards a campaign. It counts when someone uses the discount code printed on the card, at any time. It also counts when the email or delivery address matches someone the campaign mailed, inside the campaign's attribution window: 60 days from the card's estimated arrival, unless agreed otherwise. Each order counts towards one campaign at most. Send every order; TouchDrop decides which ones attribute.
| Code | status | Meaning |
|---|---|---|
| 202 | attributed | Matched to a campaign. revenueTracked is the amount credited |
| 202 | recorded_unattributed | Accepted, matched nothing. Expected for orders that did not come from a card |
| 200 | duplicate | This order_id has already attributed. The first value stands |
| 400 | Invalid payload, or none of email, discount_code and address | |
| 401 | unauthorized | Missing, wrong or rotated token |
| 429 | Too many requests. Wait for Retry-After |
If your account also has a connected store, the response carries a warnings entry: TouchDrop already imports that store's orders, so reporting the same sale here under another ID counts it twice.
For subscriptions, use subscription_initial for the reactivation the card won and subscription_renewal for the rebills after it. That keeps revenue the card created apart from revenue you were already collecting.
Report a refund
POST https://touchdrop.com.au/api/webhooks/conversion/refund
Use your API token and send { "order_id": "SUB-1001" }. If you sent source_system with the conversion, send the same value here.
A refund removes the whole conversion from revenue and keeps the record. Repeat calls change nothing. An order that never attributed returns 404, because TouchDrop does not store unmatched conversions. This version does not support partial refunds.
Errors
Error responses are JSON with an error message. Where it applies you also get a stable code and a details list. Branch on code, never on the wording of error; TouchDrop may reword messages and will not change codes. Responses carry an X-Request-Id header. Quote it when you contact support.
| Code | Retry? |
|---|---|
| 400, 401, 404, 409, 422 | No. Fix the request first |
| 429 | Yes, after Retry-After |
| 500 | Yes, with exponential backoff |
Idempotency
| Call | Key | On repeat |
|---|---|---|
| Trigger | campaign + email | duplicate_pending or already_mailed. Nobody receives a second card |
| Conversion | account + order_id | duplicate once the order has attributed. TouchDrop counts the revenue once |
| Refund | account + order_id | No change |
Retries are safe. There is no batch endpoint: send one person per request, in a loop.
Rate limits
TouchDrop limits how many requests a caller can send per minute. Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers, so a large import can pace itself. On a 429, honour Retry-After. If you plan a one-off bulk release, tell TouchDrop in advance.
Sending from a marketing platform
Most marketing and CRM platforms can call a webhook when someone enters a segment or a flow step fires. In Klaviyo, add a webhook step to a flow:
- Method: POST
- URL:
https://touchdrop.com.au/api/webhooks/campaign-trigger - Headers:
Authorization: Bearer wht_your_tokenandContent-Type: application/json
{
"email": "{{ person.email }}",
"first_name": "{{ person.first_name|default:'' }}",
"last_name": "{{ person.last_name|default:'' }}",
"source": "klaviyo-winback-flow",
"address": {
"address1": "{{ person|lookup:'$address1'|default:'' }}",
"address2": "{{ person|lookup:'$address2'|default:'' }}",
"city": "{{ person|lookup:'$city'|default:'' }}",
"state": "{{ person|lookup:'$region'|default:'' }}",
"postcode": "{{ person|lookup:'$zip'|default:'' }}",
"country": "AU"
}
}
If TouchDrop is connected to your Shopify store, you can leave the address out and send email and name: TouchDrop then uses an address from the customer's recent Shopify orders. Otherwise check that every field holds a value for the people in that flow. Most platforms send a missing field as an empty string, which reaches TouchDrop as a blank address and returns a 422.
Going live
- Create the Webhook Trigger campaign and copy its Trigger URL.
- Send one trigger with a real address you control. Expect 202 and store the
entryId. - Send one with a broken address (no postcode) and a different email. Expect 422 with
code: "invalid_address". This proves your system sees failures. - Poll the status endpoint until
entryStatusisprocessed, then readmailing.statusanddiscount.code. - Post a test conversion for that person. Expect
recorded_unattributeduntil the card's estimated arrival date, thenattributed. Refund the test order afterwards so it does not count as revenue. - Release a small group first, check the count your system sends against what TouchDrop accepted, then switch on the full flow. Ask TouchDrop for a hard campaign ceiling if you want one.
Data handling
| Data | Why TouchDrop needs it |
|---|---|
| Postal address | To print and deliver the card |
| Name | To personalise the card. Optional |
| To recognise the same person across calls and match orders | |
external_id | Optional. Stored so you can reconcile with your system |
| Order value and ID | To attribute revenue to a campaign |
TouchDrop stores each request against its campaign for processing and audit, and drops any field this page does not list. Do not send payment details or special-category data; no endpoint accepts them. TouchDrop never sells your customer data and shares it only with its print partner to produce your cards. For a data processing agreement or a deletion request, email support@touchdrop.com.au.
Frequently asked questions
Can I trigger postcards from my own CRM or app?
Yes. Create a campaign with the Webhook Trigger, then have your system send one request per person, with email, name and address, to that campaign's trigger URL. TouchDrop checks the address, applies your sending rules, prints the card and posts it.
Do I need Shopify or Klaviyo to use the API?
No. Without a connected store, send the address in every trigger, read the discount code from the status endpoint into your checkout, and report orders through the conversion endpoint.
Does a 202 response mean the card was printed?
No. It means TouchDrop accepted the person. Suppression and frequency rules run a few minutes later; the status endpoint tells you whether a card was produced and when it was handed to the post.
Can I send a whole list in one call?
No. Send one person per request. For a one-off list, a CSV upload in the app is the simpler route.
Does the API work for Australian, UK and Dutch accounts?
Yes. UK and Dutch accounts use touchdrop.co and Australian accounts use touchdrop.com.au. The address fields differ by country, as in the table above; each account mails within its own country.
API version 1.1. New response fields, optional request fields and new skip reasons can appear without notice; build your system to ignore fields it does not know. TouchDrop announces breaking changes in advance.