Introduction
Welcome to the WeTeachMe API! You can use our API to access WeTeachMe API endpoints. (Do you know that you can embed the checkout widget using JavaScript? View Docs)
The API has two families of endpoints, and they use different formats.
Products and Orders follow JSON API v1.0. Send and receive application/vnd.api+json, with bodies wrapped in data, type, and attributes.
External integration endpoints (external products, dates and tickets, bookings, transfers, refunds) use plain JSON. Send Content-Type: application/json with a flat request body as shown in each section, and expect a flat JSON response. They do not use the JSON API envelope.
Code examples are provided in Shell and Ruby. You can view them in the dark area to the right, and you can switch the programming language of the examples with the tabs in the top right.
Developers can email support@weteachme.com with questions about any endpoint. Include the vendor account email, the endpoint, the request body with the API key removed, and the response you received.
This API documentation page was created with Slate.
Authentication
To authorize, use this code:
# With shell, you can just pass the correct header with each request
curl "api_endpoint_here"
-H "API-KEY: meowmeowmeow"
# OR
curl "api_endpoint_here?api-key=meowmeowmeow"
require 'httparty'
api = HTTParty.get('some_endpoint_here', headers: { 'API-KEY' => 'meowmeowmeow' })
# OR
api = HTTParty.get('some_endpoint_here?api-key=meowmeowmeow')
Make sure to replace
meowmeowmeowwith your API key.
WeTeachMe uses API keys to allow access to the API. You can find your API key in the WeTeachMe Dashboard under Company Settings → API Access (dashboard.weteachme.com/company-settings). Need a new key? Email support@weteachme.com.
API key is expected to be included in all API requests to the server in a header that looks like the following:
API-KEY: meowmeowmeow
A missing or unknown API key returns 404 with an empty body.
Products (Listings)
READ All Products (Listings)
curl
-H "Accept: application/vnd.api+json"
-H "Content-Type: application/vnd.api+json"
"https://api.weteachme.com/vendors/products?page[number]=1&page[size]=15"
-H "API-KEY: meowmeowmeow"
require 'httparty'
HTTParty.get(
"https://api.weteachme.com/vendors/products?page[number]=1&page[size]=15",
headers: {
"Accept" => 'application/vnd.api+json',
"Content-Type" => 'application/vnd.api+json'
"API-KEY" => 'meowmeowmeow'
}
);
The above command returns JSON structured like this:
{
"data":[{
"id":"1234567",
"type":"products",
"links":{
"self":"http://api.weteachme.com:3000/vendors/products/1234567"
},
"attributes":{
"name":"One On One Programme: Ages 5 to 20",
"subheading":"Personalise Your Training Plan",
"description":"</p>We provide feedback, perspective and encouragement to really develop new skills and habits to improve the young player's game.</p>",
"location-name":"41-43 Stewart St, Richmond VIC 3121, Australia",
"url":"https://weteachme.com/1111111/1234567-one-on-one-programme-ages-5-to-20",
"image":{
"original": "https://image.com/uploads/image/url/1234567/original_22869cb0-84f9-0133-03e6-02b0fd6e4061.jpg",
"large": "https://image.com/uploads/image/url/1234567/large_22869cb0-84f9-0133-03e6-02b0fd6e4061.jpg",
"medium": "https://image.com/uploads/image/url/1234567/medium_22869cb0-84f9-0133-03e6-02b0fd6e4061.jpg",
},
"status":"active",
"private":false,
"things-to-bring":["Your enthusiasm!"],
"things-to-get":["Book 2 blocks of 5 sessions and receive a 5% discount!","Expert tuition from an experienced teacher!"],
"things-to-learn":["Work at developing the latent soccer abilities of the young player and maximise player development. ","Coaching is targeted and focused on the individual's needs and goals are set for that young player. ","Individual basis to accelerate growth as a soccer player. "],
"target-audience":"5-20 years old",
"dress-code":"Sportswear",
"dates":[
{
"id":136172,
"start-datetime":"2017-02-04T10:30:00.000Z",
"end-datetime":"2017-02-05T12:30:00.000Z",
"capacity":12,
"sold":6,
"tickets-left":6,
"session-number":2,
"session-name":"Week",
"schedules":[
{ "start-datetime":"2017-02-04T10:30:00.000Z", "end-datetime":"2017-02-04T12:30:00.000Z" },
{ "start-datetime":"2017-02-05T10:30:00.000Z", "end-datetime":"2017-02-05T12:30:00.000Z" }
],
"tickets":[
{
"id":219222,
"name":"Single",
"number-of-tickets":12,
"price":"206.0",
"member-price":"200.0",
"sold":5,
"tickets-left":6,
"start-datetime":"1743-04-22T10:30:00.000Z",
"end-datetime":"2290-11-20T10:30:00.000Z",
"is-private":false,
"min":1,
"max":null
},
{
"id":219221,
"name":"VIP",
"number-of-tickets":12,
"price":"306.0",
"member-price":null,
"sold":1,
"tickets-left":6,
"start-datetime":"1743-04-22T10:30:00.000Z",
"end-datetime":"2290-11-20T10:30:00.000Z",
"is-private":true,
"min":1,
"max":null
}
]
}
]
}
}],
"links":{
"first":"http://api.weteachme.com:3000/vendors/products?page%5Bnumber%5D=1&page%5Bsize%5D=15",
"next":"http://api.weteachme.com:3000/vendors/products?page%5Bnumber%5D=2&page%5Bsize%5D=15",
"last":"http://api.weteachme.com:3000/vendors/products?page%5Bnumber%5D=16&page%5Bsize%5D=15"
}
}
This endpoint retrieves all listings.
HTTP Request
GET https://api.weteachme.com/vendors/products
Query Parameters
| Parameter | Required | Default | Description |
|---|---|---|---|
| page | Pagination (http://jsonapi.org/format/#fetching-pagination) | ||
| filter | Filtering (http://jsonapi.org/format/#fetching-filtering) |
Pagination Fields
| Parameter | Description |
|---|---|
| size | Default 15, Maximum 45 |
| number | Default 1 |
Filtering Fields
| Parameter | Type | Description |
|---|---|---|
| all | boolean | Returns all records |
| name | string | Filter by Listing Name |
| location-name | string | Filter by Listing Location |
| tag-name | string | Filter by Listing Tag Name |
| tag-id | number | Filter by Listing Tag Id |
| private | boolean | Filter by Private or Public Listing |
READ Product (Listing)
curl
-H "Accept: application/vnd.api+json"
-H "Content-Type: application/vnd.api+json"
"https://api.weteachme.com/vendors/products/1234567"
-H "API-KEY: meowmeowmeow"
require 'httparty'
HTTParty.get(
"https://api.weteachme.com/vendors/products/1234567",
headers: {
"Accept" => 'application/vnd.api+json',
"Content-Type" => 'application/vnd.api+json'
"API-KEY" => meowmeowmeow"
}
);
The above command returns JSON structured like this:
{
"id":"1234567",
"type":"products",
"links":{
"self":"http://api.weteachme.com:3000/vendors/products/1010225"
},
"attributes":{
"name":"One On One Programme: Ages 5 to 20",
"subheading":"Personalise Your Training Plan",
"description":"</p>We provide feedback, perspective and encouragement to really develop new skills and habits to improve the young player's game.</p>",
"location-name":"41-43 Stewart St, Richmond VIC 3121, Australia",
"url":"https://weteachme.com/1111111/1234567-one-on-one-programme-ages-5-to-20?ref=a1b2c3",
"image":{
"original": "https://image.com/uploads/image/url/1234567/original_22869cb0-84f9-0133-03e6-02b0fd6e4061.jpg",
"large": "https://image.com/uploads/image/url/1234567/large_22869cb0-84f9-0133-03e6-02b0fd6e4061.jpg",
"medium": "https://image.com/uploads/image/url/1234567/medium_22869cb0-84f9-0133-03e6-02b0fd6e4061.jpg",
},
"status":"active",
"private":false,
"things-to-bring":["Your enthusiasm!"],
"things-to-get":["Expert tuition from an experienced teacher!", "Skill Upgrades"],
"things-to-learn":["Work at developing the latent abilities of the young player and maximise player development. ","Coaching is targeted and focused on the individual's needs and goals are set for that young player. "],
"target-audience":"5-20 years old",
"dress-code":"Sportswear",
"dates":[
{
"id":136172,
"start-datetime":"2017-02-04T10:30:00.000Z",
"end-datetime":"2017-02-04T12:30:00.000Z",
"capacity":12,
"sold":6,
"tickets-left":6,
"schedules":[
{ "start-datetime":"2017-02-04T10:30:00.000Z", "end-datetime":"2017-02-04T12:30:00.000Z" },
{ "start-datetime":"2017-02-05T10:30:00.000Z", "end-datetime":"2017-02-05T12:30:00.000Z" }
],
"tickets":[
{
"id":219222,
"name":"Single",
"number-of-tickets":12,
"price":"206.0",
"member-price":"200.0",
"sold":5,
"tickets-left":6,
"start-datetime":"1743-04-22T10:30:00.000Z",
"end-datetime":"2290-11-20T10:30:00.000Z",
"is-private":false,
"min":1,
"max":null
},
{
"id":219221,
"name":"VIP",
"number-of-tickets":12,
"price":"306.0",
"member-price":null,
"sold":1,
"tickets-left":6,
"start-datetime":"1743-04-22T10:30:00.000Z",
"end-datetime":"2290-11-20T10:30:00.000Z",
"is-private":true,
"min":1,
"max":null
}
]
}
]
}
}
This endpoint retrieves all classes.
The ?ref= on url is required — it’s what tells checkout the booking came from you, the vendor, so you aren’t charged the marketplace fee.
HTTP Request
GET https://api.weteachme.com/vendors/products/1234567
Orders
Create Order
curl
-X POST
-H "Accept: application/vnd.api+json"
-H "Content-Type: application/vnd.api+json"
-H "API-KEY: meowmeowmeow"
-d '{ "data": { "type": "vendors/orders", "attributes": { "ticket": { "id": 1234, "qty": 2 } } } }
"https://api.weteachme.com/vendors/orders"
require 'httparty'
HTTParty.post(
"https://api.weteachme.com/vendors/orders",
body: {
"data": {
"type": "vendors/orders",
"attributes": {
"ticket": {
"id": 1234,
"qty": 2
}
}
}
}
}.to_json,
headers: {
"Accept" => 'application/vnd.api+json',
"Content-Type" => 'application/vnd.api+json'
"API-KEY" => meowmeowmeow"
}
);
The above command returns JSON structured like this:
{
"id":"1234567",
"type":"vendors/orders",
"attributes":{
"url": "https://weteachme.com/l/Xk7fQ2",
}
}
This endpoint generates a new order and returns the url to redirect to.
HTTP Request
POST https://api.weteachme.com/vendors/orders
JSON BODY
| key | Required | Default | Description |
|---|---|---|---|
| ticket | true | contains ticket id and qty |
External products
Use these endpoints to create or update a listing from an external catalog and to read its current WTM status. The API-KEY determines the vendor; identifiers never grant access to another vendor’s listings.
Create or update an external product
curl "https://api.weteachme.com/vendors/external_products" \
-X POST \
-H "Content-Type: application/json" \
-H "API-KEY: meowmeowmeow" \
-d '{
"external_product_id": "course-555",
"title": "Cooking Workshop",
"subheading": "Learn to cook delicious meals",
"description": "In this workshop, you will learn...",
"audience": "Adults",
"private": false,
"skill_level": "Beginner",
"location": {
"venue": "Main Kitchen",
"address": "123 Kitchen Street",
"timezone": "Australia/Melbourne",
"city": "Melbourne",
"state": "Victoria",
"country": "Australia",
"postal_code": "3000"
},
"image_url": "https://example.com/cooking-workshop.jpg",
"image_alt": "Students preparing a meal",
"learn_list": ["Kitchen skills", "Recipe techniques"],
"get_list": ["Ingredients", "Recipe notes"],
"bring_list": ["Apron"],
"dress_code": "Closed-toe shoes"
}'
require "httparty"
HTTParty.post(
"https://api.weteachme.com/vendors/external_products",
headers: { "Content-Type" => "application/json", "API-KEY" => "meowmeowmeow" },
body: {
external_product_id: "course-555",
title: "Cooking Workshop",
subheading: "Learn to cook delicious meals",
description: "In this workshop, you will learn...",
audience: "Adults",
private: false,
skill_level: "Beginner",
location: {
venue: "Main Kitchen",
address: "123 Kitchen Street",
timezone: "Australia/Melbourne",
city: "Melbourne",
state: "Victoria",
country: "Australia",
postal_code: "3000"
},
image_url: "https://example.com/cooking-workshop.jpg",
image_alt: "Students preparing a meal"
}.to_json
)
A successful create or update returns
201 Createdand the raw serialized listing, which includes many product attributes. Key fields include:
{
"id": 1234567,
"name": "Cooking Workshop",
"status": "under_review",
"external_product_id": "course-555"
}
external_product_id is the recommended stable identifier. A request with a matching vendor-owned external ID updates that listing; otherwise it creates a listing in under_review. For an existing WTM listing, omit external_product_id and supply its internal product_id. An explicit product_id that is missing or belongs to another vendor returns 422 and never creates a replacement listing.
Omitted top-level listing fields are left unchanged. If you send any of audience, learn_list, get_list, bring_list, or dress_code, include the complete desired set of those metadata fields because that metadata object is replaced. Send status: "active" to reactivate an archived listing. The optional dates array uses the same format as External dates and tickets.
Product fields
| Field | Required | Description |
|---|---|---|
| external_product_id | Recommended | Stable ID in the external system; used to create or update the vendor’s listing |
| product_id | Alternative | Internal WTM listing ID; used only when external_product_id is omitted |
| title | New listings | Listing title |
| subheading | No | Short listing summary |
| description | No | Full listing description; HTML is accepted |
| status | No | active reactivates a matching archived listing |
| audience | No | Intended audience |
| private | No | Whether the listing is private |
| skill_level | No | Skill level label |
| seo_meta_title | No | SEO title |
| seo_meta_description | No | SEO description |
| image_url | No | Publicly reachable listing image URL |
| image_alt | No | Accessible image description |
| learn_list | No | Array of learning outcomes |
| get_list | No | Array of inclusions |
| bring_list | No | Array of things attendees should bring |
| dress_code | No | Clothing or safety guidance |
| location | No | Venue object; matching venue and address reuse an existing vendor location |
| dates | No | Dates and tickets to synchronize with the listing |
Location fields
| Field | Required | Description |
|---|---|---|
| venue | Yes | Venue name |
| address | Yes | Street address |
| lat | No | Latitude |
| lng | No | Longitude |
| timezone | No | IANA timezone, for example Australia/Melbourne |
| city | No | City |
| state | No | State or region |
| country | No | Country |
| postal_code | No | Postal code |
| parking | No | Parking instructions |
Read external product status
curl "https://api.weteachme.com/vendors/external_products/course-555" \
-H "Content-Type: application/json" \
-H "API-KEY: meowmeowmeow"
require "httparty"
HTTParty.get(
"https://api.weteachme.com/vendors/external_products/course-555",
headers: { "Content-Type" => "application/json", "API-KEY" => "meowmeowmeow" }
)
The path identifier is the external product ID, not the WTM product ID.
{
"status": "active"
}
The endpoint returns 200 for a matching vendor-owned listing and 404 when the external ID is unknown or belongs to another vendor.
Response statuses
| Status | Meaning |
|---|---|
| 201 | Listing created or updated |
| 200 | External product status returned |
| 404 | External product is not found for the authenticated vendor; empty response body |
| 413 | Request exceeds 1 MiB (request_too_large) |
| 422 | Product validation fails, or payment credentials are rejected (sensitive_payment_data) |
| 503 | Request auditing is unavailable (request_audit_unavailable); no listing mutation is attempted |
External dates and tickets
Use this endpoint to add or update scheduled dates and ticket types on an existing vendor listing. Identify the listing by external_product_id or by its internal WTM product_id. Both lookups are restricted to the vendor authenticated by API-KEY.
Synchronize dates and tickets
curl "https://api.weteachme.com/vendors/external_dates_tickets" \
-X POST \
-H "Content-Type: application/json" \
-H "API-KEY: meowmeowmeow" \
-d '{
"external_product_id": "course-555",
"dates": [{
"external_date_id": "course-555-2026-10-24-10am-1pm",
"start_date": "2026-10-24",
"start_time": "10:00 am",
"end_date": "2026-10-24",
"end_time": "1:00 pm",
"capacity": 12,
"tipping_point": 1,
"sold": 0,
"status": "active",
"is_private": true,
"tickets": [{
"name": "General admission",
"description": "Workshop admission",
"ticket_type": "full_price",
"open_difference": -720,
"close_difference": -24,
"qty": 12,
"price": 95.00
}]
}]
}'
require "httparty"
HTTParty.post(
"https://api.weteachme.com/vendors/external_dates_tickets",
headers: { "Content-Type" => "application/json", "API-KEY" => "meowmeowmeow" },
body: {
external_product_id: "course-555",
dates: [{
external_date_id: "course-555-2026-10-24-10am-1pm",
start_date: "2026-10-24",
start_time: "10:00 am",
end_date: "2026-10-24",
end_time: "1:00 pm",
capacity: 12,
tipping_point: 1,
sold: 0,
status: "active",
is_private: true,
tickets: [{
name: "General admission",
description: "Workshop admission",
ticket_type: "full_price",
open_difference: -720,
close_difference: -24,
qty: 12,
price: 95.00
}]
}]
}.to_json
)
A successful request returns
201 Created.
{
"message": "1 dates successfully created"
}
external_date_id is the date idempotency key within the listing. Replaying it updates the existing date instead of creating another one. Tickets on that date are updated by exact ticket name; keep ticket names unchanged between synchronization runs.
Upload reviewable dates with is_private: true. Publish an approved date by replaying the same external_date_id and ticket names with is_private: false.
Top-level fields
| Field | Required | Description |
|---|---|---|
| external_product_id | One identifier | Stable external listing ID |
| product_id | One identifier | Internal WTM listing ID; used only when external_product_id is omitted |
| dates | Yes | Non-empty array of date objects |
Date fields
| Field | Required | Description |
|---|---|---|
| external_date_id | Recommended | Stable external date ID used for updates and safe replays |
id-code |
No | External source code stored in WTM as external_code; it is not an update key |
| start_date | Yes | Start date in YYYY-MM-DD format |
| start_time | Yes | Start time, for example 10:00 am |
| end_date | Yes | End date in YYYY-MM-DD format |
| end_time | Yes | End time |
| capacity | Yes | Total date capacity |
| tipping_point | Yes | Minimum attendance threshold |
| sold | Yes | Existing sold quantity in the source system |
| status | No | Defaults to active |
| cancelled | No | Defaults to false |
| is_private | No | Hide or publish the date |
| tickets | Yes | Non-empty array of ticket objects |
Ticket fields
| Field | Required | Description |
|---|---|---|
| name | Yes | Ticket name and update key within the date |
| description | No | Ticket description |
| ticket_type | Yes | Ticket type, commonly full_price |
| qty | Yes | Ticket quantity; 0 uses the date capacity |
| price | Yes | Ticket price |
| open_difference | No | Booking-open time in hours relative to class start; omitted uses the API default |
| close_difference | No | Booking-close time in hours relative to class start; -24 closes 24 hours before |
| weight | No | Display order; omitted uses array order |
| ticket_merchandises | No | Compulsory merchandise rows containing merchandise_id and qty |
Response statuses
| Status | Meaning |
|---|---|
| 201 | Dates and tickets synchronized |
| 413 | Request exceeds 1 MiB (request_too_large) |
| 422 | Listing/date/ticket validation fails, or payment credentials are rejected (sensitive_payment_data) |
| 503 | Request auditing is unavailable (request_audit_unavailable); no synchronization is attempted |
The controller currently returns 422 for unexpected internal failures as well as validation failures. In those cases errors may be a bare string; message remains human-readable. A multi-date request is transactional: a validation failure rolls back that synchronization call.
External bookings
Use this endpoint to record a booking that an external service has already confirmed and paid. It creates the WTM order and attendee records but does not collect money or call a payment gateway.
The API-KEY determines the vendor. The request does not accept a vendor or company ID.
Finding the ticketing_id
Every line item names a WTM ticket by ticketing_id. Get it from the vendor Products API:
GET https://api.weteachme.com/vendors/products(JSON API format, see Products) lists the vendor’s listings. Each listing carries adatesarray, each date carries aticketsarray, and each ticket carries anid.- Use
dates[].tickets[].idasticketing_id.dates[].idis the date, not the ticket, and is rejected.
If you created the dates through the dates and tickets endpoint, WTM does not return ticket IDs in that response and the Products API does not echo your external_date_id. Match your date to dates[].start-datetime (UTC) and your ticket to tickets[].name, then read tickets[].id. Ticket names are the update key for synchronized dates, so keep them stable.
A ticketing_id that belongs to another vendor, or does not exist, returns 404. WTM does not reject a booking because the class date has passed. The attendee details link is only offered while the class is still ahead.
Record an external booking
curl "https://api.weteachme.com/vendors/external_bookings" \
-X POST \
-H "Content-Type: application/json" \
-H "API-KEY: meowmeowmeow" \
-d '{
"source": "partner_portal",
"external_booking_id": "booking-123",
"booked_at": "2026-08-25T10:15:00+10:00",
"notify_customer_enrolment": true,
"notify_customer_payment": false,
"notify_vendor": false,
"purchaser": {
"firstname": "Billie",
"lastname": "Buyer",
"email": "billie@example.com",
"mobile": "0400000000",
"requirements": "Wheelchair access"
},
"line_items": [{
"ticketing_id": 123,
"unit_price": "55.00",
"quantity": 2
}],
"payment": {
"amount": "110.00",
"currency": "AUD",
"reference": "payment-456"
}
}'
require "httparty"
HTTParty.post(
"https://api.weteachme.com/vendors/external_bookings",
headers: { "Content-Type" => "application/json", "API-KEY" => "meowmeowmeow" },
body: {
source: "partner_portal",
external_booking_id: "booking-123",
booked_at: "2026-08-25T10:15:00+10:00",
notify_customer_enrolment: true,
notify_customer_payment: false,
notify_vendor: false,
purchaser: {
firstname: "Billie",
lastname: "Buyer",
email: "billie@example.com",
requirements: "Wheelchair access"
},
line_items: [{
ticketing_id: 123,
unit_price: "55.00",
quantity: 2
}],
payment: { amount: "110.00", currency: "AUD", reference: "payment-456" }
}.to_json
)
A new booking returns
201 Created.
{
"order_id": 987,
"external_booking_id": "booking-123",
"source": "partner_portal",
"status": "completed",
"replayed": false
}
Each line item books seats either by count or by naming each attendee:
line_items[].quantityis a positive JSON integer. WTM creates one attendee per seat and copies the purchaser details to every attendee.line_items[].attendeesis a non-empty array of attendee objects. Each attendee requiresfirstname,lastname, andemail, and accepts optionalmobileandrequirements. Other attendee fields are rejected.
At least one of the two is required. When both are present, quantity must equal the number of attendees.
line_items[].unit_price is the actual price paid per seat. Prices and payment.amount must be finite decimal values. The sum of unit_price × seats for all lines must equal payment.amount, and payment.currency must match the vendor’s currency. Ticketings must be active, owned by the authenticated vendor, have enough capacity, and use the full-amount payment option; instalment and recurring ticketings are not supported.
Purchasers accept firstname, lastname, email, mobile, and optional requirements. Other purchaser fields are rejected. Purchaser and attendee emails must be valid email addresses, and mobile numbers must be strings of at most 32 characters. requirements, source, external_booking_id, payment.reference, and every purchaser or attendee firstname, lastname, and email must be strings of at most 255 characters. booked_at must describe a time that has already occurred; future timestamps are rejected.
Request bodies are limited to 1 MiB. Larger requests return 413 with error_code: "request_too_large". Do not send authentication secrets, card details, or bank credentials in the booking body; payment credentials are rejected before booking state is created.
Testing a request
Set dry_run: true to validate a request without recording anything. WTM runs every check a real call runs — API key, payload shape, ticket ownership and availability, prices against the payment amount, currency, and booked_at — then tells you what the booking would have contained.
Nothing is written: no order, no attendees, no payment, no messages, and no seats are taken. The external_booking_id stays free, so send the same one again for the real call.
{
"dry_run": true,
"external_booking_id": "booking-123",
"source": "partner_portal",
"booked_at": "2026-08-25T10:15:00+10:00",
"purchaser": { "firstname": "Billie", "lastname": "Buyer", "email": "billie@example.com" },
"line_items": [ { "ticketing_id": 22353, "unit_price": "100.00", "quantity": 2 } ],
"payment": { "amount": "200.00", "currency": "AUD", "reference": "payment-456" }
}
200 OK: the request is valid
{
"dry_run": true,
"valid": true,
"would_create": {
"attendees": 2,
"line_items": [ { "ticketing_id": 22353, "seats": 2, "unit_price": "100.00" } ],
"amount": "200.00",
"currency": "AUD"
}
}
422 Unprocessable Entity: the request would have been refused
{
"dry_run": true,
"valid": false,
"error_code": "invalid_payload",
"errors": [ "quantity must match the number of attendees" ]
}
Error codes and statuses are the same as the real call, so you can test your error handling against them. The dry_run and valid fields appear on refusals that came from checking the booking itself. Refusals raised before the booking is read — request_too_large, sensitive_payment_data, and request_audit_unavailable — return the standard error body with error_code and errors only. If the external_booking_id has already recorded a booking, a dry run with matching content returns that order rather than validating again:
{ "dry_run": true, "valid": true, "replayed": true, "order_id": 987 }
Changed content under a used external_booking_id returns 409 exactly as a real call does.
Two things to keep in mind:
- A valid dry run is not a reservation. Seats can sell out between the test and the real call.
- Dry runs run against your live listings, not a sandbox. The ticket IDs, prices, and availability are real.
Messages
WTM sends no messages for an external booking unless the request asks for them. All three flags are optional JSON booleans that default to false:
notify_customer_enrolmentsends the customer one enrolment confirmation per customer and ticket (seats seeded from the purchaser share one email). This is the email that carries the class details and the attendee details link.notify_customer_paymentsends the customer the payment confirmation.notify_vendorsends the vendor a booking notification for every attendee plus the payment notification.
Regardless of these flags, every attendee still gets WTM’s normal post-enrolment setup: class-pass coupons, LMS access for on-demand courses, review invites, and waiting-list removal.
The retired capture_attendee_details, email_opt_out, and notify_customer keys are rejected with 422.
Attendee details
External booking creation never rejects a request because of the vendor’s configured booking questions. Attendee, custom-field, and extra-field validation is bypassed while the booking is created.
After creation, WTM checks each attendee against the vendor’s booking rules: name and email, mobile where the vendor requires it, and every enabled extra field and required custom field for the ticket. Attendees that satisfy every rule are marked complete. Attendees that are missing details stay open, and the attendee confirmation email includes an “Update Attendee Information” link until the purchaser completes them through the WTM attendee page, where the normal configured validations apply.
The order also records registration_context.external_booking with the source, external_booking_id, booked_at, both message flags, and whether attendees were supplied explicitly.
Retry behavior
The combination of the authenticated vendor, source, and external_booking_id is the idempotency key. Retry a timed-out request with the same complete payload. Normalization ignores JSON object key ordering and the dry_run flag; values and array ordering must remain identical.
- An identical normalized payload returns the original order with
200 OKandreplayed: true. - The same key with changed quantity, attendees, message flags, requirements, ticket, payment, or other business data returns
409 Conflictwitherror_code: "idempotency_conflict". - Invalid content returns
400or422; a ticket outside the authenticated vendor returns404.
Do not reuse an external booking ID for a different booking.
Response statuses
| Status | Meaning |
|---|---|
| 201 | New booking recorded and completed |
| 200 | Identical idempotent replay, or a dry_run request; no duplicate order, attendee, payment, or inventory change |
| 400 | Malformed JSON (invalid_json) |
| 404 | A ticket does not exist for the authenticated vendor (not_found), or the API key is missing or unknown (empty body) |
| 409 | The idempotency key already exists with different business content (idempotency_conflict) |
| 413 | Request body exceeds 1 MiB (request_too_large) |
| 422 | Structurally invalid payload (invalid_payload), a business rule failed such as a sold-out ticket (booking_invalid), or payment credentials were sent (sensitive_payment_data) |
| 503 | The request audit could not be created, so no booking mutation was attempted (request_audit_unavailable) |
Example responses
200 OK: the same booking was already recorded and the payload matches
{
"order_id": 987,
"external_booking_id": "booking-123",
"source": "partner_portal",
"status": "completed",
"replayed": true
}
400 Bad Request: the body is not valid JSON
{
"error_code": "invalid_json",
"errors": ["Request body must be valid JSON"]
}
404 Not Found: the ticket does not belong to the authenticated vendor
{
"error_code": "not_found",
"errors": ["Ticket was not found"]
}
409 Conflict: same
sourceandexternal_booking_id, different content
{
"error_code": "idempotency_conflict",
"errors": ["External booking reference already exists with different content"]
}
422 Unprocessable Entity: the request shape is wrong
{
"error_code": "invalid_payload",
"errors": ["quantity must match the number of attendees"]
}
422 Unprocessable Entity: the request is well formed but a booking rule failed
{
"error_code": "booking_invalid",
"errors": ["Ticket is sold out."]
}
422 Unprocessable Entity: card or bank details were found in the body
{
"error_code": "sensitive_payment_data",
"errors": ["Payment credentials are not accepted by this endpoint"]
}
413 Payload Too Large
{
"error_code": "request_too_large",
"errors": ["Request body is too large"]
}
503 Service Unavailable: retry later with the same payload
{
"error_code": "request_audit_unavailable",
"errors": ["Request auditing is temporarily unavailable"]
}
Every error body has the same two keys: error_code and an errors array of human-readable strings. invalid_payload errors name the field that failed. booking_invalid errors are the same messages WTM shows at checkout. A missing or unknown API key returns 404 with an empty body before any of the above.
External transfers
Use this endpoint to move an attendee to another ticket owned by the same vendor, optionally selling them merchandise at the same time. The transfer may collect a fee from a reusable card already stored on the order.
Transfer an attendee
curl "https://api.weteachme.com/vendors/external_transfers" \
-X POST \
-H "Content-Type: application/json" \
-H "API-KEY: meowmeowmeow" \
-d '{
"order_id": 987,
"attendee_id": 654,
"to_product_id": 321,
"to_teaching_id": 432,
"to_ticketing_id": 543,
"subtotal": 35.00,
"transfer_charge": 10.00,
"merchandise_list": [
{ "merchandise_id": 111, "qty": 1, "subtotal": 25.00 }
],
"initiated_by": "vendor"
}'
require "httparty"
HTTParty.post(
"https://api.weteachme.com/vendors/external_transfers",
headers: { "Content-Type" => "application/json", "API-KEY" => "meowmeowmeow" },
body: {
order_id: 987,
attendee_id: 654,
to_product_id: 321,
to_teaching_id: 432,
to_ticketing_id: 543,
subtotal: 35.00,
transfer_charge: 10.00,
merchandise_list: [
{ merchandise_id: 111, qty: 1, subtotal: 25.00 }
],
initiated_by: "vendor"
}.to_json
)
A successful transfer returns
201 Created.
{
"transfer_id": 765,
"attendee_id": 654,
"status": "transferred",
"payment_status": "paid"
}
Request fields
| Field | Required | Description |
|---|---|---|
| order_id | Yes | Existing order containing the attendee |
| attendee_id | Yes | Active attendee to transfer |
| to_product_id | Yes | Destination WTM listing ID |
| to_teaching_id | Yes | Destination date/session ID |
| to_ticketing_id | Yes | Destination ticket ID |
| subtotal | No | Total charged; defaults to 0; a positive value must be at least 2.00 in every currency, and must equal transfer_charge plus every merchandise_list line |
| transfer_charge | No | Transfer fee alone, without merchandise; defaults to 0 |
| merchandise_list | No | Items to sell with the transfer, each { merchandise_id, qty, subtotal }. A line with qty of 0 is ignored. Every other merchandise_id must be an active merchandise of the same vendor |
| initiated_by | No | attendee (default) or vendor, and nothing else. Only attribution — it decides whether the activity log names the customer or the vendor as the actor |
| charge_now | No | Boolean override; omitted charges now when the subtotal is 0 or a reusable card exists |
payment_status is paid when the fee is settled during the request and pending_payment when the attendee is moved but the fee remains due. If a stored-card charge returns 422 with error_code: "charge_declined", the caller may retry with charge_now: false to complete the held, pay-later transfer.
| Status | Meaning |
|---|---|
| 201 | Attendee transferred; inspect payment_status |
| 404 | The order, attendee, destination listing, date, ticket, or merchandise is missing or belongs to another vendor; empty response body |
| 413 | Request exceeds 1 MiB (request_too_large) |
| 422 | Invalid amount, unrecognised initiated_by, same-ticket transfer, past cutoff, unavailable destination, payment failure, or rejected payment credentials (sensitive_payment_data) |
| 503 | Request auditing is unavailable (request_audit_unavailable); no transfer is attempted |
Successful requests move inventory and send the transfer confirmation asynchronously. This endpoint has no idempotency key; do not blindly replay a request that may already have succeeded.
External refunds
Use this endpoint to execute a refund decision that an external policy service has already approved. It refunds the named payment or issues a credit note, then cancels the named attendee spots and returns inventory.
Execute an approved refund
curl "https://api.weteachme.com/vendors/external_refunds" \
-X POST \
-H "Content-Type: application/json" \
-H "API-KEY: meowmeowmeow" \
-d '{
"payment_id": 9497,
"amount": 55.00,
"attendee_ids": [8844],
"refund_reason": "customer_requested",
"credit_note": false,
"note": "Approved by the partner cancellation policy"
}'
require "httparty"
HTTParty.post(
"https://api.weteachme.com/vendors/external_refunds",
headers: { "Content-Type" => "application/json", "API-KEY" => "meowmeowmeow" },
body: {
payment_id: 9497,
amount: 55.00,
attendee_ids: [8844],
refund_reason: "customer_requested",
credit_note: false,
note: "Approved by the partner cancellation policy"
}.to_json
)
A successful refund returns
201 Created.
{
"refund_id": 2468,
"status": "completed"
}
Request fields
| Field | Required | Description |
|---|---|---|
| payment_id | Yes | Payment to refund; must belong to the authenticated vendor |
| amount | Yes | Approved refund or credit-note amount; cannot exceed the remaining refundable amount |
| attendee_ids | No | Attendees whose spots should be cancelled; each must belong to the payment’s order |
| refund_reason | No | Approved reason, for example customer_requested or class_cancelled |
| credit_note | No | true issues vendor credit instead of refunding through the payment gateway |
| expiry_date | No | Optional credit-note expiry date |
| note | No | Internal note recorded with the refund |
All named attendees are validated before money moves. An already-cancelled attendee returns 422; a missing or foreign payment/attendee returns 404. A successful request cancels the attendees and decreases the associated ticket sold counts.
| Status | Meaning |
|---|---|
| 201 | Refund or credit note completed |
| 404 | Payment or attendee not found within the authenticated vendor’s booking; empty response body |
| 413 | Request exceeds 1 MiB (request_too_large) |
| 422 | Amount exceeds the refundable balance, an attendee is already cancelled, another refund rule fails, or payment credentials are rejected (sensitive_payment_data) |
| 503 | Request auditing is unavailable (request_audit_unavailable); no refund is attempted |
This endpoint has no idempotency key. After an ambiguous timeout, verify the payment and attendee state in WTM before deciding whether to retry.
Errors
Products and Orders
These endpoints follow JSON API. Errors come back as a JSON API errors array, each entry carrying status, title, and detail.
External endpoints
The external endpoints return plain JSON. When the endpoint defines an error code, the body is:
{
"error_code": "invalid_payload",
"errors": ["quantity must be a positive integer"]
}
Where no code is defined, the body is just errors:
{
"errors": ["Listing not found for product_id course-555"]
}
errors is always an array of human-readable strings.
| HTTP status | error_code |
Endpoints | When |
|---|---|---|---|
| 400 | invalid_json |
bookings | Request body is not valid JSON. |
| 404 | none, empty body | all | API key missing or unknown, or the record does not belong to the authenticated vendor. |
| 404 | not_found |
bookings | A ticketing_id in the request was not found. |
| 409 | idempotency_conflict |
bookings | Same source + external_booking_id as an earlier booking, but different content. |
| 413 | request_too_large |
all external | Request body over 1 MiB. |
| 422 | invalid_payload |
bookings | A field is missing, the wrong type, over its limit, or a retired key was sent. |
| 422 | booking_invalid |
bookings | The booking failed a business rule, for example a sold-out ticket. |
| 422 | charge_declined |
transfers | The stored card was declined for the transfer fee. Retry with charge_now: false. |
| 422 | sensitive_payment_data |
all external | Card or bank credentials were found in the body. They are never accepted. |
| 422 | none | products, dates and tickets, transfers, refunds | Validation failed; see errors. Dates and tickets also returns message. |
| 503 | request_audit_unavailable |
all external | Request auditing is down. Retry later; nothing was created. |